spring boot logging 설정

spring boot logging 설정

저번에는 spring logging에 대해서 알아봤다.
이번에는 설정에 대해 알아볼려고 한다.
아래는 boot의 기본 설정으로 되어있는 로그 포맷이다.

2016-03-15 12:31:52.479  INFO 602 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-03-15 12:31:52.480  INFO 602 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-03-15 12:31:52.573  INFO 602 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-03-15 12:31:52.767  INFO 602 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2016-03-15 12:31:52.891  INFO 602 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-03-15 12:31:52.898  INFO 602 --- [           main] ication$$EnhancerBySpringCGLIB$$3dbc3010 : test 
2016-03-15 12:31:52.901  INFO 602 --- [           main] com.example.DemoApplication              : Started DemoApplication in 7.351 seconds (JVM running for 8.53)

첫 번째는 날짜와 시간
두 번째는 로그레벨
세 번째는 프로세스 ID
네 번째는 구분자
다섯 번째는 스레드 명
여섯 번째는 로그네임(클래스 네임) 잘 릴수도 있다 org.springframework.xxxxxx.xxxxxxxxxx.xxxx = o.s.x.x.x
마지막으로 로그메시지다.

스프링 부트의 기본적인 로그 레벨은 info이다.
만약 어플리케이션을 실행 할때 바꾸고 싶다면

java -jar app.jar --debug

이렇게 디버그 레벨로 가능하다.
혹은 설정 자체를 하고 싶다면 application.properties이나 application.yml 파일에 다음과 설정해도 된다.

debug: true

전부다 디버그가 아니라 특정 패키지에만 지정 하고 싶다면 이렇게 하면 된다.
logging.level.* (패키지명) : debug

logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate= ERROR
com.example=ERROR

다음은 로그 파일에 대해 알아보자!
손쉽게 로그파일로 만들수 있다
logging.pathlogging.file 프로파티로 설정 가능하다.
logging.path는 경로를 지정하면 된다. 파일명은 spring.log로 생성된다.
logging.file은 파일 명을 설정해주면 된다. 디폴트는 루트에 저장된다.
두개 같이 쓰면 파일을 먼저 선택하는 듯 하다.
logging.pattern.console 과 logging.pattern.file 로 패턴을 지정 할 수 있다.
DefaultLogbackConfiguration 클래스를 보면 다음과 같이 설정 되어 있다.

private static final String CONSOLE_LOG_PATTERN = "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} "
        + "%clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} "
        + "%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "
        + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";

private static final String FILE_LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss.SSS} "
        + "${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";

만약에 ansi 터미널을 지원한다면 콘솔에 색상을 추가 할 수 있다.
인텔리j는 기본으로 되는듯 하다. 이클립스는 플러그인을 깔면 되는거 같은데 테스트를 해보지 않았다.

spring.output.ansi.enabled=always

spring-boot-ansi

이번에는 커스텀한 로그를 찍 을 수 있다.
classpath에 logback.xml 혹은 logback-spring.xml에 설정을 해두면 boot가 classpath에 있는 파일을 끌고 올라간다.
문서에는 .groovy도 된다고 한다. 하지만 잘모르기때문에 넘어간다.ㅎㅎㅎ
문서에보면 다음과 같이 있다.

로그 시스템 사용자 정의 파일
Logback logback-spring.xml, logback-spring.groovy, logback.xml or logback.groovy
Log4j log4j-spring.properties, log4j-spring.xml, log4j.properties or log4j.xml
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties

만약 커스텀한 로그파일의 위치를 변경하고 싶다면 logging.config 프로퍼티를 사용하면 된다.
예를 들어 /resources/logback/logback.xml 에 위치 하고 싶다면

logging.config=classpath:logback/logback.xml

다음과 같이 설정 할 수 있다.

spring boot logging 을 참고하여 이글을 작성하였다.

spring boot logging

spring boot logging

spring boot의 로깅을 알아볼려한다. 예전에 백기선님이 스프링 캠프에서 발표한 내용을 참고하여 정리했다.
예전에 한번 봤었는데 기억이 가물가물에서 아예 정리를 해야겠다.

spring은 기본적으로 JCL을 사용한다.
spring bean 라이브러리에 DefaultSingletonBeanRegistry 클래스의 일부분

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

...

protected final Log logger = LogFactory.getLog(getClass());

...

if (logger.isDebugEnabled()) {
    logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
}

하지만 근래에는 JCL보다 slf4j를 더 많이 쓴다고 관련해서는 백기선님 발표자료를 참고.
JCL과 slf4j는 같은 추상 로깅 라이브러리다.
동작방식이 약간만 다를뿐…

그럼 어떻게 로깅을 통합하고 관리하는지 알아보자.
일단 slf4j에는 기본적으로 3가지 라이브러리가 있다.
첫 번째론 slf4j api 라이브러리가 있다.
이 라이브러리는 그냥 기본 인터페이스 껍대기라 한다.
거기서보면 실제 클래스패스에서 log 라이브러리를 가져오는 곳으로 보이는 함수가 있다.

private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
    // use Set instead of list in order to deal with bug #138
    // LinkedHashSet appropriate here because it preserves insertion order during iteration
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration<URL> paths;
        if (loggerFactoryClassLoader == null) {
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else {
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }
        while (paths.hasMoreElements()) {
            URL path = paths.nextElement();
            staticLoggerBinderPathSet.add(path);
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
}

logback classic 에 보면 org/slf4j/impl/StaticLoggerBinder 클래스가 있다.
아무튼 일단 slf4j api는 껍대기 인터페이스라 생각하면 되겠다.

두번째론 slf4j binding 이라는 라이브러리다.
실직적으로 slf4j api 의 구현체들이다.
위에서 언급한 logback classic 도 binding 이다.
logback classic 의 Logger 클래스의 일부분이다.

public final class Logger implements org.slf4j.Logger, LocationAwareLogger,
    AppenderAttachable<ILoggingEvent>, Serializable {
...

  public void debug(String format, Object arg) {
    filterAndLog_1(FQCN, null, Level.DEBUG, format, arg, null);
  }
}

이외에도
slf4j-log4j12 : log4j binding
slf4j-jdk14 : java.util.logging binding 기본 자바 패키징
slf4j-jcl : 아파치 common logging binding

등이 있다.
logback만 logback-classic이다. 흠

세번째는 slf4j bridge 라이브러리다.
레거시들을 위한 라이브러리다.
예전에 개발할때는 직접적으로 로그들을 선택해서 쓰거나 혹은 JCL을 썼다.
그래서 그 라이브러리들을 slf4j로 호출하도록 해주는 라이브러리이다.
그래야 내가 원하는 로그들로 갈테니…
아주 좋은 라이브러리다.

만약에 어떤 라이브러리에서 log4j를 직접적으로 쓴다고 가정하자.
그럼 log4j -> slf4j-log4j-bridge -> slf4j-api -> slf4j-binding(내가 원하는 로그 binding) -> 내가 원하는 로그 라이브러리(logback, jul, jcl)
이런 방식이다.
만약 같은 종류의 브릿지와 바인딩을 쓰면 어떻게 되나

만약 log4j를 쓴다고 가정하면 log4j -> slf4j-log4j(bridge) -> slf4j-api -> slf4j-log4j(binding) -> log4j -> sl4j->log4j(bridge) -> slf4j-api -> slf4j-log4j(binding)….. ????

무한로프에 빠지게 된다. 같은 종류의 브릿지와 바인딩을 쓰면 안된다.
그것만 주의하자!!

아래는 스프링 boot의 기본 로깅 이다.
spring-boot-logger-maven

한개의 binding 라이브러리와 세개의 bridge 라이브러리가 존재한다.
동일한 브릿지와 바인더는 보이지 않는다.

만약 boot의 기본 라이브러리인 logback 말고 log4j를 쓰고 싶다면 어떻게 할까
저기 발표내용에도 나와있지만
spring boot logging
여기에 자세히 나와있다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j</artifactId>
</dependency>

을 추가 해주자
spring-boot-log4j

그럼 위와 같이 변경이 되어있을 것이다.

slf4j-log4j 에도 org/slf4j/impl/StaticLoggerBinder 클래스가 존재한다.

log4j를 쓰면서 JCL 코드를 호출 해보았다.
SLF4JLocationAwareLog 클래스를 호출하였다.
slf4-bridge다. 정확한 명칭은 jcl-over-slf4j 이다. 그리고 slf4j-api의 구현체인 Log4jLoggerAdapter 클래스를 호출하였다.
그런다음에 실질적인 logj4를 호출 하였다.

순서를 대략 이렇다.
info 기존이다.

jcl-over-slf4j(bridge) 라이브러리의 SLF4JLocationAwareLog 클래스

public void info(Object message) {
    logger.log(null, FQCN, LocationAwareLogger.INFO_INT, String.valueOf(message), null, null);
}

slf4j-log412(binding) 의 Log4jLoggerAdapter 클래스(slf4j-api 구현체)

public void log(Marker marker, String callerFQCN, int level, String msg, Object[] argArray, Throwable t) {
    Level log4jLevel = toLog4jLevel(level);
    logger.log(callerFQCN, log4jLevel, msg, t);
}

log4j 의 Category 클래스

public void log(String callerFQCN, Priority level, Object message, Throwable t) {
    if(repository.isDisabled(level.level)) {
      return;
    }
    if(level.isGreaterOrEqual(this.getEffectiveLevel())) {
      forcedLog(callerFQCN, level, message, t);
    }
}

이런 방식으로 로그를 찍는다. 저기 백기선님이 설명을 잘해주셔서 도움이 많이 되었다.
만약 우리가 라이브러리를 만든다면 실직적으로 slf4j-api만 사용하면 된다.
나머지만 해당 어플리케이션 개발자가 무엇을 쓸지 선택하기만 하면 된다.

이렇게 로그에 대해서 알아봤다.

추가 : 만약 동일한 브릿지 바인딩이 클래스 패스에 있을경우 서버 시작시 에러를 낸다. jcl, log4j 테스트했음!

spring mock test 에 대해 알아보자

spring mock test

이번엔 spring mock test에 대해서 알아 볼 것이다.
mock 으로 테스트를 잘 하지 않아서 익숙하지 않다.
그래서 이제부터는 mock test를 사용 하도록 노력 할라고 하는 중이다.
일단 spring boot로 할 것이다.
그래서 아래와 같이 메이븐을 추가하자.

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

일단 기본적으로 테스트에 필요한 라이브러리다.
첫번째는 json-path 여기에 자세히 나와있다.
두번째는 spring test를 위한 mock 라이브러리다.
이번에도 스칼라도 했다. 흠하

일단 테스트 클래스에 아래와 같이 어노테이션을 추가 한다.

@RunWith(classOf[SpringJUnit4ClassRunner])
@SpringApplicationConfiguration(Array(classOf[SpringBootConfig]))
@WebAppConfiguration
@FixMethodOrder(MethodSorters.JVM)

첫번째는 Spring에서 제공하는 Runner다
두번째는 Spring Boot를 사용해서 저 어노테이션을 쓴거다. 빈 들을 관리해준다.
Boot를 쓰지 않았을때는 @ContextConfiguration 어노테이션을 썼다. 물론 SpringApplicationConfiguration 안에 ContextConfiguration 포함되어 있다.
세번째는 WebApplicationContext를 생성해주는 아이다.
네번째는 필수 사항이 아니다. 테스트 순서를 정하는 거다.
테스트를 위해 아래와 같이 셋팅 해주자.

var objectMapper: ObjectMapper = _

var mockMvc: MockMvc = _

@Autowired
var wac: WebApplicationContext = _

@Before
def before: Unit = {
  objectMapper = new ObjectMapper
  mockMvc = MockMvcBuilders.webAppContextSetup(wac).build
}

objectMapper는 다들 아시다 시피 json으로 값을 넘길때 사용 할거다.
mockMvc를 선언한다.
그리고 WebApplicationContext 는 mockMvc를 생성하기 위해서 필요하다.
before 메소드에 objectMapper와 mockMvc 생성해준다.

일단 코드 부터 보자

@Test
@Test
def mockTest: Unit = {
  mockMvc.perform(get("/accounts") header ("Accept","application/json")  contentType(MediaType.APPLICATION_JSON))
    .andDo(print())
    .andExpect(status isOk)
    .andExpect(handler handlerType (classOf[AccountController]))
    .andExpect(handler methodName ("accounts"))
    .andExpect(content contentType(MediaType.APPLICATION_JSON_UTF8))
    .andExpect(jsonPath("$.content[0].name", is("wonwoo")))
    .andExpect(jsonPath("$.content[1].name", is("kevin")))
}

첫번째줄은 Http method와 url, 헤더 정보를 셋팅 할 수 있다. contentType와 accpet를 셋팅 했다. contentType처럼 메소드를 사용 할 수 있고
커스텀 header 도 설정 할 수 있다.
이 외 에도 accept, cookie, locale, sessionAttr, session, principal 등 여러가지 메소드가 존재 한다.
print() 는 request response 정보를 콘솔창에 출력해준다.
status.isOk 는 http code가 200일 경우를 체크 하는거다.
이 외 에도 isCreated, isNoContent, isBadRequest, isUnauthorized, isNotFound 등 이 있다. 자주 쓰는 것만 넣어뒀다.
웬만한 http code가 다 있는 듯하다.
handler.handlerType은 요청 컨트롤러이다.
handler methodName은 요청 메소드이다.
content.contentType은 response의 미디어 타입이다.
일단 여기 까지 성공 되었다면 다음은 데이터를 확인할 차례이다.
json-path 라이브러리를 추가한 이유이다.
문법은 저기 링크에 자세히 나와있다.
content 키를 갖고 있는 배열의 첫번째 name이 wonwoo와 같은지 비교하는거다.
만약 틀린다면 에러를 내뱉는다.

코드를 좀더 보자.

@Test
def mockTest1: Unit = {
  mockMvc.perform(get("/account/{id}", 2.asInstanceOf[Object]) contentType (MediaType.APPLICATION_JSON))
    .andDo(print())
    .andExpect(status isOk)
    .andExpect(handler handlerType (classOf[AccountController]))
    .andExpect(handler methodName ("account"))
    .andExpect(jsonPath("$.name", is("kevin")))
}

urlTemplate 처럼 만들 수도 있다.
나머지는 같지만 단일 데이터이기 때문에 $.name 이렇게 했다.

mockMvc.perform(get("/account/search") param("name", "wonwoo")

이렇게 파라미터로 보낼 수도 있다.

@Test
def mockTest4: Unit = {

  val account = new Account()
  account.setId(3L);
  account.setName("mockTest")
  account.setPassword("pwMockTest")

  mockMvc.perform(post("/account")
    contentType (MediaType.APPLICATION_JSON)
    content (objectMapper.writeValueAsString(account)))
    .andDo(print())
    .andExpect(status isCreated)
    .andExpect(jsonPath("$.name", is("mockTest")))
    .andExpect(jsonPath("$.password", is("pwMockTest")))
}

이번엔 requestbody로 보내는 데이터를 만들었다.
objectMapper 를 이용해서 엔티티빈을 json String으로 만들었다.
http code는 201로 생성 했다고 코드를 받았다.

이렇게 테스트 케이스를 만들어서 사용할 예정이다.
예전엔 그냥 크롬 확장프로그램에서 Advanced REST client를 썼는데 이젠 테스트 케이스를 만들어서 사용 해야 겠다.