라이브러리는 제어의 주체가 개발자이고, 프레임워크는 그 주체가 프레임워크에 있다는 차이가 있습니다.
이번 글에서는, 구현 프레임워크로 Logback을 다룹니다.
slf4j 공식문서에서는 Logback과 같은 functionality를 구현 프레임워크라고 소개하고, Logback 공식문서에서는 로깅 라이브러리라고 소개하고 있습니다.
따라서, 이번 글에서는 프레임워크와 라이브러리라는 단어를 혼용하여 작성하였습니다.
로깅
프로그램 동작시 발생하는 모든 일을 기록하는 행위
기록하는 항목은 아래가 될 수 있다.
- 서비스 동작 상태
- 시스템 로딩
- http 통신
- 트랜잭션
- DB 요청
- 의도를 가진 Exception
- etc
- 장애 - exception, error
- I/O Exception
- NullPointException
- 의도하지 않은 Exception
- etc
로깅 프레임워크와 System.out.println의 차이
- 로깅은 출력 형식을 커스터마이징 할 수 있다.
- 로깅은 로그 레벨에 따라 남기고 싶은 로그를 별도로 지정할 수 있다.
System.out.println
은 무조건 출력해야한다.
- 로깅은 콘솔뿐만 아니라 파일이나, 네트워크 등 로그를 별도의 위치에 남길 수 있다.
- 로깅은
System.out.println
보다 더 좋은 성능을 가지고 있다.
로그 레벨
- Fatal
- 매우 심각한 에러
- 프로그램이 종료될 수 있다.
- Error
- 의도하지 않은 에러
- 프로그램이 종료되지 않는다.
- ex ) 외부 API 호출 실패
- Warn
- 에러가 될 수 있는 잠재적 가능성이 있는 경우
- 이 로그를 통해, 개발자가 위험한 에러를 만나기 전에 해결할 수 있다.
- Info
- 요구사항에 따른 시스템 동작 출력
- 개발자가 의도한, 명확한 의도가 있는 에러
- ex ) 회원가입 시 중복된 email로 가입 요청되어, DuplicatedEmailError가 발생한 경우
- Debug
Info
레벨보다 더 자세한 정보가 필요한 경우- Develop 환경에서 사용한다.
- Trace
Debug
레벨보다 더 자세한 정보가 필요한 경우- Develop 환경에서 버그를 해결하기 위해 사용한다.
- Prod 환경, 커밋에 포함되어서는 안 된다.
로깅과 디버깅의 차이
Prod 환경과 같이 디버깅을 적용할 수 없을 때에는, 로깅을 선택해야 한다.
디버깅을 쓸 수 있는 환경에서는, 더 자세한 예외 trace를 출력해 버그를 잡는 등 프로그래밍에 도움을 받을 수 있도록, 디버깅을 선택해야 한다.
SLF4J 프레임워크
Simple Logging Framework For Java
로깅을 위한 추상화(인터페이스) 역할 프레임워크
- 단독으로 사용하지 않는다.
- 최종 사용자가 배포 시 원하는 구현체(라이브러리)를 선택한다.
SLF4J
가 인터페이스화 되어있어 실제 구현을 몰라도 되므로, 가능하다.
동작과정
1. Bridge
- 다른 로깅 API로부터의 Logger 호출을
SLF4J
인터페이스로 연결해,SLF4J
API가 대신 처리할 수 있도록 하는 모듈 - 이전의 레거시 로깅 프레임워크를
SLF4J
로 호환되게 만든다. - 다른 로깅 API와
SLF4J
API 간의 어댑터 역할 - 여러 개의 Bridge 라이브러리를 사용할 수 있지만, Binding 모듈에서 사용되는 라이브러리와 달라야 한다.
- ex ) 프로젝트의 어떤 컴포넌트는 Jararta Commons Logging(JCL)을 사용하고 있다. 따라서, bridging 모듈에 jcl-over-slf4j 라이브러리를 사용하면,
SLF4J
로 마이그레이션 할 수 있다.
2. SLF4J
API
- 로깅에 대한 추상 레이어, 즉 인터페이스를 제공하는 모듈
- 로깅에 수행할 추상 메서드를 제공한다.
- 하나의
SLF4J
API 모듈에는 하나의 Binding 모듈과 연결되어야 한다.
3. Binding
SLF4J
API를 로깅 구현체와 연결하는 모듈- 로깅 구현체 - ex ) Logback 라이브러리
SLF4J
API와 로깅 구현체 간의 어댑터 역할SLF4J
API를 구현한 클래스에서, Binding으로 연결될 구현체 Logger의 API를 호출한다.- 하나의
SLF4J
API 모듈에는 하나의 Binding 모듈과 연결되어야 한다. - ex ) Logback 구현 프레임워크를 사용하고자 할 때,
logback-classic
라이브러리 의존성을 추가해 바인딩 모듈로 사용할 수 있다.
logback-class 라이브러리는 외부 라이브러리도 함께 가져오는데, artifact에 맞게 별도의 의존성을 추가하는 것이 더 적합하므로, logback-classic 의존성을 넣을 때, SLF4J API와, logback-core 라이브러리는 가져오지 않도록 설정하였다.
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.31'
implementation ('ch.qos. Logback: logback-classic: 1.2.3' ){
exclude group: 'org.slf4j', module: 'slf4j-api'
exclude group: 'ch.qos. logback', module: 'Logback-core'
}
}
4. 구현 프레임워크 - ex ) Logback
- ex ) Logback 구현 프레임워크를 사용하고자 할 때,
logback-core
라이브러리 의존성을 추가해 최종적으로 구현 프레임워크를 사용한다.
dependencies {
implementation 'org.slf4j:slf4j-api:1.7.31'
implementation 'ch.gos. logback:logback-core:1.2.3'
implementation ('ch.qos. Logback: logback-classic: 1.2.3' ){
exclude group: 'org.slf4j', module: 'slf4j-api'
exclude group: 'ch.qos. logback', module: 'Logback-core'
}
}
위 과정은 개발과 배포의 두 단계에 걸쳐 수행된다.
- Bridge →
SLF4J
API - 개발 시,
SLF4J
API를 사용해서 로깅 코드를 작성한다. SLF4J
API → Binding → 구현체 프레임워크( ex. Logback )- 배포 시, 바인딩된 (구현) Logging Framework가 실제 로깅 코드를 수행한다.
Logback
Logback의 구조
- logback-core 모듈
- 다른 두 모듈을 위한 기반 역할
Appender
인터페이스와Layout
인터페이스가 속한 모듈
- logback-classic 모듈
- (logback-core 모듈에서 확장되어) logback-core 모듈을 내장하였고,
SLF4J
API를 구현하는 모듈 - 내부에 logback-core 모듈과
SLF4J
API을 내장하고 있지만, 올바른 artifact 버전을 별도로 사용하는 것이 적합하기 때문에 모두 별도로 의존성을 선언하는 것이 좋다. ( 따라서 exclude 처리해야 한다. ) Logger
클래스가 속한 모듈
- (logback-core 모듈에서 확장되어) logback-core 모듈을 내장하였고,
- logback-access 모듈
- Servlet Container(ex.Tomcat)와 통합되어 HTTP 액세스에 대한 로깅 기능을 제공하는 모듈
- logback-core 모듈 기반 모듈이다.
- logback-classic, slf4j와 무관하다.
- 서블릿 컨테이너 레벨에서 설치한다.
- 웹 어플리케이션 레벨에 설치하는 것이 아니다!
Logback의 설정요소 - Logger, Appender, Layout
Logback은 Logger, Appender, Layout의 3가지 메인 클래스를 기반으로 한다.
- Logger : 어떤 레벨로 로깅할 것인가
- 실제 로깅을 수행하는 모듈(컴포넌트)
- Level 속성을 통해, 출력할 로그의 레벨을 조절할 수 있다.
- default level : debug
- 지정된 레벨 이하의 메서드는 기록되지 않는다.
- Trace < Debug < Info < Warn < Error
- ex ) Info 레벨로 지정된 Logger는 Info, Warn, Error만 기록한다.
- Appender : 어디에 로그를 출력할 것인가
- Logback에서 로그 이벤트를 작성하는 모듈(컴포넌트)
- 로그 메시지가 출력할 대상을 결정한다.
- 콘솔에 출력 : ConsoleAppender
- 파일에 출력 : FileAppender
- 파일을 일정 조건에 맞게 따로 저장하여 출력 : RollingFileAppender
ch.qos.logback.core.Appender
interface를 구현해야 한다.
- Layout(Encoder) : 어떤 레이아웃(형식)으로 최종적으로 출력할 것인가
- 로그 이벤트를 바이트 배열로 변환해 OutputStream에 쓰는 모듈(컴포넌트)
- 사용자가 지정한 형식으로 표현될, 로그 메시지를 변환한다.
- Appender에 포함되어 있다.
서버 환경별 로그 관리 방식 예시
테스트 서버 / 개발 서버에서는 Info 레벨의 로그는 Console에 출력한다.
Prod 서버(실서버)에서는 Info, Warn, Error 레벨의 로그는 각 레벨 별 파일로 저장한다.
Logback 활용 사례 - 오늘 하루를 그려줘 API 서버
‘오늘 하루를 그려줘’ 앱 프로젝트의 api 서버 레포지토리의 로그를 도입한 PR을 통해 다뤄본다.
로그를 적용하기 위해, 3가지의 코드/설정을 수정, 추가하였다.
먼저, SpringSecurity 설정을 수정하였다. ( SecurityConfig.java )
62, 63 번째 코드 라인을 보면, MDCRequestLoggingFilter를 수행하는 코드가 추가되었다.
MDCRequestLoggingFilter
는 현재 실행 중인 컨텍스트(혹은 스레드)에 메타 정보를 넣고 관리할 수 있는 기능을 하는 필터로, 모든 로그에 request_id
를 추가한다.
현재 프로젝트에서는 요청의 jwt 토큰 검증을 위해, jwt 필터
를 거치고 있다. 따라서, 로그에 request_id
를 부여하는 MDCRequestLoggingFilter
의 작업은 jwt filter 이전에 수행되어야 하므로, addFilterBefore를 통해 설정하였다.
.addFilterBefore(new MDCRequestLoggingFilter(), JwtAuthenticationEntryPoint.class);
새롭게 추가된 MDCRequestLoggingFilter
클래스이다. ( MDCRequestLoggingFilter.java )
package tipitapi.drawmytoday.common.log;
import java.io.IOException;
import java.util.UUID;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.web.filter.OncePerRequestFilter;
public class MDCRequestLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
MDC.put("request_id", UUID.randomUUID().toString());
filterChain.doFilter(request, response);
MDC.clear();
}
}
그리고, 로그에 대한 설정 파일 logback-spring.xml을 추가하였다. ( logback-spring.xml )
로그로 남길 encoder에 대한 패턴 - CONSOLE_LOG_PATTERN
, FILE_LOG_PATTERN
을 명세해, 각 appender에서 encoder를 설정할 때 사용하였다.
prod 환경과 test 환경에서는 일정 조건에 맞게 파일로 따로 저장하여 출력하고 싶었기 때문에,RollingFileAppender
클래스의 appender ‘FILE_INFO’
, 'FILE_WARN’
, ‘FILE_ERROR’
를 추가해,<springProfile name="prod">
프로필 설정을 통해 prod 환경에서는 별도의 로그 파일을 저장하도록 구성했다.
dev 환경에서는 콘솔로 출력하고 싶었기 때문에, ConsoleAppender
클래스의 appender를 선언하였다.
<configuration>
<property name="CONSOLE_LOG_PATTERN"
value="[%d{HH:mm:ss.SSS}] [%blue(%X{request_id})] %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg%n"/>
<property name="FILE_LOG_PATTERN"
value="[%d{HH:mm:ss.SSS}] [%blue(%X{request_id})] %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg%n"/>
<property name="LOG_PATH" value="./logs"/>
<appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="FILE_INFO">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM}/info-%d{yyyy-MM}.log</fileNamePattern>
<maxHistory>12</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
</appender>
<appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="FILE_WARN">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM}/warn-%d{yyyy-MM}.log</fileNamePattern>
<maxHistory>12</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
</appender>
<appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="FILE_ERROR">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM}/error-%d{yyyy-MM}.log</fileNamePattern>
<maxHistory>12</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
</appender>
<springProfile name="develop">
<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</springProfile>
<springProfile name="test">
<property name="LOG_PATH" value="~/TipiTapiTestServer/log"/>
<root level="INFO">
<appender-ref ref="FILE_INFO"/>
<appender-ref ref="FILE_WARN"/>
<appender-ref ref="FILE_ERROR"/>
</root>
</springProfile>
<springProfile name="prod">
<property name="LOG_PATH" value="~/SpringServer/log"/>
<root level="INFO">
<appender-ref ref="FILE_INFO"/>
<appender-ref ref="FILE_WARN"/>
<appender-ref ref="FILE_ERROR"/>
</root>
</springProfile>
</configuration>
레퍼런스
https://www.slf4j.org/manual.html
https://logback.qos.ch/manual/architecture.html
'팀 프로젝트 > 오늘하루를그려줘 : 2023.04 ~' 카테고리의 다른 글
성능테스트를 통해 CaffeineCache를 도입한 API의 효과 확인하기 (0) | 2023.09.22 |
---|---|
응답 속도가 늦는 외부 API 호출 이슈 해결하기 (0) | 2023.08.04 |