김영한님 Spring MVC 강의를 듣다가 @SLF4J 를 달고 log.info(), log.debug()등을 통해 로그를 출력하는 예제를 공부하다가 SLF4J에 대해 공부를 해본 내용을 정리하게 되었다.
먼저 로깅이 무엇이고 왜 해야하는지를 알아보자.
로깅이란?
log를 생성하도록 시스템을 작성하는 행위이다.
로깅은 왜 해야 할까?
로그들은 테스트할 때 재현하기 힘든 버그가 개발 완료된 환경에서 발생했을 경우, 그런 버그들에 대한 정보를 알려줄 수 있으며,
구문들 사이에 걸리는 시간 등의 성능에 관한 통계와 정보를 제공할 수 있다.
로그가 제공하는 정보의 양은 프로그램이 실행되는 중에도 설정이 가능한 것이 이상적이다.
설정이 가능할 때, 로그는 예기치 못한 특정 문제들을 디버그하기 위해 코드를 수정하고 다시 적용(redeploy)하지 않아도, 일반적인 정보를 갈무리할 수 있게 해 준다.
초보자들은 프로그래밍에 대해 아는 것에 한계가 있기 때문에 로그를 사용해야 하고,
시스템 설계자들은 시스템의 복잡성 때문에 로그를 이해하고 사용해야 한다
(참고: https://enai.tistory.com/35)
로깅을 어떻게 해야 할까?
단순히 Sout을 이용하는 경우
Sout으로 나오는 다양한 출력들 중 해당 로그를 식별하기가 어렵다. 해당 로그를 식별하기 위해 Sout의 출력에 복잡한 로직을 추가해주는 것 또한 복잡하다.
이 외에도, 다양한 개발 환경에 맞는 로그 레벨을 설정하기 위해 일일히 조건을 설정해줘야 하는 번거로움이 존재한다. 또한, 파일에 저장해야 하는 경우 별도의 코드를 추가로 작성해야 한다.
로깅 라이브러리 활용
이러한 문제를 해결하기 위해 로깅 라이브러리를 사용한다. 로깅 라이브러리는 위에서 말한 문제점을 해결할 수 있는 기능 이외에도 다양한 기능들을 제공한다.
로깅 라이브러리
자바에서 사용할 수 있는 로깅 라이브러리의 종류는 굉장히 많다.
java.util.logging
JDK 1.4부터 포함된 표준 로깅 API. 표준이기 때문에 별도로 라이브러리를 추가하지 않아도 사용할 수 있다.
Log4j
과거에 주로 사용되던 로깅 라이브러리. 현재는 이 Log4j에서 파생된 라이브러리들을 많이 사용한다
Logback
Log4j의 단점을 개선하고 기능을 추가하여 개발한 로깅 라이브러리. 스프링 부트는 이 Logback 라이브러리를 기본으로 제공한다.
어떤 로깅 라이브러리를 사용할까?
SLF4J
SLF4J는 로그 라이브러리는 Logback, Log4J, Log4J2 등등 수많은 라이브러리가 있는데, 그것을 통합해서 인터페이스로 제공한다.
(구현체가 아니다.)
스프링 부트에서 SLF4J를 사용하면 Logback을 기본으로 사용하게 된다.
SLF4J는 인터페이스이다. 즉, 동작하는 기능 자체는 어떤 라이브러리를 사용하던 간에 동일하다. 구현체를 바꿔도 SLF4Jf를 사용하는 Application 코드는 바뀔일이 없다.
로그 선언
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)
Lombok 라이브러리를 사용하면
@Slf4j 애노테이션으로 위 코드를 작성하는 과정을 생략할 수 있다.
Logback 설정하기
Logback을 설정할 때 가장 대표적으로 사용하는 설정 파일은 logback.xml이다.
스프링 부트 환경에서는 확장된 버전인 logback-spring.xml도 사용할 수 있는데, 이는 스프링 환경에 따라 프로파일별(예: dev, test, prod) 설정을 분리하거나, 스프링에서 제공하는 기능을 조금 더 편리하게 활용할 수 있게 해준다.
참고
- 스프링 부트는 logback-spring.xml이 있을 경우 이를 우선적으로 사용하고, 없으면 logback.xml을 사용한다.
- 테스트 시에는 주로 logback-test.xml이 있으면 이를 우선 적용한다.
Logback의 설정요소
Logger
Logger는 “Logger 이름”을 가진 객체로, 어떤 로그 레벨을 사용할지를 정의하고 로그를 생성(기록)하는 주체이다.
Logger는 계층 구조를 가지며, root Logger가 가장 상위에 존재한다.
하위 Logger는 상위 로거의 설정을 상속할 수 있지만, 필요에 따라 다른 설정을 가질 수도 있다.
- name: logger 이름 (주로 패키지나 클래스 기준)
- level: 로그 레벨(TRACE, DEBUG, INFO, WARN, ERROR, OFF)
- additivity: 상위 logger로 로그를 전달할지 여부 (기본값은 true)
<configuration>
<!-- root Logger 설정 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!-- 특정 패키지/클래스 Logger 설정 (예: com.mycompany 패키지에 대한 로그 레벨 별도 설정) -->
<logger name="com.mycompany" level="DEBUG" additivity="false">
<appender-ref ref="FILE" />
</logger>
</configuration>
위 예시에서,
- root Logger는 INFO 이상의 로그만 남기고, STDOUT(Appender)로 출력한다.
- com.mycompany 의 Logger는 DEBUG 레벨로 로그를 남기며, additivity가 false이므로 상위 logger(root)로 전달하지 않고 FILE(Appender)로만 출력한다.
Appender
Appender는 Logger가 생성한 로그를 어디에 기록할지를 정의하는 요소이다.
로그를 콘솔로 보낼 수도 있고(콘솔Appender), 파일로 쓸 수도 있고, DB나 원격 서버로 보낼 수도 있다.
- ConsoleAppender
- 로그 메시지를 콘솔(System.out)로 출력하는 Appender
- 가장 기본적이고 많이 사용하는 Appender
- FileAppender
- 로그 메시지를 지정한 파일에 쌓는 Appender
- 개발/운영 환경에서 일정 기간의 로그를 파일로 남기고 분석할 때 사용한다
- RollingFileAppender
- 일정 용량이나 일정 주기로 로그 파일을 롤링(분할 저장)하는 Appender
- 예: 10MB가 넘어가면 새로운 파일로 생성해서 저장
- 예: 날짜별로 로그 파일을 생성(daily rolling)
- 일정 용량이나 일정 주기로 로그 파일을 롤링(분할 저장)하는 Appender
- AsyncAppender
- 비동기적으로 로그를 처리해 성능에 영향을 덜 주도록 하는 Appender
- 내부적으로 큐를 사용해, Logger가 로그 메시지를 남길 때 즉각적으로 반환하고, 별도의 스레드로 실제 쓰기 작업을 처리한다
<configuration>
<!-- ConsoleAppender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- Layout 설정 -->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- FileAppender -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app.log</file>
<append>true</append>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
Layout
Layout 혹은 Encoder는 로그 메시지(문자열)를 어떤 포맷으로 만들지 정의하는 요소이다.
Logback에서는 일반적으로 Encoder를 통해 메시지 포맷을 설정하며, 그중에서도 가장 자주 사용하는 것이 PatternLayout이다.
<pattern> 태그 안에 변수(formatting tokens)를 넣어서 원하는 형태의 로그 형식을 꾸밀 수 있다.
- %d: 날짜/시간(포맷 지정 가능. 예: %d{yyyy-MM-dd HH:mm:ss.SSS})
- %thread: 현재 실행 스레드 이름
- %level: 로그 레벨 (TRACE, DEBUG, INFO, WARN, ERROR)
- %logger: 로거 이름 (예: 패키지명.클래스명)
- %msg: 실제 로그 메시지
- %n: 줄바꿈(newline)
- %highlight: 컬러로 강조(콘솔에서만)
<encoder>
<pattern>
%highlight(%-5level)
[%thread]
%logger{36}
%d{yyyy-MM-dd HH:mm:ss.SSS}
- %msg%n
</pattern>
</encoder>
위와 같이 <pattern> 안에 표현식을 자유롭게 넣어주면,
[WARN] [main] my.example.ClassName 2024-12-25 10:10:10.123 - 로그 메시지...
와 같은 식으로 콘솔에서 컬러 및 형식이 적용되어 출력된다.
로그 레벨 설정 - 환경 변수
아래와 같이 application.properties에서 로그 레벨을 직접 설정하기도 하지만
# 전체 로그 레벨 설정(기본 info)
logging.level.root=info
# hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=debug
하지만 배포 환경이 다르기 때문에 주로 환경 변수로 만들어 사용한다.
# 환경 변수를 사용하여 기본값 INFO로 설정
logging.level.root=${LOGGING_LEVEL_ROOT:INFO}
logging.level.hello.springmvc=${LOGGING_LEVEL_HELLO:DEBUG}
- 이렇게 해두면 LOGGING_LEVEL_ROOT라는 환경 변수가 존재하면 그 값을 사용하고, 없으면 INFO를 기본값으로 사용한다.
- 같은 방식으로 LOGGING_LEVEL_HELLO 환경 변수가 있으면 그 값을 사용하고, 없으면 DEBUG가 사용된다.
운영 환경(혹은 로컬)에서 LOGGING_LEVEL_ROOT=ERROR 같은 식으로 주면, Spring Boot 구동 시점에 이 변수를 읽어서 설정하게 된다.
logback.xml? logback-spring.xml?
- 스프링 부트가 logback-spring.xml 파일을 인식할 때는 스프링 Environment(application.properties, 애플리케이션 환경 변수 등)를 통해 @Value 또는 ${...} 형태의 플레이스홀더를 해석할 수 있다.
- 반면, 일반 logback.xml 파일은 스프링이 아닌 순수 Logback 메커니즘을 통해 로딩되므로, ${...}와 같은 플레이스홀더를 바로 인식하지 못한다.
- 따라서 “스프링 부트에서 설정한 환경 변수(또는 application.properties 값)”을 Logback 설정에 반영하고 싶다면, 반드시 logback-spring.xml 파일을 사용해야 한다.
정리
- Logger: 어떤 로그 레벨로 찍을 것이며, 어떤 Appender를 사용할 것인지 결정하는 “로거” 객체이다.
- Appender: Logger가 생성한 로그를 실제로 “어디에 기록할지” 결정하는 요소이다. (콘솔, 파일, DB 등)
- Layout(Encoder): 로그 메시지의 “형식”을 정의한다. (날짜/시간, 로거 이름, 로그 레벨 등)
Logback은 유연한 설정이 가능하고, 스프링 부트와 자연스럽게 연동돼 편리하게 로그를 남길 수 있다. SLF4J(인터페이스) + Logback(구현체) 조합은 스프링 부트 프로젝트에서 사실상 표준으로 자리 잡았다고 해도 과언이 아니다.
- 개발 단계에서는 DEBUG 혹은 TRACE 레벨로 상세 정보를 확인하고,
- 배포 후에는 INFO 또는 WARN 이상의 로그만 출력하여 성능을 확보하는 식으로,
환경이나 프로파일에 따라 로그 레벨과 Appender, 파일 롤링 정책 등을 다르게 설정해 줄 수 있다는 점이 큰 장점이다.