Spring Boot 프로젝트에서 @RestControllerAdvice로 에러를 한곳에서 처리하고, 로그에 traceId를 남기고 싶을 때가 있다. 이때 많은 사람들이 흔히 하는 실수가 있다. 바로 “Advice 안에서 traceId를 만들고 로깅하는 것”이다.
겉보기엔 간단하지만, traceId는 요청 전체를 추적하기 위한 값이기 때문에 예외가 터진 시점이 아니라, 요청이 들어온 시점에 만들어야 한다. 그래야 “요청 시작 → 서비스 로직 → 응답/예외 처리” 모든 로그가 같은 traceId로 묶인다.
그래서 등장하는 개념: MDC (Mapped Diagnostic Context)
MDC는 로그에 문맥(Context)을 담는 기능이다.
각 스레드마다 독립적인 key-value 맵을 가지고 있어서 “현재 요청에 대한 추가 정보”를 저장할 수 있다.
예를 들어 이렇게 동작한다.
MDC.put("traceId", "8b92f-1234-abc");
log.info("요청 처리 시작");
logback 패턴을 아래처럼 설정해두면, 이 traceId가 자동으로 로그에 포함된다.
<pattern>%d{HH:mm:ss.SSS} %-5level [traceId=%X{traceId}] %logger - %msg%n</pattern>
15:12:43.802 INFO [traceId=8b92f-1234-abc] com.example.StoreService - 가맹점 조회 시작
15:12:43.913 INFO [traceId=8b92f-1234-abc] com.example.StoreService - DB 조회 완료
15:12:43.920 ERROR [traceId=8b92f-1234-abc] com.example.GlobalExceptionHandler - 예외발생
즉, MDC는 “로그에 문맥을 입혀주는 ThreadLocal 기반 저장소”다.
덕분에 로그 한 줄만 봐도 “이 로그가 어떤 요청에서 나온 건지” 바로 알 수 있다.
그렇다면 traceId는 어디서 넣을까?
정답은 필터(Filter) 다. 요청이 들어올 때 가장 먼저 실행되는 곳이기 때문이다.
TraceIdFilter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String traceId = Optional.ofNullable(req.getHeader("X-Request-Id"))
.orElse(UUID.randomUUID().toString());
MDC.put("traceId", traceId);
res.setHeader("X-Request-Id", traceId); // 응답 헤더에도 넣어주기
try {
chain.doFilter(req, res);
} finally {
MDC.clear(); // 꼭 정리!
}
}
}
이렇게 하면
- 요청 시작 시 traceId 생성/승계
- 모든 로그에 자동 삽입 (MDC + logback 설정)
- 응답에도 traceId 헤더 포함
까지 한 번에 해결된다.
그럼 Advice는?
@RestControllerAdvice는 예외가 발생한 뒤 호출된다.
이 시점에서 새 traceId를 만들면 “정상 로직 로그”와 “에러 로그”가 다른 ID로 찍히는 문제가 생긴다.
그래서 Advice에서는 MDC에 이미 있는 traceId를 읽기만 한다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
String traceId = MDC.get("traceId");
log.error("Unhandled exception traceId={}", traceId, e);
return ResponseEntity.internalServerError()
.body(new ErrorResponse("INTERNAL_ERROR", "서버 오류 발생", traceId));
}
}
마지막 정리
MDC는 “로그에 문맥을 담는 저장소”,
traceId는 “요청을 구분짓는 꼬리표”,
그리고 그 꼬리표는 필터에서 만들어 전 구간에 퍼뜨리는 게 정석이다.
이렇게 해두면 logback 설정만으로도 요청 단위의 모든 로그를 하나로 묶어 추적할 수 있고, 분산 환경에서는 X-Request-Id, traceparent 등 외부 시스템과도 자연스럽게 연결된다.
