사이드 프로젝트 중 버그가 생긴 경우 그에 대한 요청/응답 값을 추적하던 중에 MDC 를 사용하면 좋을 것 같다는 생각이 들었다.
MDC (Mapped Diagnostic Context) 란
Logback, Log4j2 등에서 지원하는 기능으로 로깅 메시지에 컨텍스트 정보를 저장할 수 있고 로깅 중에 스레드별로 특정 정보를 저장하고 관리하는 메커니즘을 말한다.
주요 이점
- 특정 사용자, 세션, 요청 등의 ID를 로그에 포함시켜 모니터링을 용이하게 한다.
- 사용자 및 요청 정보를 로그에 포함시켜 감사 및 보안 모니터링을 강화할 수 있다.
- 다중 스레드 환경에서 발생하는 요청을 효과적으로 관리하고 구분할 수 있다.
동작 방식
MDC 는 각 스레드에 대한 데이터 저장소를 Map 형태로 제공한다. 그렇기 때문에 스레드마다 고유한 저장 공간을 가지고 있으며 각 스레드 컨텍스트에 데이터를 put 하여 저장하고 remove 를 통해 데이터를 삭제할 수 있다.
※ 사용 방법
1. MDC.put(key, value) : 데이터 추가
2. MDC.get(key) : 특정 키에 대한 데이터 검색
3. MDC.remove(key) : 특정 키에 대한 데이터 삭제
4. MDC.clear() : MDC 의 모든 데이터 삭제
주의 사항
MDC 는 스레드 로컬 변수를 사용하여 데이터를 저장하기 때문에 스레드가 재사용될 경우를 대비하여 꼭 clear() 를 해주어야한다. 또한, 비동기 처리 시에는 MDC 정보가 전달되지 않기 때문에 수동 전달이 필요하다.
Logback MDC 사용하기 with Spring Boot
MDC 를 Intercepter 에 적용하여 특정 유저에 대한 Request 및 Response 를 구분할 수도 있고 비즈니스 로직 내에 사용하여 사용자의 패턴을 분석할 수도 있다. 또한, logback 내 rollingPolicy 를 적용하여 로그를 백업해보자.
1. 로깅 설정 파일 작성 ( logback.xml )
resource 폴더 내 logback.xml 파일을 생성하고 appender 태그 내 원하는 name 과 로그 메시지를 처리할 때 사용할 구체적인 appender class 를 지정하여 로깅 이벤트를 작성한다.
encoder 태그 내 메시지 패턴은 자유롭게 작성하되 아래의 uuid 는 Intercepter 에서 넣어줄 key 추가한 내용이다.
- resources/logback.xml
<configuration>
<!-- 로깅 이벤트 작성 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 타임스탬프 [스레드] 로그레벨 logger(36자까지) [UUID] 로그메시지 -->
<pattern>%d{yy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - [%X{uuid}] %msg %n</pattern>
</encoder>
</appender>
<!-- 로깅 이벤트 출력 -->
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
2-1. 클라이언트 요청 및 응답 로그 생성
유저의 요청 및 응답 로그를 찍기 위해서 HandlerInterceptor 를 상속받은 커스텀 Interceptor 를 생성하고 regitry 에 생성한 Interceptor 를 추가한다.
- LoggingInterceptor.class
@Component
@Slf4j
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uuid = UUID.randomUUID().toString();
MDC.put("uuid",uuid); // ★ uuid 키에 대한 데이터 넣기
log.info("[REQUEST] [" + request.getMethod() + "]" + request.getRequestURI()); // 요청 로그
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("[RESPONSE] [" + request.getMethod() + "]" + request.getRequestURI()); // 응답 로그
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (ex != null) {
log.error( ex.getMessage());
}
MDC.clear(); // ★ 모든 데이터 초기화
}
}
- WebConfig.class
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor()) // ★ Interceptor 추가
.order(1)
.addPathPatterns("/**");
// .excludePathPatterns("/login"); // 특정 패턴 제외 가능
}
}
2-2. 비즈니스 로직 내 로그 생성
의미있는 내용을 넣어보고자 회원 가입 엔드포인트(/sign-up) 을 만들고 트랜잭션 내 로그를 추가하였는데 생략해도 상관없다.
- UserServiceImpl.class
public class UserServiceImpl implements UserService{
..
@Override
public void saveUser(UserDto userDto) {
Optional<User> findUser = userRepository.findByEmail(userDto.getEmail());
if(findUser.isPresent()){
log.info("회원 중복 가입 예외 발생");
} else {
userRepository.save(
User.builder()
.email(userDto.getEmail())
.password(userDto.getPassword())
.build()
);
log.info("회원 가입 성공");
}
}
}
3. 실행 결과
모든 설정을 마치고 애플리케이션을 실행하여 로그를 확인해보자. UUID 를 통해 사용자의 트래픽을 구분할 수 있게 되었다.
4. 주기적으로 로그 백업하기
logback.xml 에 LOGFILE appender 를 추가하여 로그를 백업해보자. 로그 파일 위치는 미리 생성해두어야한다.
- logback.xml
<configuration>
..
<appender name="LOGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 로그 파일 위치와 파일 이름 패턴 -->
<file>/log/til.log</file>
<!-- 백업 정책 ( 매일 자정 ) -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/log/til-%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<!-- 로그 메시지 패턴 -->
<encoder>
<pattern>%d{yy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - [%X{uuid}] %msg %n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="LOGFILE"/> <!-- ★ 추가 -->
</root>
</configuration>
- 실행 결과 ( 한 시간 주기 )
참고
https://medium.com/javarevisited/mapped-diagnostic-context-mdc-6447b598736d
https://www.baeldung.com/mdc-in-log4j-2-logback
https://blogs.halodoc.io/implementation-of-mdc-mapped-diagnostic-context-on-golang/
'Web' 카테고리의 다른 글
[Spring] Mapstruct Mapper Custom with Qualifier, @DecoratedWith (0) | 2024.04.28 |
---|---|
[JPA] deleteAll() 과 deleteAllInBatch() (0) | 2024.03.17 |
[JPA] @ColumnDefault 이해하기 (0) | 2024.01.28 |
[Spring] Soft Delete with JPA (0) | 2024.01.14 |
[Spring] QueryDsl 로 동적 쿼리 짜기 with JPA (0) | 2024.01.07 |