이번에는 맨날 단순히 사용하기만 한 SLF4J와 Logback 개념을 정리해보려고한다.
SLF4J
SLF4J란 The Simple Logging Facade for Java의 약어다.
쉽게 말하면 SLF4J 자바 진영의 로깅 프레임워크 인터페이스이며 개발자가 배포 시 원하는 로깅 프레임워크를 연결할 수 있도록 도와준다.
즉 다양한 로깅 프레임워크 구현체들 (java.util.logging, logback 및 reload4j)을 쉽게 갈아끼워서 사용할 수 있게 도와준다.
그럼 이제 공식 문서에서 소개하는 SLF4J 내용에 대해 정리해보겠다.
레거시 로깅 API 사용 시 SLF4J 사용
만약 프로젝트에서 사용하는 로깅 프레임워크가 SLF4J 이외의 로깅 API에 의존한다고 생각해보자.
기존의 로깅 프레임워크를 사용하고 SLF4J로 전환을 시도하지 않을 수 있는데 이런 상황에서도 SLF4J를 사용할 수 있다고 한다.
SLF4J는 log4j, JCL 및 java.util.logging API에 대한 호출을 리디렉션하여 SLF4J API에 대해 만들어진 것처럼 동작하도록 하는 여러 브리징 모듈을 제공한다. 브리징 모듈이라는 개념이 아주 중요하다.
공식 문서에서 제공하는 몇 가지 예시를 살펴보겠다.
먼저 첫 번째 예시는 다른 로깅 프레임워크를 사용하는데 SLF4J로 리다이렉트를 해 SLF4J인 구현체를 사용하는 경우다.
중요한 점이 있다.
- jcl-over-slf4j.jar가 commons-slf.jar를 대체한다. jcl-over-slf4j가 바로 브리징 모듈이다.
- log4j-over-slf4j.jar는 마찬가지로 log4j.jar를 대체하고 reload4j.jar도 대체한다. 이것도 브리징 모듈이다.
- 이렇게 해서 브리징 모듈을 통해 SLF4J API를 통해 만들어진 것처럼 동작한다.
두 번째 예시는 Commons logging API 및 java.util.loggin API를 SlF4J로 리다이렉션해 reload.js를 다시 사용하는 것이다.
마찬가지로 jcl-over-slf4j.jar가 Commons logging API를 대체하며 java.util.logging API도 동일하다.
이 밖에도 공식문서에는 더 많은 예시가 나와있다.
또한 주의할점도 몇 가지 있다.
바로 SLF4J에 바인딩되는 로깅 프레임워크와 브릿지를 통해 SLF4J API로 연결되는 프레임워크는 동일하면 안된다고 한다.
예를 들면, log4j-over-slf4j.jar 및 slf4j-reload4j.jar는 동시에 사용할 수 없고,
jcl-over-slf4j.jar 를 slf4j-jcl.jar 동시에 사용할 수 없다는 것이다.
공식 문서를 통해 살펴보면 무한 루프가 발생한다고 한다.
그러나 slf4j-jcl.jar와 jcl-over-slf4j.jar는 같이 사용할 수 있다는 것 같다.
브릿지 - > API -> 바인딩 구조를 확실하게 기억해야겠다.
다음은 레거시 로깅 프레임워크와 SLF4J를 브릿징할 뿐만 아니라 일반적인 SLF4J 사용법도 보여주는 이미지다.
그리고 이제 한 가지 중요한 개념에 대해 정리해보자.
바로 MDC이다.
Mapped Diagnostic Context (MDC)
MDC는 로깅하는 로그 메시지가 키-값 쌍으로 만들어지는 로깅 프레임워크에서 유지되는 맵이다.
MDC 데이터는 메시지를 필터링하거나 특정 작업을 트리거하는데 매우 유용하다고 한다.
SLF4J는 MDC 기능을 지원하는데, 현재는 log4j 및 logback만 MDC 기능을 제공한다.
기본 프레임워크가 MDC를 제공하지 않는 경우(예: java.util.logging) SLF4J는 여전히 MDC 데이터를 저장하지만
그 안의 정보는 사용자 정의 사용자 코드로 검색해야 한다.
모든 프레임워크가 MDC를 사용하지는 않으므로 이에 대한 강요적인 내용은 없는 것 같다.
우리가 로깅을 보면 요청마다 예쁘게 깔끔하게 잘 나오는데 바로 MDC 때문이다.
더 자세한 내용은 Logback에서 더 살펴보겠다.
SLF4J에서는 로깅 프레임워크를 설정할 때 발생하는 예외, 자주하는 질문, 국제화와 현지화 등
많은 정보에 대해 정리해놨으므로 궁금한게 있을 때 참고하면 좋을 것 같다.
그럼 이제 마지막으로 SLF4J 사용 이유를 정리해보겠다.
- SLF4J는 인터페이스므로 원하는 로깅 프레임워크를 클래스 경로에 적절한 jar 파일(바인딩)을 넣어 배포 시점에 연결할 수 있다.
- JVM 클래스 로드 방식을 통해 매우 이른 시기에 프레임워크의 바인딩의 성공 여부를 자동으로 확인할 수 있다. 만약 SLF4J가 클래스 경로에서 바인딩을 찾을 수 없는 경우 단일 경고 메시지를 내보내고 기본적으로 작업 없음 구현으로 설정된다.
- SLF4J는 log4j, java.util.logging, 단순 로깅 및 NOP와 같은 널리 사용되는 로깅 프레임워크를 지원한다. logback은 기본적으로 SLF4J를 지원합니다.
- 이전에 소개한대로 레거시 로깅 API를 브릿징해 SLF4로 만들어진 것처럼 사용할 수 있다.
- 매개변수화된 로그 메시지를 지원한다. 예를 들어 log.info("log = {}", log) 이런 경우다.
중요한 점은 역시 추상화에 의존해야 한다.
즉 SLF4J 인터페이스를 사용하고 구현체에 대해 직접적으로 사용하는건 매우 좋지 않다는 것이다.
Logback
Logback은 log4j 프로젝트의 후속 프로젝트라고 한다. log4j보다 성능도 좋고 장점도 많다.
현재 logback은 logback-core, logback-classic 및 logback-access의 세 가지 모듈로 나뉜다.
- logback-core 모듈은 다른 두 모듈의 토대를 마련한다.
- logback-classic은 기본적으로 SLF4J API 를 구현하므로 log4j 1.x 또는 java.util.logging(JUL)과 같은 기타 로깅 프레임워크와 logback 간에 쉽게 전환할 수 있다.
- logback-access 모듈은 HTTP 액세스 로그 기능을 제공하기 위해 Tomcat 및 Jetty와 같은 Servlet 컨테이너와 통합 가능하다.
결론적으로 개발자들은 logback-core 위에 모듈을 쉽게 구축할 수 있다.
logback의 기본 설정은 기초 configuration file이 발견되지 않으면, logback은 ConsoleAppender를 root logger에 추가한다.
그래서 우리가 처음 스프링 프로젝트를 작동시키면 로거가 콘솔에 촤라락 예쁘게 나온 것 같다.
Appender은 출력문이 나오는 객체(출력 대상) 클래스다.
Appender는 콘솔, 파일, Syslog, TCP 소켓, JMS 등을 포함한 다양한 대상이 있다.
개발자는 특정 상황에 맞게 자신만의 Appender를 꾸밀 수 있다고도 한다. 추후에 더 살펴보겠다.
사용법도 설명해주는데 로그백 개념이 아니라 사용법 게시글에서 좀 더 자세히 설명하겠다.
Logback 아키텍처
로그백은 Logger, Appender, Layout 이 세가지 유형의 구성요소가 함께 동작해야 개발자에게 메시지 유형 및 로그 레벨에 따라 메시지를 기록하고 런타임 시 로그 메시지의 형식과 로그 메시지의 기록 장소를 제어할 수 있다고 한다.
Logger 클래스는 logback-classic 모듈에 포함된다.
Appender 및 Layout 인터페이스는 logback-core 모듈의 일부다. 범용 모듈로서 logback-core에는 Loggger에 대한 개념이 없다.
Logger Context
- Logger Context는 애플리케이션에서 요청한 모든 Logger 목록과 구성에 대한 참조를 유지한다.
- configuration은 configured loggers, appenders, filters 등이 포함되며 reconfigure이 발생할 때마다 자동으로 업데이트된다.
- Logger 생성뿐만 아니라 트리 같은 계층에 로거를 배열하는 역할을 한다.
- 계층 구조는 이름을 기준으로 만들어진다. 예시는 아래와 같다.
- com.foo는 com.foo.Bar의 부모 로거다. java는 java.util.의 부모 로거다.
Effective Level
- Logger에는 로깅 레벨을 할당할 수 있는데 TRACE, DEBUG, INFO, WARN, ERROR로 이루어진다.
- 만약 주어진 로거에 수준이 할당되지 않는다면 가장 가까운 조상 하나를 상속한다. 예시는 다음과 같다.
기본적인 로깅 룰은 로깅 레벨이 로거의 유효 레벨보다 높거나 같으면 로깅 요쳥이 활성화 된다.
로깅 레벨은 다음과 같이 정렬된다. TRACE < DEBUG < INFO < WARN < ERROR.
Appenders and Layouts
Logger에는 하나 이상의 Appender를 연결할 수 있다.
어떤 로깅 요청이 들어와서 우리의 Logger가 활성화되면 각 로깅 요청은 해당 로거의 모든 어펜더와 계층 구조의 상위 어펜더로 전달된다.
즉 Appender는 Logger 계층 구조에서 추가로 상속된다.
예를 들어 Console Appender가 root Logger에 추가되면 활성화된 모든 로깅 요청이 콘솔에 출력되는 것이다.
자녀 Logger에 대한 요청이 결국 부모 Appender에 의해 출력이 되는데 이걸 막는 방법도 존재한다.
여기에서 중요한 개념인 Appender Additivity가 등장한다.
부록 추가성이라고 해석이 되는데 편하게 추가성이라고 부르겠다. 그럼 이제 이 추가성을 이해하기 위해 한 가지 예시를 들어보겠다.
L이라는 로거가 있다. 만약 L의 조상 P라는 로거가 false라는 추가성 플래그를 가지고 있다고 생각해보자.
만약 로깅 요청이 와서 L에서 로깅 메시지가 출력되면 이것은 L의 부모 로거의 Appender에게도 출력 요청이 갈 것이다.
당연히 여기에는 P도 포함되어 있다. 그러나 P의 추가성 플래그가 false이므로 P의 조상 Appender에게는 L의 출력 요청이 가지 않는다.
로거는 기본적으로 추가성 플래그를 true로 가진다.
Layout은 로깅 메세지의 출력 형식을 우리 개발자가 정의할 수 있게 도와주는 것이다.
Layout은 Appender와 연결하여 수행된다. Layout은 로깅 형식을 지정하고,
Appender은 형식이 지정된 메시지를 출력 대상으로 보내는 작업을 수행한다.
Logback 동작 분석
1. 만약 TurboFilter의 구현체가 존재한다면 이에 대한 필터 체인이 호출된다. Turbo Filter는 Logging Context에 연결된다.
Turbo Filter는 Marker, Level, Logger, Message, Throwable 등 로깅 요청과 관련된 정보를 기반으로 특정 이벤트를 필터링할 수 있다. 즉 지정된 Appender가 사용될 때뿐만 아니라 로깅 요청이 실행될 때마다 호출된다.
필터 체인의 응답이 FilterReply.DENY면 로깅 요청이 삭제된다. 만약 필터 체인의 응답이 FilterReply.NEUTRAL 2단계로 계속 진행한다. 응답이 FilterReply.ACCEPT이면 다음을 건너뛰고 3단계로 바로 이동한다.
2. 이 단계에서 로그백은 Logger의 레벨 수준을 로깅 요청 레벨과 비교한다.
이 심사에 따라 로깅 요청이 비활성화된 경우 로그백은 추가 처리 없이 요청을 삭제한다. 그렇지 않다면 다음 단계로 진행한다.
3. 로깅 요청이 2단계에서 살아 남았다면 ch.qos.logback.classic.LoggingEvent라는 객체를 생성한다.
요청의 Logger, 요청 레벨, 메시지 자체, 요청과 함께 전달되었을 수 있는 예외, 현재 시간, 현재 스레드, 로깅 요청을 실행한 클래스에 대한 다양한 데이터 및 MDC와 같은 요청의 모든 관련 매개 변수를 포함하는 객체다.
필드는 느리게 초기화(lazy init)되며, 이는 실제로 필요할 때만 초기화된다.
MDC는 추가적인 상황별 정보로 로깅 요청을 꾸미는 데 사용된다.
MDC는 다음 시간에 별도의 포스팅으로 정리하겠다.
4. LoggingEvent 생성 후, Logback는 요청에 응답할 수 있는 Appender의 doAppend()를 호출한다.
해당 메서드는 Logging Context에서 상속된 Appender의 메서드다.
로그백 배포와 함께 제공된 모든 Appender는 동기화된 블록에서 doAppend 메서드를 구현하는
AppenderBase 추상 클래스를 확장하여 스레드 안전을 보장한다. 해당 AppenderBase에 대한 자세한 내용도 추후에 살펴보겠다.
5. Logging 이벤트의 형식을 지정하는 것은 호출된 Appender의 책임이다.
Appender는 로깅 이벤트 형식 지정 작업을 layout에 위임한다.
layout은 LoggingEvent 인스턴스를 포맷하고 결과를 문자열로 반환한다.
6. 로깅 이벤트가 완전히 형식화 된 후 각 Appender에 의해 출력 대상으로 전송된다.
주의 사항.
만약 동일한 인자 값으로 LoggerFactory.getLogger 메서드를 호출하면 항상 동일한 로거 객체에 대한 참조 값을 리턴한다.
Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");
// x == y 임.
또한 문자열 연산자 + 보다는 매개변수 식으로 콤마(,)를 사용해야 한다는 부분은 알고 있어서 포스팅에서 넘겼다.
더 많고 자세한 내용은 공식문서를 들어가면 확인할 수 있다.
이상으로 SLF4J와 로그백에 대한 개념 포스팅을 마무리합니다. 감사합니다.
참고 자료
댓글