Slf4j logback 이란 무엇이며, 왜 로깅을 해야할까? 💁‍♂️

Posted by , May 01, 2024
스프링Slf4j

학습 동기

현재 미트윈 팀프로젝트를 진행하면서 로깅의 필요성을 느끼지 못하여 제대로 도입을 하지 않았다. 이전까지는 테스트 코드를 제대로 짜는것에도 급급해서 로깅에 대해 신경쓰지 않았는데, 슬슬 로깅에도 신경써야 할 타이밍이 된 듯하다. 애플리케이션이 점차 커지고, 로직이 많아지다보니 간단한 출력문만으로는 힘들다는 것을 느꼈다. 테스트만큼이나 로깅에도 신경쓸 타이밍이 된 듯 하다.

이번 포스트에선 로깅의 목적, Logback 과 Slf4j 등의 혼동되는 개념에 대해 생각을 정교화하고, 로그 레벨은 어떤것이 있으며 어떤 상황에서 어떻게 로그를 저장하고 추적해야하는지 다루어보고자 한다. 이미 알고있는 사소한 개념일지라도 메타인지 활성화를 위해 글을 적는다. 사실 second brain 에 개념을 정리해봤는데, 생각이 잘 정교화되지 않는 것 같아 블로그에 글을 발행한다. 특히 System.out.println() 과의 차이점, 그리고 이 단순 출력문대신 굳이 로깅을 해야하는 명확한 이유에 대해서도 정리해본다.

MDC 학습 배경

또한 로깅에 대해 학습하고 프로젝트에 도입하던 중 MDC(Mapped Diagnostic Context) 이라는 훌륭한 로깅 기법도 알게 되었다. 향후 MDC 에 대해서도 프로젝트에 도입을 고려하고 있기 때문에, MDC 에 대한 기초 개념 또한 간단히나마 다루고자 한다.

다만 MDC 는 지금 당장 미트윈 프로젝트에 도입할 생각은 없다. MDC 와 같은 효과적인 로그 추적 기법없이 Slf4j 기반 기본 로깅, 로그 파일을 저장하는 방식을 도입하여 MDC 의 필요성을 직접 체감한 후에 도입할 예정이다. 그럼에도 지금 학습하는 이유는 MDC 의 특징을 사전에 이해하고 있어야만 이후 로깅 전략을 리팩토링시 좋은 방향성으로 나아갈 수 있을것이라는 생각 때문이다 🙂


Why?

왜 로깅을 해야하는가? "왜" 라는 질문은 내가 새로운 기술을 학습할 때 가장 먼저 떠올리는 키워드이다.

당연하게도 시스템이 작동될 떄 그 작동 상태의 기록이 보존되어야 하거나 이용자의 습성 분석을 위한 로그를 기록해 둘 필요가 있다. 하지만 보통 (나도 예전에 그랬고) JAVA 프로그래밍을 학습한지 얼마 안된 상태에선 System.out.println() 출력문으로 로그를 출력하면서 나름의 간단한 로깅을 학습한다. 왜 이런 간편하고 익숙한 출력문을 지양하고 Logging 을 하는가? 로깅의 이유는 System.out.println 과 비교하면 도입의 근거를 이해하기 수월해진다. 따라서 System.out.println 과의 비교를 통해 로깅을 학습해보자.

1. 성능에 부하를 줄일 수 있다.

System.out.println 은 리소스를 꽤나 많이 잡아먹는다. 리소르를 많이 사용하기 떄문에 성능에 부하를 준다. 반면 Logger 는 내부 버퍼링과 멀티 쓰레드를 지원해주기 떄문에 성능이 상대적으로 좋다. 그렇다면 왜 리소스를 많이 잡아먹는가?

println 의 경우 아래와 같은 newLine() 이라는 메소드를 호출함으로써 출력이 수행된다. 우리가 유심히 관찰해야 할 키워드는 synchronized 이다. synchronized자바의 동시성 제어 키워드 : volatile 과 synchornized 에서도 다루었듯이, 한 임계영역에 대해 쓰레드를 하나씩 진입시켜 작업을 수행하는 방법으로, 베타적으로 실행된다. 하지만 이는 서비스에 트래픽이 발생할 경우 심각한 성능 저하가 발생할 수 있다는 주요 요인이 된다. 가령 트래픽이 많은 서비스에서 이 방법으로 로깅을 시도한다면 100만개의 쓰레드가 동시에 임계영역에 진입한다고 생각해보자. 운이 좋지 못한 쓰레드는 얼마가 지나서야 로그를 조회할 수 있겠는가? 🤔

private void newLine() {
    try {
        synchronized (this) {
            ensureOpen();
            textOut.newLine();
		// ...
        }
    }
}

2. 다양한 상황에 알맞는 상세한 정보를 제공받을 수 있다.

System.out.println 은 직접 코드를 작성해 우리가 원하는 정보를 만들어야 한다는 매우 번거로운 단점이 존재한다. 반면 Logger 는 클래스의 이름, 시간, 레벨 등의 다양한 정보를 제공해준다.

이를 다르게 해석해보면, System.out.println 를 사용헀을 떄 유의미한 정보를 제공받기가 힘들다. 우리가 로깅을 해야하는 근본적인 이유는 서버에 장애가 발생했을 떄, 또는 개발 도중 중요한 로직을 처리후 매우 상세한 정보를 제공받길 원하여 사용한다. 또한 반드시 유익한 정보만을 로그로 남기고, 필요없는 로그를 최소화하여 불필요한 용량 낭비를 최소화시켜야 한다. 이런면에서 단순 출력문은 적합하다고 할 수 없다.

반면 Slf4j 와 같은 Logging 라이브러리를 활용하면 TRACE, DEBUF, INFO, WARN, ERROR, FATAL 과 같은 다양한 타입의 로깅 레벨을 알맞게 제공받을 수 있다. 또한 앞서 언급했듯이 시간, 이름에 대한 상세한 정보를 제공해준다. 단순 출력문으로 항상 동일한, 무의미한 System.out.println 에 비해 Logger 는 다양한 상황에 알맞게 적절한 로그를 출력하고, 저장할 수 있다.

3. 로그를 기록할 수 있다.

로그의 내용을 콘솔 밖에서 관리하고자 한다면 코드를 통해 로그 파일을 만들어야한다. 반면 Logger 는 appender 를 통해서 쉽게, 또 원하는 조건, 원하는 형태에 따라 로그를 파일로 남길 수 있다.

반면 System.out.println 를 로그로 남긴다면 기록할 수 있겠는가? 이는 단순히 파라미로 넘겨주는 문자열만을 출력하는 기능이다. 만일 가능한다고 한들, System.out.println 로 얻는 이점이 존재하는가? 우리가 로깅을 해야하는 이유에 대해 다시금 짚어보자. 로그를 출력하고, "저장하는" 이유는 유의미한 정보를 조회하여 발생한 장애를 해결하는데에 있다. 하지만 단순 출력문 만으로는 어떤 애러인지, 어떻게 해결해야하는지 파악하기 힘들다.


Logging Framework

이 정도면 로깅을 해야하는 이유에 대해 파악했다고 생각한다. 자바 진영에서 제공하는 Logging 기법, 라이브러리는 무엇이 있을까?

Logging 관련 프레임워크는 대표적으로 log4j, logback, log4j2, 그리고 그것을 통합해서 인터페이스로 제공하는 Slf4j 라이브러리가 있다. 역사적으로 log4j, logback, log4j2 순서대로 등장했으며,logback 과 log4j 는 둘 다 log4j 를 기반으로 하고 있기 때문에 사용 방법이 꽤 유사하다.

💡 log4j 는 2015년 이후로 deprecated 되었다. 따라서 이에 대한 설명은 제외한다.

logback

logbaack 은 log4j 이후에 출시되어, 보편적으로 사용되고 있는 JAVA 진영의 Logging 프헤임워크다. Slf4j 인터페이스의 구현체로써 동작한다는 것이 가장 큰 특징이다. (내가 Slf4j 와 logback 의 차이가 다소 혼동되었는데, 간단히 인터페이스와 구현체의 차이였다.)

slf4j 의 구현체로 동작하기 떄문에 spring-boot-starter-web 안에 spring-bott-starter-logging 이 logback 이 기본적으로 포함되어 있어서 별다른 의존성 추가 없이 사용 가능하다. 우리가 일반적으로 LoggerFactory 를 직접 선언하거나, 또는 @Slf4j 어노테이션을 통해 로깅을 구현할텐데, 이에 대한 구현체는 logback 이다.


로깅 레벨의 종류와 적절한 사용 단계

앞서 설명했듯이 Logging 시 5가지의 레벨이 존재하며, 로그를 섲어할 떄 레벨을 설정하여 원하는 로그만을 출력할 수 있다.

  • error : 사용자 요청을 처리하는 중 발생한 문제 (예상치 못한 에러가 발생했을 때)
  • warn : 처리 가능한 문제이지만, 향후 시스템 에러의 원인이 될 수 있는 문제 (예상한 예외 상황이며, 당장 문제가 발생하진 않아도 문제가 우려되는 경우. 가령 트래픽이 순간적으로 몰리는 상황)
  • info : 로그인이나 상태 변경과 같은 정보성 메시지 (중요한 비즈니스 로직 처리 결과)
  • debug : 개발시 디버깅 목적으로 출력하는 메시지 (개발 단계에서 주로 사용. SQL 로깅에 적합)
  • trace : debug 보다 좀 더 상세한 메시지 (마찬가지로 개발 단계에서 사용. 특정 로직을 상세하게 추적하고 싶을 경우 활용한다.)

프로젝트마다 어떤 상황에서 어떤 레벨까지 도입해야할지는 상이하다. 다만 현재 미트윈 프로젝트에선 3가지 단계 error, warn, info 만을 도입해도 충분할 것이라고 판단된다. 가령 error 는 ControllerAdvice 와 같은 예기치 못한 예외처리 상황에 로깅하기에 매우 적합해보인다. (이미 활용중이기도 하고.) warn 또한 정의 그대로 우려되는 문제에 대해 로깅이 필요할 것으로 판단되며, 당연하게도 비즈니스 로직에 대한 로깅시 info 가 유용하게 쓰일 것으로 생각된다.

한편 아직까진 debug 와 trace 레벨까지 파고들어 의미있는 정보를 추출해낼 필요성을 느끼지 못했기에, 향후 필요성을 느낀다면 도입을 고려해야겠다.

최소 로그 레벨 지정하기

로그를 작성할 때 레벨을 설정해 원하는 로그 레벨들만을 출력할 수 있다. 아래와 같이 application.yml 에 로깅 최소 레벨을 지정하면 해당 레벨 까지만 로깅이 되며, 그 이상은 로깅이되지 않는다. 예를들어 아래처럼 INFO 로 설정시 TRACE, DEBUG 레벨은 로깅 대상에서 제외된다.

logging:
  level:
    root: info

간단한 로깅 설정이라면 위처럼 application.yml 에서 설정이 가능하다. 하지만 상세한 설정대로 동작하길 기대한다면 .xml 파일을 작성해야한다. xml 파일은 src/main/resources 에 생성하도록 하자.


마치며

MDC 에 대한 내용을 함꼐 발행하고자 했으나, 따로 주제를 구분하여 포스트를 발행하는 것이 더 낫겠다는 생각이 들었다. 곧 MDC 에 대해서도 정리해보겠다.


참고