MDC는 요청별 로그 흐름을 구분할 수 있도록 도와주는 로깅 컨텍스트이다. 멀티스레드 환경에서 각 요청은 별도의 스레드에서 처리되기 때문에 MDC는 내부적으로 ThreadLocal<Map<String, String>>
형태로 동작하며 현재 스레드에 컨텍스트 데이터를 저장한다.
Spring Boot와 같은 웹 애플리케이션은 동시에 수많은 요청을 처리하고 각 요청은 별도의 스레드에서 수행된다. 이 때 각 요청의 로그 흐름을 추적하려면 요청마다 고유한 컨텍스트 정보(traceId, method, uri 등)를 로그에 남겨야한다.
MDC를 적용하지 않았을 경우
[INFO] 로그인 완료
[INFO] 상품 등록 완료
이렇게 누가 어떤 요청을 보낸 건지 구분이 불가능하다.
MDC를 적용한 경우
[INFO] [traceId=abc123] 로그인 완료
[INFO] [traceId=abc123] 상품 등록 완료
traceId
덕분에 요청 흐름을 정확히 추적 가능하다.
// 값 저장
MDC.put("traceId", UUID.randomUUID().toString());
MDC.put("method", request.getMethod());
// 로그 출력 시 자동 포함
log.info("요청 처리 시작");
// 요청 끝나면 반드시 clear
MDC.clear();
MDC.clear()는 왜 필수 인가?
MDC는 ThreadLocal 기반으로 동작하므로 요청이 끝난 후에도 값이 스레드에 남아 있을 수가 있다. 그래서 스레드 풀을 재사용할 때 다음 요청이 이전 요청 정보가 섞이는 문제가 발생할 수 있기 때문이다. 이렇게 되면 로그가 오염되고 사용자 정보 혼동 등의 심각한 오류 유발 가능성이 생길 수 있다. 그렇기 때문에 반드시 다 사용하고 나선 MDC.clear()를 사용해줘야 한다.
Key | 예시 값 | 설명 |
---|---|---|
traceId | UUID.randomUUID().toString() | 요청 단위 식별: 요청 흐름 전체 추적 |
userId | user.getId() or user.getEmail() | 사용자 식별: 누가 요청했는지 추적 가능 |
method | HTTP 요청 메서드(GET , POST 등) | 요청 성격 구분 |
uri | /api/items/1 | 어떤 API에 대한 요청인지 추적 |
ip | request.getRemoteAddr() | 보안 로그, 이상 요청 탐지 등 |
Key | 예시 값 | 설명 |
---|---|---|
authorization | JWT 토큰 | 디버깅 시 유용하나 보안 유의(마스킹 필수) |
client | web , android , ios 등 | 플랫폼별 요청 구분 시 유용 |
locale | ko_KR , en_US | 글로벌 서비스일 경우 로케일별 분석 |
requestId | TraceId와 별도로 클라이언트에서 보내는 ID | 프론트/백 로그 연결에 사용 |
sessionId | request.getSession().getId() | 동시 로그인 추적 등에 활용 가능 |
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%5level) %cyan(%logger) - %magenta(%msg) traceId=%X{traceId} userId=%X{userId}%n
</pattern>
%X{key}
: MDC에 저장한 key 값%msg
: 실제 로그 메세지traceId
가 포함되게 된다.구분 | 설명 |
---|---|
INFO | 로그 레벨 (DEBUG < INFO < WARN < ERROR) |
MDC | 로그에 자동 포함되는 추가 컨텍스트 정보 |
INFO는 로그의 중요도를 나타내는 것이고 MDC는 로그의 부가 정보를 채워주는 기능이다.
@Component
public class LoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
MDC.put("traceId", UUID.randomUUID().toString());
MDC.put("method", request.getMethod());
MDC.put("uri", request.getRequestURI());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
MDC.put("userId", auth.getName());
}
filterChain.doFilter(request, response);
} finally {
MDC.clear(); // 반드시 clear해줘야 함
}
}
}
userId
는 내가 UserDetailsImpl에서 유니크한 값인 email
로 했기 때문에 난 이메일로 찍히게 된다.
지금까지는 traceId
, userId
를 로그에 직접 붙여 출력했는데, MDC를 활용하면 로그 구성과 관리가 훨씬 깔끔하고 일관성 있게 가능하다는 걸 알게 되었다. 특히 멀티스레드 환경에서 스레드풀 재사용의 문제점 때문에 MDC.clear()가 꼭 필요하다는 걸 알 수 있었다. 앞으로 로그 설계 시 MDC를 적극 활용해 로그 추적성을 높이는 설계를 해야겠다.