반응형

1. 개요

  • Connection을 생성한 상태에서 Controller, Service, Repository, Mapper 구조를 통해 DB 데이터를 Select 하자
  • Controller, Service, Repository 클래스에 대한 자세한 설명은 하지 않겠다.

2. 준비

  • postgreSQL DB의 Select 테이블
  • mybatis 연동 경험

3. Controller 생성

  • 먼저 클라이언트의 요청을 받을 Controller 클래스를 생성한다.
  • 필자는 각각 Controller, Service, Repository 모두 각각의 패키지를 만든 후 생성했다.
  • test-select로 오는 get 요청에 대해 testService 인터페이스 구현체 응답값을 리턴하도록 하였다.
@Controller
public class TestController {

	@Autowired
	private TestService testService;
	
	@GetMapping("/test-select")
	@ResponseBody
	public List<BoardDto.Info> testSelect(){
		
		return testService.testSelect();
	}
}

4. ServiceInterface 생성

  • 서비스 인터페이스를 생성한다.
public interface TestService {

	public List<BoardDto.Info> testSelect();
}

5. Service 구현체 생성

  • TestService Interface의 구현체 클래스를 생성한다.
  • TestRepository를 Autowired 한다.
@Service
public class TestServiceImpl implements TestService{

	@Autowired
	private TestRepository testRepository;
	
	@Override
	public List<Info> testSelect() {

		return testRepository.testSelect();
	}

}

6. TestRepository 인터페이스 생성

  • 리포지토리 인터페이스를 생성한다.
  • 이 클래스는 인터페이스이기때문에 @Repository만 입력 시 구현체가 없으므로 Service 클래스에서 TestRepository를 주입에 실패했다는 에러가 발생한다. @Mapper 어노테이션은 이러한 인터페이스를 mybatis의 매퍼로 등록해주기 위해 사용된다. 즉 Mapper Bean이 되는것이다.
@Repository
@Mapper
public interface TestRepository {

	public List<Info> testSelect();
}

 

  • 하지만 이렇게 Mapper 어노테이션을 명시적으로 선언하게 되면 생성되는 모든 Repository에 다 넣어줘야한다. 이게 귀찮다면 DatabaseConfig 클래스에 @MapperScan("패키지 경로") 어노테이션을 선언해주자. 그럼 패키지 경로에 포함된 인터페이스에 대해 @Mapper 어노테이션을 선언한 효과를 얻을 수 있다.
@Configuration
@MapperScan("com.modu.repository")
public class DatabaseConfig {

	...
	
}

7. TestMapper 생성

  • Mapper.xml을 생성한다.
  • 필자의 경우 Dto 클래스를 static inner class 형식으로 사용하기 때문에 resultType에 $가 포함되어 있다. 만약, static inner class를 사용하지 않는다면 패키지 경로를 넣어주면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
<mapper namespace="com.modu.repository.TestRepository">

	<select id="testSelect" resultType ="com.modu.dto.BoardDto$Info">
		SELECT
			board_seq
			,cat_id
			,title
			,content
			,writer
			,create_date
			,update_date
		FROM
			tbl_board
	</select>
</mapper>

 


8. 테스트

응답 값

 


 

9. 마치며

  • hikariCP와 mybatis, postgreSQL을 연동해보았는데 그저 머릿속에 있는 DB 통신에 대한 패턴을 별 생각없이 구현했다. DB 설정 클래스의 bean들은 각각 역할을 하는지, Mapper.xml 파일에 등록한 쿼리는 내부적으로 어떻게 생성되어 실제 쿼리가 처리되는지, Repository 인터페이스와 Mapper의 id가 어떻게 매핑되는지가 궁금해졌다. 다음 포스팅에서는 이런 설정 하나하나가 시스템적으로 어떻게 돌아가는지 알아봐야겠다.
반응형
반응형

1. 개요

 - SpringBoot 환경에서 hikariCP, mybatis, PostgreSQL DB를 연동해보자!


2. 환경

 - SpringBoot

 - Gradle

 - PostgreSQL

 - mybatis

 - JDK 1.8


3. hikariCP란?

  • hikari : '빛'의 일본어, CP : ConnectionPool, 뭐다? 빛처럼 빠른 Connection Pool !(?) 이라는 의도로 히카리라고 지었는지는 모르겠지만, JDBC ConnectionPool 중 하나이다. (실제로 tomcat ConnectionPool 보다 성능이 좋다)
  • ConnectionPool이란, 서버 시작 시 DB와의 Connection을 일정 개수 생성하여 Pool에 저장시켜 놓는 기술이다. 그 후 DB에 접근할 일이 생기면 Pool에서 놀고있는 Connection을 가져다 쓰고, 사용이 끝나면 다시 반환시켜 놓는다. ConnectionPool을 사용하지 않으면 트랜잭션 요청이 들어올때마다 Connection을 맺는 작업을 해야하는데, 접속이 몰리게 되면 서버에 부하를 가져올 수 있다. 그래서 일반적으로 ConnectionPool 방식을 많이 사용한다.
  • 결론은 hikariCP란 JDBC ConnectionPool이며, 스프링 부트 2 버전에서는 jdbc 의존성 주입 시 기본적으로 요녀석을 제공할 만큼 똑똑하고 빠른놈이란걸 알 수 있다.

 4. 의존성 주입

    •  build.gradle에 postgreSQL, jdbc, mybatis에 대한 의존성을 추가해주자.
//postgreSQL
runtimeOnly 'org.postgresql:postgresql'

//jdbc (hikari)
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

//mybatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'

5. hikari 설정

  • hikari 설정은 크게 DB 접속 정보Connection Pool 설정으로 구성된다. application.properties 파일에 다음과 같이 추가하도록 하자.
# DB 접속정보
spring.datasource.hikari.driver-class-name=org.postgresql.Driver
spring.datasource.hikari.jdbc-url=jdbc:postgresql://[URL]:5432/[DB명]
spring.datasource.hikari.username=[접속 ID]
spring.datasource.hikari.password=[접속 PW]
spring.datasource.hikari.pool-name=[PoolName - 임의]

# CP Setting
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.maximum-pool-size=50
spring.datasource.hikari.minimum-idle=30
spring.datasource.hikari.idle-timeout=60000
  • DB 접속정보와 ConnectionPool 셋팅 정보를 입력해주었다. CP Setting 정보는 다음과 같다.
  • connection-timeout : 클라이언트가 Pool에 Connection을 요청했을 때 기다리는 시간(ms)이다. 만약 최대 50개의 Connection을 생성해둔 상태에서 50개의 요청이 한번에 들어올 경우 51번째 클라이언트는 텅텅 비어있는 Pool에서 유휴 Connection을 기다릴 수밖에 없다. 그 참을성에 대한 시간이다. 30초동안은 기다린다는 뜻이며, 만약 30초가 지날 경우 ConnectionTimeoutException이 throw된다. 이 경우 connection-timeout 값을 늘려주거나, maximum-pool-size를 늘려줘야한다.
  • maximum-pool-size : Pool에 저장할수 있는 Connection의 최대 개수이다.
  • minimum-idle : Pool에서 저장시켜야 할 Connection의 최소 개수이다. 서버 최초 기동 시 Pool에 Connection을 생성하는데 minimum-idle을 설정할 경우 maximum-pool-size만큼 생성하지 않고 minimum-idle개수만큼 생성한다. 30개는 유휴상태로 유지시키는 것이다. 만약 DB 통신 중 에러가 발생하여 한개의 Connection이 폐기되어 유휴 커넥션이 29개가 되면 이 개수를 맞추기 위해 1개의 Connection을 생성하게 된다.
  • idle-timeout : minimum-idle 개수를 넘어가게 되면 Connection을 사용하고 Pool에 반환하는게 아닌 폐기시킨다. 앞서 말했듯이 30개의 Connection만 유휴상태로 유지시키기 때문이다. 그런데 요청이 계속 들어오면 오히려 폐기하는 것보다는 유휴상태로 유지시키는 것이 효율적인 상황이다. minimum-idle 개수를 넘어간 상황에서 Connection 추가 생성 후 해당 커넥션을 일정 시간 유휴상태로 유지시키는 설정이 idle-timeout이다. 이 설정을 넣지 않으면 바로 폐기가 될까?라고 생각할 수 있지만 그것도 아니다. default 값이 60000이기 때문에 1분동안은 유지되다가 더이상 사용되지 않을 경우 폐기된다.

6. DatabaseConfig 클래스 생성

  • 의존성 주입 및 application.properties에 CP 설정을 마친 상태에서 서버를 기동하면 다음과 같은 에러가 발생한다.
    Consider the following:
    	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
    	If you have database settings to be loaded from a particular profile you may need to activate it (the profiles oauth,prod are currently active).
     
  • H2, HSQL db에 대한 classpath를 추가해달란 것인데, 이 오류가 뜨는 이유는 앞서 설정했던 설정값들이 현재 어플리케이션에 적용이 되지 않아 default DB인 H2, HSQL로 셋팅이 되고, 실제 application.properties에는 이에 대한 설정값이 없기 때문에 발생하는 에러이다. DB 설정하는 Configuration 클래스를 만들면 해결이 된다.

 

  • DatabaseConfig.Class
package com.modu.config;


import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

@Configuration
public class DatabaseConfig {

	@Bean
	@ConfigurationProperties(prefix = "spring.datasource.hikari")
	public HikariConfig hikariConfig() {
		return new HikariConfig();
	}
	
	@Bean
	public DataSource dataSource() {
		return new HikariDataSource(hikariConfig());
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
		sessionFactory.setDataSource(dataSource);
		PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
		sessionFactory.setMapperLocations(resolver.getResources("mapper/*.xml")); 	//mapper 파일 로드
		sessionFactory.setConfigLocation(resolver.getResource("mybatis-config.xml"));//mybatis-config 로드
		return sessionFactory.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception{
		final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
		return sqlSessionTemplate;
	}
}
  • ConfigurationProperties(prefix = "spring.datasource.hikari") 구문은 prefix로 시작하는 properties 값들을 추출하여 특정 객체에 바인딩 시키는 어노테이션이다. 즉, hikari 관련 설정들을 HikariConfig Bean 객체 생성 시 바인딩을 시키는 것이다.  바인딩 시 완화된 규칙이 적용되어 있어 jdbcUrl이라는 변수에 바인딩 시 jdbc-url, jdbc_url도 정상적으로 바인딩된다. 실제로 bean 객체를 Autowired 하여 debug 해보면 properties에 설정한 데이터들이 객체화되어 들어가고 있음을 알 수 있다. 나머지 값들을 HikariConfig Bean 생성 시 기본으로 주입된 값들이다.

hikariConfig

  • DataSource를 생성할 때 위에서 만든 객체를 HikariDataSource의 생성자로 주입하면 히카리 CP에 대한 Datasource 객체가 생성된다.
  • mybatis 설정파일 및 mapper 파일을 로드하는 설정에 맞게 resource 경로에 mybatis-config.xml 파일과 mapper 폴더를 생성해준다.
  • 서버를 기동하면 정상적으로 기동됨을 확인할 수 있다. 추가적으로 로그가 설정되어 있다면 debug 레벨에 다음과 같이 Connection이 생성되었다는 로그를 확인할 수 있다.

Connection 생성 완료

 

실제 DB에서 데이터를 조회해보는 것은 다음 게시글에 포스팅하도록 하겠다.

반응형
반응형

1. 개요

 - 회사 로컬 PC에 STS 최신버전 (4.12.1) 설치 후 Lombok 연동 시 에러 발생


2. 환경

 - STS 4.12.1

 - Lombok 1.18.20


3. 에러 상황

 - 개인 PC에 설치한 STS에서는 Lombok이 정상기동되었음.

 - git을 통해 clone하여 소스 로드함.


4. 조치사항

 - 구글링을 통해 다음과 같은 조치를 하였으나 안먹힘

 1) SpringToolSuite4.ini의 마지막줄에 -javaagent:lombok.jar 코드 추가 (x)

 2) SpringToolSuite4.ini의 마지막줄에 -vmargs -javaagent:lombok.jar 코드 추가 (x)

 3) SpringToolSuite4.ini의 -vmargs 구문의 제일 위에 -javaagent:lombok.jar 코드 추가 (x)

 4) SpringToolSuite4.ini의 마지막줄에 --illegal-access=warn --add-opens java.base/java.lang=ALL-UNNAMED 추가 (x)
 5) PC 재부팅, 프로젝트 clean (x)


5. JDK 16의 보안정책 강화

 - JDK 16부터 보안정책이 강화되어 최신의 STS에 Lombok 라이브러리 연동 시 에러가 발생한다고 함.

https://github.com/projectlombok/lombok/issues/2810

 

[BUG] Unhandled event loop exception in Eclipse · Issue #2810 · projectlombok/lombok

After updating Eclipse to use Java 16, building projects gives an error. Install Lombok 1.18.20 in Eclipse, either through the update site or the jar (I tried both). If you used the update site, yo...

github.com


6. 해결

 - 최신버전 멈춰! STS 4.9.0 버전으로 재설치

 - 스무스하게 동작. 역시 최신버전보단 검증된 버전으로..

 

 

반응형
반응형

1. 개요

 - Git에서 관리하던 Gradle 프로젝트를 Clone한 후 빌드 시 org.gradle.wrapper.GradleWrapperMain가 발생하였다.


2. 원인

 - org.gradle.wrapper.GradleWrapperMain 패키지 클래스는 gradle-wrapper.jar 파일 내의 패키지이다. 이 파일이 프로젝트 내에 없었고, 그로 인해 위 클래스를 찾지 못해 발생한 에러였다. 어디선가 누락이 된것이다. 

 - 기본적으로 gradle-wrapper.jar는 '프로젝트 루트 디렉토리/gradle/wrapper' 경로에 있는데, 에러가 발생했던 프로젝트에는 이 파일이 누락되어 있었다.

gradle-wrapper.jar


3. 누락은 어디서?

 - 필자의 경우 gitignore에 *.jar 가 있었고, 그로인해 누락되었다. gitignore의 default 코드에 작성되어있어 신경을 쓰지 못한 것이 화근이었다.

gitignore


4. 해결

 - gradle.wrapper.jar파일을 gradle/wrapper 경로에 넣어주면 된다. 필자의 경우 다른 프로젝트에 있는 jar파일을 가져왔지만, 아래 링크를 통해 다운받아도 무관하다.

https://mvnrepository.com/artifact/org.gradle/gradle-wrapper/5.2.1

 

 

 

반응형
반응형

1. 개요

 - Git webhook을 사용하여 배포 자동화 시스템을 구축해보자.


2. 준비

 - Jenkins

 - Git 레포지토리

 - 스프링 부트 / Gradle 프로젝트 (jar파일 배포)

 


3. webhook이란?

 - webhook이란 원격 저장소의 소스에 push, commit 등의 이벤트가 발생하면 Jenkins와 같은 CI 서버에 해당 이벤트를 전달하는 기능입니다. Jenkins에서 이 이벤트 정보를 받아 리빌드, 배포와 같은 작업을 연계하여 진행해보도록 하겠습니다.

 


4. Item 생성

 4.1. Freestyle project를 생성합니다. 

freestyle 프로젝트 생성

 

 4.2. 설명 및 GitHub project url을 입력합니다. 실제로 여기에 입력한 Git 주소가 연동에 사용되진 않습니다.

 

 4.3. 소스코드 관리로 Git을 체크한 후, 실제로 연동할 Git 주소를 입력합니다. 그럼 인증을 하라는 에러가 발생하며,  Git 계정 인증 시 'password 방식'이 지원을 하지 않으니 'Token authentication 방식'을 사용하라고 합니다. 그럼 토큰을 만들어야겠죠? 토큰은 깃허브 홈페이지에서 만들 수 있습니다.

Git 주소 연동

 

 4.4. 하던 작업을 잠깐 멈추고 '깃허브 로그인 / settings / developer settings / Personal access tokens' 탭에 들어가 Generate new token 버튼을 선택합니다.

token 생성

 

 4.5. 리포지토리에 대한 전체 접근 권한 및 hook 권한을 체크한 후, Generate Token 버튼을 눌러 토큰을 생성합니다.

  * repo, admin:repo_hook을 체크하면 됩니다.

  * Expiration을 No expiration으로 설정하면 토큰 만료 기간 없이 계속 사용 가능합니다.

token 권한 설정

 

 4.6. 생성된 토큰 값을 확인 후 복사합니다.

 * 참고로 해당 페이지를 넘어가게 되면 현재 발급된 토큰 키값을 다시 확인할 수 없으며, 분실 시 재발급 받아야합니다.

토큰 값 확인 및 복사

 

 4.7. Jenkins로 돌아가 Credentials / Add를 선택 후 Kind에는 Username with password, UserName에는 GitHub ID, Password에는 복사한 토큰 키를 입력합니다. 아래 ID와 Description은 식별자와 설명입니다. 임의로 입력 후 Add를 누릅니다. 에러가 사라졌다면 인증이 완료된 것입니다.

자격증명 등록

 

 4.8. Branch는 Master로 설정합니다.

 

 4.9. 빌드 유발은 GitHub hook trigger for GITScm polling을 선택합니다. 이 옵션이 있어야 webhook을 통해 Jenkins로 들어온 push 이벤트 정보를 인식하여 빌드를 유발할 수 있습니다. 실제 빌드는 다음 Build 탭에서 설정합니다.

빌드 유발

 

 4.10. 필자의 경우 Gradle SpringBoot 프로젝트이기때문에 Invoke Gradle Script를 선택 후 Tasks에 bootJar를 입력했습니다. build.gradle 파일에는 다음 코드를 입력하여 modu.jar라는 파일명으로 빌드되도록 설정한 상태입니다.

 * jar 파일 빌드가 완료되면 빌드 후 조치 부분에 빌드된 jar 파일을 실행시키도록 하여 스프링 부트에 내장되어 있는 tomcat 서버를 통해 서비스를 기동할 예정입니다.

build.gradle

 

Build 설정

 

 * Gradle Version에서는 빌드에 Gradle을 지정해줘야하는데 이는 Global Tool Configuration 탭에서 설정이 가능합니다.

 

 4.11. 빌드 성공 시 start.sh라는 스크립트를 실행하도록 합니다. 해당 스크립트안에는 현재 실행되고 있는 프로세스가 있다면 kill 후 재시작하는 코드가 작성되어 있습니다. Script에 해당하는 경로에 start.sh 파일을 적절히 커스터마이즈하여 생성해줍시다.

 

 * start.sh

#!/bin/bash

echo "PID Check..."
CURRENT_PID=$(ps -ef | grep java | grep modu | awk '{print $2}')

echo "Running PID: {$CURRENT_PID}"

if [ -z ${CURRENT_PID} ] ; then
        echo "Project is not running"
else
        echo "Kill Current PID"
        kill -9 $CURRENT_PID
        sleep 10
fi

echo "Deploy Project...."
nohup java -jar /var/lib/jenkins/workspace/practice-project/build/libs/modu.jar >> /home/ec2-user/practice-script/modu.log &

echo "Done"

~

 

 

 이걸로 간단한 형태의 Item 생성 및 설정이 끝났습니다. 이제 마스터 브랜치로 push를 하여 자동으로 빌드 및 jar파일을 실행시켜 배포가 되는지 확인해보도록 하겠습니다.


5. Git Push

 5.1. 소스코드 수정 후 GitHub Desktop 프로그램을 사용해 commit / push를 진행합니다.

push !


6. Jenkins 자동 빌드 및 배포 확인

 6.1. Push를 하면 자동으로 Jenkins 빌드가 시작됩니다. Build History에서 확인이 가능합니다.

자동 Build

 6.2. 해당 빌드 선택 후 Console Output을 확인하면 빌드 로그를 확인할 수 있습니다. 확인 결과, 빌드에 성공하여 BUILD SUCCESSFUL를 뿌리고 있습니다. 그에 따라 start.sh 스크립트를 실행하는 로그도 확인되고 있습니다. 스크립트도 정상적으로 실행되었으니 실제로 서비스가 올라갔는지 확인해보도록 하겠습니다.

 

Build Log

 

 6.3. public DNS로 조회하여 서비스 확인 결과, 정상적으로 jar파일이 실행되어 tomcat 서비스가 올라간 것을 확인할 수 있습니다.

서비스 정상 기동 확인


7. 마치며...

 Jenkins를 사용하여 정말 간단한 구조의 DevOps 환경을 구성해보았습니다. SpringBoot / Gradle 프로젝트, Git을 통한 소스관리, Jenkins를 통한 자동 빌드 및 배포, Swagger를 통한 REST API Interface를 제공하게 되었네요.. 뿌듯..

 아무래도 프리티어 서버를 통해 작업을 진행하다 보니 중간중간 메모리 부족으로 인한 문제도 발생하였습니다. 간혹가다 빌드 시 OutofMemory가 발생하거나, 서버 자체가 꺼져버리는 현상이었습니다. 그럴때마다 AWS EC2 인스턴스를 재시작했었는데, 현재는 Swap Memory를 0 > 2GB로 늘려주어 해결한 상태입니다. 관련 포스팅 다음에 진행하여 링크를 남겨놓도록 하겠습니다!

반응형
반응형

1. 개요

 - Amazon Linux 2 OS에 Jenkins 설치


2. 준비

 - Amazon Linux 2 서버


3. 데몬화 모듈 설치

 - Jenkins를 설치하는 방법 중 하나는 yum 명령어를 이용하는 것이다. 그런데 Amazon Linux 2 서버에서는 yum 명령어를 사용하여 설치했을 때 daemonize 에러가 발생한다.

Package: jenkins-2.308-1.1.noarch (Jenkins) Requires: daemonize

 

 - Amazon Linux 2 OS는 다른 linux os와는 다르게 daemonize 라는 모듈을 기본으로 지원하지 않는다. 때문에 daemonize를 install 해줘야한다. 다음 명령어를 통해 설치를 진행하자

vim /etc/yum.repos.d/epelfordaemonize.repo // epelfordaemonize.repo 파일 생성

-- 다음 코드 입력 후 저장 --
[daemonize]
baseurl=https://download-ib01.fedoraproject.org/pub/epel/7/x86_64/
gpgcheck=no
enabled=yes

 

 - 만약 저장 시 readonly 에러가 뜰 경우 :wq가 아닌 다음 명령어를 입력하자

:w !sudo tee % > /dev/null

 

 - 저장이 완료되면 모듈을 설치한다.

yum install daemonize -y //daemonize 설치

4. Jenkins 설치

 - 데몬화 모듈 설치가 완료되면 Jenkins를 설치한다.

 - 이왕이면 현재 서버의 Java 버전과 동일한 버전으로 Jenkins를 설치한다.

yum install jenkins java-1.8.0-openjdk-devel -y

 


5. Jenkins 실행

 - 설치가 완료되면 Jenkins를 실행한다. 만약 권한 에러가 뜨면 sudo를 붙여주자

sudo service jenkins start  //jenkins start
sudo service jenkins stop   //jenkins stop

 

 - 시작 명령어를 입력했을 때 다음과 같은 로그가 출력될 경우 정상적으로 서비스가 올라간 것이다.

jenkins start

 - 프로세스 확인 명령어로 실행 여부를 확인 가능하다. port, pid 등의 정보가 조회된다.

ps -ef | grep jenkins

프로세스 정보

 

 - 이제 실제 Jenkins 페이지로 접속해보자. 기본적으로 ip:8080 포트를 입력하면 접속된다.

시작화면

  - 만약 8080 포트가 이미 사용중이라면 포트를 변경해주어야한다. 필자의 경우 9090으로 변경 후 AWS 보안그룹에 내 IP 에 대해 9090 포트를 오픈해주었다. 포트 변경 명령어는 다음과 같다.

sudo vim /etc/sysconfig/jenkins //jenkins 설정파일

코드에서 JENKINS_PORT 값을 원하는 포트로 수정 후 저장

 

  -  수정한 포트는 Jenkins 재시작 시 적용된다.

 


6. 마치며

 - 다음은 Jenkins와 git, gradle을 연동하여 git 마스터 브랜치에 push가 갈 경우 빌드 및 배포 자동화에 대한 블로깅을 하도록 하겠다. 실제로 작업은 해놓았으나, 내용 전달을 위한 정리가 아직 되지않았다.

반응형
반응형

1. 개요

  • AWS 서버에 스프링 부트 프로젝트를 jar파일 배포한다.

2. 준비

  • Amazon Linux 2 서버
  • Spring Boot + gradle 프로젝트
  • JDK 설치
  • Git 설치 및 Source Clone

3. JDK 설치

  • 프로젝트를 배포하기 전 AWS 서버에 JDK를 설치해야합니다. SpringBoot의 build.gradle 파일의 sourceCompatibility를 확인해봅시다. sourceCompatibility는 현재 프로젝트에 호환된 Java 버전을 의미합니다.

프로젝트 java 버전 확인

 

  • 필자의 경우 java 1.8 이므로 aws서버에 openJDK 1.8 버전을 설치를 위해 다음 명령어를 입력하겠습니다.
 sudo yum install -y java-1.8.0-openjdk-devel.x86_64

 

  • 설치가 완료되었다면 인스턴스의 Java 버전을 8로 변경해야합니다. 다음 명령어를 입력 후 인스턴스에 적용할 java 버전의 번호를 입력합니다.
sudo /usr/sbin/alternatives --config java

자바 버전 선택

  • 필자의 경우 방금 설치한 java 버전만 있고, Selelction이 1로 지정되어 있으므로 입력창에 1을 입력합니다.
  • 설정이 완료되면 서버의 java 버전을 확인합니다.
java -version

자바 버전 확인

  • 설정한 Java 버전이 조회된다면 스프링 부트 jar 배포에 대한 기본적인 준비는 끝났습니다.이제 배포를 해봅시다!

4. Git 설치 및 Clone

  • 프로젝트 파일이나 빌드된 파일을 AWS 서버로 전송 해야합니다. 이 과정에 대해 채택할 수 있는 방법은 FTP가 될수도 있지만 필자의 경우 Git Clone을 통해 프로젝트 파일을 AWS 서버에서 받은 후 서버 내에서 빌드를 하는 방식을 채택했습니다.
  • 다음 명령어를 입력해 AWS 서버에 git을 설치 및 확인합니다.
sudo yum install git // git 설치
git --version //git 설치(버전) 확인

git 설치버전 확인

  • 설치가 완료되면 clone 될 디렉토리를 생성한다. 필자의 경우 홈 디렉토리 기준으로 "app" 폴더를 생성하여 그 안에 git에서 clone한 소스를 넣겠습니다.
mkdir ~/app  //app 폴더생성

 

  • 폴더 생성이 완료되면 본인의 깃허브 웹페이지에서 주소를 복사합니다.

Clone할 url 복사

  • AWS 서버에 다음 명령을 입력하여 clone을 진행합니다.
git clone 복사한 주소

 

  • clone이 정상적으로 완료되면 app 폴더 안에 프로젝트 소스들이 들어갔을겁니다. 이제 배포를 진행할 차례입니다.

5. 배포

  • 프로젝트 경로로 들어가 다음 명령어를 입력해 jar 빌드합니다.
./gradlew bootjar

 

  • build가 완료되면 자동적으로 build/libs 폴더 내에 "프로젝트 명/버전.jar" 형식으로 파일이 생성됩니다. 배포는 간단합니다. 이 파일을 실행시켜주면 됩니다. 스프링 부트는 jar 파일 빌드 시 tomcat 서버가 내포된 형태로 빌드되기 때문에 별도의 tomcat 서버가 필요없습니다.
java -jar jar파일 명

jar 파일 실행

  • 배포가 완료되었습니다. 배포된 서버로 접속하기 위해 '퍼블릭 DNS:포트'를 입력해봅시다.
  • 퍼블릭 DNS는 AWS 인스턴스 상세정보로 들어가면 확인할 수 있으며 포트는 배포 로그의 마지막 부분에서 확인할 수 있습니다.

퍼블릭 DNS

 

  • 짜잔~ 다음과 같이 정상적으로 접속이 되었습니다.

배포된 어플리케이션 접속 성공 화면

 

  • 만약 접속이 되지 않으신다면 AWS내에 설정한 보안 그룹에 해당 포트가 오픈되어있는지 확인해봅시다. 필자의 경우 해당 포트가 열려있지 않아 최초에 접속이 되지 않았습니다.
  • 인스턴스 세부정보 > 보안 > 인바운드 규칙에 8090포트를 추가해줍시다.
    인바운드 규칙 추가

6. 마치며...

  • Git 연동 및 클론, 빌드, AWS 배포를 해보았습니다. 되게 간소하게(?) 진행했지만 AWS 서버에 배포를 해본건 처음이라 감회가 새로웠다. 다음 목표는 Jenkins를 연동하여 git commit이 있을 때 자동으로 AWS 서버에 수정된 코드가 배포되도록 하는 것이다. 이 작업이 끝나면 본격적인 서버 로직을 작업할것이다
반응형
반응형

1. 개요

 - 프리티어 계정으로 EC2 서버를 생성한다.

 - 생성된 EC2 서버에 접속한다.


2. 환경

 - Window 10

 - AWS 계정 생성 완료


3. EC2 란?

 - Elastic Compute Cloud의 약자로 AWS에서 제공하는 사양, 용량 등을 유동적으로 사용 가능한 서버를 말합니다.

 - 프리티어 계정은 이 중 t2.micro(CPU 1, 메모리 1GB, 용량 최대 30GB)라는 서버를 1년동안 무료로 사용 가능합니다. 

 - 프리티어 기준으로 월 750 시간의 제한이 있으며 초과하면 과금이 발생합니다. 하지만 24*31은 744 시간이므로 한대의 프리티어 서버를 운용한다면 과금은 발생하지 않습니다.


4. EC2 t2.micro 서버 생성

 4.1. 검색창에 ec2 검색 및 선택

ec2 검색

 

 4.2. 인스턴스 시작 버튼 선택

  - 화면 진입 시 상단에 위치한 인스턴스 시작 버튼을 선택합니다.

인스턴스 시작

 

 4.3. OS 선택

  - 프리티어로 사용 가능한 Amazon Linux 2 AMI 64비트를 선택합니다.

 

 4.4. EC2 유형 선택

  - 프리티어로 사용 가능한 t2.micro 를 선택합니다.

 

 4.5. 인스턴스 세부 정보 구성

  - 이 부분에서 딱히 설정해야할 부분은 없습니다. 넘어갑시다.

 

 4.6. 스토리지 설정

  - t2.micro 에서 지원 가능한 최대 스토리지 용량인 30GB를 입력합니다.

 

 4.7. 태그 추가

  - Name 이라는 Key를 생성하고 값(ex. 프로젝트 명)을 입력해줍니다.

 

 4.8. 보안 그룹 추가

 - [SSH, 내 IP] : SSH를 통해 서버에 접근할 수 있도록 추가합니다. '나'만 접근 가능하도록 하기 위해 '내 IP'로 설정하였습니다. 만약, 서버에 접근해야할 동료가 있다면 SSH 유형을 하나 더 만들어 추가해줍시다.

 - [사용자 지정 TCP, 8080] : 8080 포트로 모든 IP가 접근 가능하도록 설정하였습니다.

 - [HTTPS, 443] : 443 포트로 모든 IP가 접근 가능하도록 설정하였습니다.

 

 4.9. 검토

  - 다음 항목을 검토 후 이상이 없다면 시작버튼을 클릭합니다.

   - OS : Amazon Linux 2

   - 인스턴스 유형 : t2.micro

   - 보안그룹

   - 스토리지 : 30GB

 

 4.10 키페어 설정

 - 키 페어 설정을 하게 되면 서버 접근 시 접근자의 개인키와 서버의 공개키를 체크합니다. 보안그룹을 설정했지만 키페어 설정도 꼭 필요합니다.

  - 현재 보안 그룹 설정을 했기 때문에 마스터 서버로의 접근은 '내 IP'에서만 가능합니다. 악의적인 사용자가 '나'의 서버 접근 정보를 알고있다고 하더라도 IP가 달라 접근이 불가능합니다. 하지만 극단적인 예로, 내 옆집에 사는 사람이라면 제 공인 IP로 접근이 가능할 수 있고, 그렇게 되면 제 서버로의 접근도 가능할 수 있습니다. 만약, 사내 프로젝트를 위해 EC2 서버를 생성하고 보안그룹을 '내 IP'로 설정하였습니다. 그렇게되면 같은 사내망을 사용하는 모든 사람들이 접근 가능하게 됩니다.

  - 이런 유형의 접근을 막기 위해 필요한 것이 바로 키페어입니다. 키페어를 설정하면 서버의 공개키와 대응되는 개인키를 발급받게 되고, 실제 접근 시 이 키파일을 서버로 제공해야합니다.

 - RSA 유형으로 키 페어 생성 선택 후 키 페어 이름에 프로젝트 명을 넣습니다. 그 후 키 페어 다운로드를 클릭합니다.

 - pem 확장자의 개인키 파일 다운로드가 확인되면 인스턴스 시작 버튼을 클릭합니다.

 

 4.11. EC2 인스턴스 생성 완료

  - 다음과 같은 화면이 나올 경우 EC2 인스턴스가 생성 완료된것입니다.

  - 이제 개인키 파일을 통해 내가 만든 EC2 서버에 접속해볼 차례입니다.


5. EC2 서버 접속

 - 서버에 접근하기 위해서는 몇가지 과정이 필요합니다. 차근차근 밟아봅시다.

 

 5.1. putty, puttygen 설치

  - putty : SSH를 통한 서버 접속에 사용합니다.

    * 설치 url : https://putty.softonic.kr/

 

PuTTY

완전하고 안정적인 텔넷 및 SSH 클라이언트

putty.softonic.kr

  - puttygen : 서버로 접근하기 위해서는 개인키 파일(.pem)을 .ppk 확장자로 변환해야 하며, 이 변환에 사용합니다. 

    * 설치 url : https://www.puttygen.com/download-putty

 

 5.2. puttygen 실행

  - puttygen.exe 실행 후 Conversions > import key 를 선택합니다.

import key

 5.3. 개인키 파일 임포트

  - 아까 발급받은 개인키 파일을 선택합니다.

pem 파일 선택

 

 5.4. ppk 파일 생성

  - 다음과 같은 화면이 조회되면 Save private Key 버튼을 선택하여 ppk 파일을 생성합니다.

  - 알림 메시지가 하나 뜨는데 무시하고 예를 누릅니다.

  - 파일이 생성되었다면 이제 서버로 접속할 차례입니다.

ppk 파일 생성

 5.5. putty 실행

  - putty를 실행한 후 HostName에 ec2-user@[퍼블릭 DNS]를 입력합니다.

  - ec2-user는 Amazon Linux 2 생성 시 자동 생성되는 ID 입니다. 다른 OS를 선택했다면 헤매지 말고 구글링!

HostName 입력

  

  - 만약, 퍼블릭 DNS를 모르겠다면 EC2 콘솔에 접속해 인스턴스 상세보기로 확인할 수 있습니다.

 

 5.6. Auth 설정

  - putty 카테고리의 Connection > SSH > Auth를 선택 후 Browse 버튼을 선택하여 생성한 ppk 파일을 로드합니다.

Auth 설정

 5.7. Open!

  - 모든 설정이 끝났습니다. 이제 Open 버튼을 눌러 서버에 접속해봅시다!

  - 다음과 같은 화면이 뜬다면 성공입니다!

  - 만약 에러가 뜨신다면 다음을 확인해보세요.

    1) 서버에 대한 개인키가 맞는지

    2) OS에 대한 ID (ex. amazon linux = ec2-user)가 맞는지

    3) HOST NAME에 퍼블릭 DNS가 정확하게 입력됐는지

  - 저의 경우 2번과 3번이 잘못되어 시간이 걸렸답니다 ㅠㅠ.


6. 마치며

 - 예전에 책을 보며 무작정 따라한 적이 있는데, 이게 무슨 설정이고, 왜 하는지도 이해가 가지 않았었던것 같아요. 용어의 의미와 역할을 이해하려고 노력하며 다시 책을 따라해보세요! 훨씬 의미있고 기억에 남을거에요!

반응형
반응형

1. 개요

 - 스프링 부트 환경에서 기본으로 제공하는 LogBack을 사용하여 로그를 남겨보자

 - spring.profiles.active를 사용하여 운영, 개발 환경에 따라 로그 설정을 분기하여 적용해보자

 [참고 : https://goddaehee.tistory.com/206 깃대희의 작은공간]

 

2. LogBack이란?

 - LogBack이란 Log4j를 만든 개발자가 Log4j를 기반으로 속도와 메모리 점유율을 개선하여 만든 로깅 프레임워크이다.

 - org.slf4j.Logger 인터페이스의 구현체이다.

   >> 코드 작성 시 이 인터페이스를 임포트해주면 된다.

 

3. LogBack 특징

 - Level : 로그 레벨을 설정할 수 있다.

 - Appender : 출력 방법을 선택할 수 있다. ex) Console, RollingFile 등

 - Logger : 로그마다 다른 설정을 적용시킬 수 있다.

 - Authmatic Reloading Configuration File : 특정 시간마다 별도의 스레드를 통해 설정 파일 변경 여부 파악 및 적용이 가능하다. > logback 설정 변경 시 서버 재시작이 필요없다.

 

4. 프로젝트 세팅

 1) Controller - 로그를 찍기 위한 클래스

 2) application.properties - spring.profiles.active 설정 추가

 3) logback-spring.xml - logback 설정

 4) logback-{운용환경}.properties - logback 설정파일에서 로드할 상수 (로그파일, 레벨 등)

 

 4.1. Controller

  /api/log 요청 시 trace부터 error 까지의 로그를 쌓는 메서드를 작성한다.

@Controller
@RequestMapping("/api")
public class FileController {

	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
	@GetMapping("/log")
	@ResponseBody
	public ResponseDTO main() {
		logger.trace("trace Log");
		logger.debug("debug Log");
		logger.info("info Log");
		logger.warn("warn Log");
		logger.error("error Log");
		
		return null;
	}
}

 

 4.2. application.properties

  spring.profiles.active=dev 를 추가하여 개발 및 운영 환경을 정의해준다.

#서버포트
server.port=9090

#운용환경 : 개발
spring.profiles.active=dev

 

 4.3. logback-spring.xml

resource 경로에 logback-spring.xml 을 생성한 후 logback 설정을 정의한다.

* resource 경로에 logback-spring.xml 파일이 있으면 서버 기동 시 자동으로 로드한다.

<?xml version="1.0" encoding="UTF-8"?>

<!-- 10초마다 파일 변화를 체크하여 갱신시킨다. -->
<configuration scan="true" scanPeriod="10 seconds">

	<!-- spring.profile에 따른 설정파일 분기 -->
	<springProfile name = "prod">
		<property resource = "logback-prod.properties"/>
	</springProfile>
	
	<springProfile name = "dev">
		<property resource = "logback-dev.properties"/>
	</springProfile>
	
	
	<!-- 루트 로그 레벨 -->
	<property name ="LOG_LEVEL" value = "${log.config.level}"/>
	
	<!-- 로그 파일 경로 -->
	<property name ="LOG_PATH" value = "${log.config.path}"/>
	
	<!-- 로그 파일 명 -->
	<property name ="LOG_FILE_NAME" value = "${log.config.filename}"/>
	<property name ="ERR_LOG_FILE_NAME" value = "${log.config.filename}_error"/>
	
	<!-- 로그 파일 패턴 -->
	<property name ="LOG_PATTERN" value = "%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] [%logger{0}:%line] - %msg%n"/>
	
	
	
	<!-- 콘솔 Appender 설정 -->
	<appender name ="CONSOLE" class ="ch.qos.logback.core.ConsoleAppender">
		<encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
	</appender>
	
	<!-- 파일 Appender 설정 -->
	<appender name="FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender">
		<!-- 파일 경로 설정 -->
		<file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
		
		<!-- 로그 패턴 설정 -->
		<encoder class = "ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
		
		<!-- 롤링 정책 -->
		<rollingPolicy class = "ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- gz, zip 등을 넣을 경우 자동 로그파일 압축 -->
			<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${LOG_FILE_NAME}_%i.log</fileNamePattern>
			
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<!-- 파일당 최고 용량 -->
				<maxFileSize>10MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
			
			<!-- 로그파일 최대 보관주기 -->
			<maxHistory>30</maxHistory>
		</rollingPolicy>
	</appender>
	
	
	<appender name = "ERROR" class ="ch.qos.logback.core.rolling.RollingFileAppender">
		<filter class ="ch.qos.logback.classic.filter.LevelFilter">
			<level>error</level>
			<onMatch>ACCEPT</onMatch>
			<onMismatch>DENY</onMismatch>
		</filter>
		<file>${LOG_PATH}/${ERR_LOG_FILE_NAME}.log</file>
		
		<!-- 로그 패턴 설정 -->
		<encoder class = "ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>${LOG_PATTERN}</pattern>
		</encoder>
		
		<!-- 롤링 정책 -->
		<rollingPolicy class = "ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- gz, zip 등을 넣을 경우 자동 로그파일 압축 -->
			<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${ERR_LOG_FILE_NAME}_%i.log</fileNamePattern>
			
			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<!-- 파일당 최고 용량 -->
				<maxFileSize>10MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
			
			<!-- 로그파일 최대 보관주기 -->
			<maxHistory>30</maxHistory>
		</rollingPolicy>
	</appender>
	
	<root level = "${LOG_LEVEL}">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</root>
	
	<logger name="org.apache.ibatis" level = "DEBUG" additivity = "false">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</logger>
</configuration>

 - springProfile 태그를 사용하여 시스템 변수의 spring.profile.active 값을 조회할 수 있다. 이 값에 따라 logback-prod, logback-dev 파일 중 하나를 로드하여 설정파일의 프로퍼티로 사용한다.

 

 4.4. logback-dev.properties

 - 개발환경에서는 로그레벨을 debug로 설정한다.

#로그 레벨
log.config.level=debug

#로그파일 경로
log.config.path=/logs

#로그파일 명
log.config.filename=reckey

 

 4.5. logback-prod.properties

 - 운영 환경에서는 로그레벨을 info로 설정한다.

#로그 레벨
log.config.level=info

#로그파일 경로
log.config.path=/logs

#로그파일 명
log.config.filename=reckey

 

5. 테스트

 spring.profile.active를 prod 설정하면 로그레벨이 INFO로 잡혀있어 불필요한 로그는 조회되지 않지만 dev로 설정할 경우 서버 기동 시 발생되는 로그, DB 로그 등이 중구난방으로 나오게 된다.

 원인은 dev로 설정할 경우 logback 설정의 루트 로그 레벨이(root level) 값이 debug로 설정되어 어플리케이션 내 모든 클래스의 debug 로그가 찍히기 때문이다.

 이런 로그들은 특수한 목적이 없는 한 debug보단 info로 올리는 것이 좋다. 이를 위해서는 logger 태그를 사용하여 클래스별 로그 레벨 분기처리를 해야한다.

DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [AnonymousAuthenticationFilter:96] - Set SecurityContextHolder to anonymous SecurityContext
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [FilterSecurityInterceptor:210] - Authorized filter invocation [GET /api/log] with attributes [permitAll]
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [FilterChainProxy:323] - Secured GET /api/log
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [DispatcherServlet:91] - GET "/api/log", parameters={}
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [PropertySourcedRequestMappingHandlerMapping:108] - looking up handler for path: /api/log
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [RequestMappingHandlerMapping:522] - Mapped to com.reckey.controller.FileController#main()
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:24] - debug Log
INFO  2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:25] - info Log
WARN  2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:26] - warn Log
ERROR 2021-10-07 02:11:23[http-nio-9090-exec-1] [FileController:27] - error Log
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [RequestResponseBodyMethodProcessor:268] - Using 'application/json;q=0.8', given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [application/json, application/*+json, application/json, application/*+json]
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [RequestResponseBodyMethodProcessor:298] - Nothing to write: null body
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [DispatcherServlet:1131] - Completed 200 OK
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [HttpSessionSecurityContextRepository:346] - Did not store anonymous SecurityContext
DEBUG 2021-10-07 02:11:23[http-nio-9090-exec-1] [SecurityContextPersistenceFilter:118] - Cleared SecurityContextHolder to complete request
DEBUG 2021-10-07 02:11:25[reckeyPool housekeeper] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:11:25[reckeyPool housekeeper] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.

 

 5.1. 특정 클래스 분기처리 (로그레벨 INFO로 격상)

  일단 분기처리할 클래스의 경로를 확인해보자. 현재 로그 설정으로는 클래스 명만 나오고 있기 때문에 클래스 경로를 파악하기 힘들다. 로그 패턴에 클래스 경로 패턴인 %C를 다음과 같이 추가해보자.  자동 리로드 설정이 있으므로 서버 재시작은 하지 않아도 된다.

<property name ="LOG_PATTERN" value = "%-5level %d{yyyy-MM-dd HH:mm:ss}[%thread] [%C] [%logger{0}:%line] - %msg%n"/>

 그리고 다시 요청을 보내면 다음과 같이 클래스 경로가 함께 조회된다.

INFO  2021-10-07 02:14:37[restartedMain] [org.springframework.boot.StartupInfoLogger] [ReckeyApplication:61] - Started ReckeyApplication in 0.703 seconds (JVM running for 3806.024)
DEBUG 2021-10-07 02:14:37[restartedMain] [org.springframework.boot.availability.ApplicationAvailabilityBean] [ApplicationAvailabilityBean:77] - Application availability state LivenessState changed to CORRECT
INFO  2021-10-07 02:14:37[restartedMain] [org.springframework.boot.devtools.autoconfigure.ConditionEvaluationDeltaLoggingListener] [ConditionEvaluationDeltaLoggingListener:63] - Condition evaluation unchanged
DEBUG 2021-10-07 02:14:37[restartedMain] [org.springframework.boot.availability.ApplicationAvailabilityBean] [ApplicationAvailabilityBean:77] - Application availability state ReadinessState changed to ACCEPTING_TRAFFIC
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@36e9f17b
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@73eb69c2
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@439a7476
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@42521e57
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@3bc95d4f
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@13c4968d
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator] [HikariPool:728] - reckeyPool - Added connection org.postgresql.jdbc.PgConnection@36d6947
DEBUG 2021-10-07 02:14:37[reckeyPool connection adder] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - After adding stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:15:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:15:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:15:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:15:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:16:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:16:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:16:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:16:37[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.
DEBUG 2021-10-07 02:17:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:421] - reckeyPool - Pool stats (total=10, active=0, idle=10, waiting=0)
DEBUG 2021-10-07 02:17:07[reckeyPool housekeeper] [com.zaxxer.hikari.pool.HikariPool] [HikariPool:517] - reckeyPool - Fill pool skipped, pool is at sufficient level.

 org.springframework경로의 여러 클래스와 com.zaxxer.hikari로 시작하는 여러 클래스에서 DEBUG레벨로 로그들이 찍히고 있다. (히카리 로그는 DB연결로 인함입니다. 신경쓰지않으셔도됩니다.) 이 클래스들만 DEBUG레벨에서 INFO 레벨로 올린다면 이 문제가 해결될 것이다.

logback-spring.xml 파일의 logger 태그에 분기처리할 클래스를 다음과 같이 추가한다.

name에 들어가는 경로의 하위 모든 클래스에서 발생하는 로그를 INFO 레벨로 올리게 된다.

	<logger name="org.springframework" level = "INFO" additivity = "false">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</logger>
	
	<logger name="com.zaxxer.hikari" level = "INFO" additivity = "false">
		<appender-ref ref="CONSOLE"/>
		<appender-ref ref="FILE"/>
		<appender-ref ref="ERROR"/>
	</logger>

 

이제 실제 요청을 보내면 다음과 같이 불필요한 debug 레벨의 로그들을 쌓지 않게 된다.

DEBUG 2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:24] - debug Log
INFO  2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:25] - info Log
WARN  2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:26] - warn Log
ERROR 2021-10-07 02:26:21[http-nio-9090-exec-1] [com.reckey.controller.FileController] [FileController:27] - error Log

 

6. 마치며

 스프링 부트에서 로그백을 사용하여 로그를 남겨본 것은 처음이다. 생각보다 되게 간단하고, log4j 같은 경우 로그파일 리로드 설정을 외부 설정파일에서 해줘야하는데 logback은 리로드되는 기능이 설정 파일 안에 있어서 좋은것 같다.

반응형
반응형

1. 개요

 이전 포스팅에서 테스트 코드를 통해 두 클래스를 비교한 결과 다음과 같은 정보를 얻을 수 있었다.

  DefaultHttpClient CloseableHttpClient
생성 new DefaultHttpClient() HttpClients.createDefault()
close 메서드 존재 여부 X O
HTTP 통신 횟수 1 ConnectionPool 설정에 따라 다름
ConnectionPool X O

오늘 알아볼 것은

첫째, 생성한 DefaultHttpClient에서 execute를 2번 이상 실행했을 때 즉, HTTP 통신을 2번 이상 요청했을 때 예외가 발생한 원인과 생명주기.

둘째, CloseableHttpClient의 ConnectionPool 설정 정보를 jar 파일을 통해 알아보겠다.


2. DefaultHttpClient

jar파일과 예외 로그를 기반으로 코드를 찾아가보니 다음과 같은 부분에서 예외가 발생함을 확인하였다.

execute 메서드를 따라가다.

conn이라는 값이 null이 아닐 때 위 에러가 발생한다. conn은 ManagedClientConnectionImpl 형의 멤버필드였으며, 어디선가 주입이 된것같은데... 결론은 찾을 수가 없었다. 멍청한자식.

 

서치를 통해 얻은 정보를 정리한 결과 예외 발생 원인은 다음과 같았다.

DefaultHttpClient 객체를 생성하면 내부적으로 basicClientConnectionManager 인스턴스가 주입된다. 이 인스턴스는  HTTP 통신에 대한 커넥션 정보를 저장하고 있다. 단, 하나의 최초 연결한 하나의 커넥션 정보만 저장한다.

두 개의 커넥션을 연결하려 했기때문에 예외가 발생했으며, 실제 에러 로그를 확인해보니 basicClientConnectionManager 클래스의 메서드 안에서 발생함을 확인할 수 있었다.

아래 로그의 Asserts.java:34가 위 코드 사진의 첫번째 빨간 블럭부분이었다. (기존 커넥션 정보가 있기때문에 발생)

 

정리하면 DefaultHttpClient 클래스는 하나의 HTTP 통신만을 처리할 수 있도록 내부적으로 구현되어져 있다.

만약 이 인스턴스를 사용해 두번의 통신을 처리하고싶다면 두개의 인스턴스를 생성해야한다.


3. CloseableHttpClient

생성자를 찾아가보니 다음과 같은 코드가 있었다. httpClientBuilder.create().build(). 요녀석을 파헤쳐보자

createDefault()

create().build() 메서드 확인 결과, PoolingHttpClientConnectionManager를 생성한 후 connectionManager로 사용하는 것이 보인다.

create().build()

 

생성된 ConnectionPool의 Default maxTotal, maxConPerRoute는 다음과 같이 2, 20임을 확인할 수 있다.

CPool

변수
maxTotal 최대 커넥션 개수
maxConPerRoute 라우트당 최대 커넥션 개수(ip:port 별 최대 커넥션 개수)

 

커스텀을 하지 않고 사용할 경우 최대 커넥션 개수가 2개이기때문에 실제 서비스를 운영하기엔 문제가 있다.

PollingHttpClientConnectionManager 은 커스텀이 가능하다. 때문에 상황에 맞게 커스텀하여 CloseableHttpClient를 구현한다면 많은 HTTP 통신 요청을 필요로 하는 서비스에 적절하게 사용될 수 있다.


4. 마치며

DefaultHttpClient와 CloseableHttpClient에 대한 아~주 미세한 차이에 대해서도 몰랐었지만, 이번 스터디를 통해 차이점은 물론이며, 실제 서비스에 왜 저 클래스를 사용했는지도 이해하게 되었다.

DefaultHttpClient의 생명주기, 언제 connection이 끊어지는지에 대해서는 확인하지 못해 뭔가 깨림직한 기분이지만, 오늘하루도 잘 보냈음에 위안을 삼는다!

반응형

+ Recent posts