2.1 TraceID
TraceID는 하나의 요청을 시작부터 끝까지 추적할 수 있는 고유 식별자입니다. 마이크로서비스 환경에서 여러 서비스를 거쳐가는 요청을 추적하는 데 필수적입니다.
2.2 TraceIdFilter의 동작 원리
2.2.1 필터의 역할
TraceIdFilter는 모든 HTTP 요청을 가로채서 TraceID를 처리합니다.
파일: TraceIdFilter.java
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter implements Filter {
private static final String TRACE_ID_HEADER = "X-Trace-Id";
private static final String MDC_TRACE_ID_KEY = "traceId";
private static final String MDC_CLIENT_IP_KEY = "client_ip";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// ...
}
}2.2.2 TraceID 생성 및 추출 프로세스
필터는 다음 순서로 동작합니다.
단계 1: TraceID 확인
private String extractOrGenerateTraceId(HttpServletRequest request) {
String traceId = request.getHeader(TRACE_ID_HEADER);
if (traceId != null && !traceId.trim().isEmpty()) {
// 클라이언트가 제공한 Trace ID 사용
log.debug("📥 클라이언트 Trace ID 사용: {}", traceId);
return traceId.trim();
}
// 새로운 Trace ID 생성
String newTraceId = generateTraceId();
log.debug("🆕 새로운 Trace ID 생성: {}", newTraceId);
return newTraceId;
}
private String generateTraceId() {
return UUID.randomUUID().toString();
}- 헤더에
X-Trace-Id가 있으면 재사용 (마이크로서비스 체인) - 없으면 UUID로 새로 생성
단계 2: MDC에 저장
try {
MDC.put(MDC_TRACE_ID_KEY, traceId);
MDC.put(MDC_CLIENT_IP_KEY, clientIp);
httpResponse.setHeader(TRACE_ID_HEADER, traceId);
filterChain.doFilter(request, response);
} finally {
// 반드시 MDC 정리 (Thread Pool 사용 시 메모리 누수 방지)
MDC.remove(MDC_TRACE_ID_KEY);
MDC.remove(MDC_CLIENT_IP_KEY);
}- MDC(Mapped Diagnostic Context)에 traceId 저장
- 응답 헤더에도 traceId 추가
- 중요: finally 블록에서 반드시 MDC 정리
2.2.3 클라이언트 IP 추출
private String extractClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader(X_FORWARDED_FOR_HEADER);
if (xForwardedFor != null && !xForwardedFor.trim().isEmpty()) {
// X-Forwarded-For 헤더가 있는 경우, 맨 왼쪽 IP만 추출
String clientIp = xForwardedFor.split(",")[0].trim();
if (!clientIp.isEmpty() && !"unknown".equalsIgnoreCase(clientIp)) {
log.debug("🌐 X-Forwarded-For에서 클라이언트 IP 추출: {}", clientIp);
return clientIp;
}
}
// X-Forwarded-For가 없거나 유효하지 않은 경우
String remoteAddr = request.getRemoteAddr();
log.debug("🔌 Remote Address 사용: {}", remoteAddr);
return remoteAddr != null ? remoteAddr : "unknown";
}우선순위:
X-Forwarded-For헤더의 맨 왼쪽 IP (실제 클라이언트)request.getRemoteAddr()(직접 연결된 클라이언트)
2.3 MDC
MDC(Mapped Diagnostic Context)는 SLF4J가 제공하는 기능으로, ThreadLocal 기반의 키-값 저장소입니다.
2.3.1 MDC의 장점
// 한 번 설정하면
MDC.put("traceId", "a1b2c3d4-...");
// 어디서든 자동으로 로그에 포함됨
log.info("사용자 조회");
// 출력: [traceId=a1b2c3d4-...] 사용자 조회2.3.2 MDC의 한계: ThreadLocal 문제
MDC는 ThreadLocal 기반이므로 스레드가 바뀌면 사라집니다.
// 메인 스레드
MDC.put("traceId", "a1b2c3d4-...");
executor.submit(() -> {
log.info("비동기 작업");
// ❌ traceId가 없음! (다른 스레드)
});2.4 MDCContext: 멀티스레드 지원
2.4.1 MDCContext의 역할
파일: MDCContext.java
public class MDCContext {
// 현재 스레드의 MDC를 Map으로 복사
public static Map<String, String> capture() {
return MDC.getCopyOfContextMap();
}
// Map을 현재 스레드의 MDC로 복원
public static void restore(Map<String, String> context) {
if (context != null) {
MDC.setContextMap(context);
} else {
MDC.clear();
}
}
// Runnable을 감싸서 MDC를 전달
public static Runnable wrap(Runnable runnable) {
Map<String, String> context = capture();
return () -> {
try {
restore(context);
runnable.run();
} finally {
clear();
}
};
}
// Supplier를 감싸서 MDC를 전달
public static <T> Supplier<T> wrap(Supplier<T> supplier) {
Map<String, String> context = capture();
return () -> {
try {
restore(context);
return supplier.get();
} finally {
clear();
}
};
}
}2.4.2 사용 예시
비동기 작업에서 TraceID 유지:
// ❌ 잘못된 예
executor.submit(() -> {
log.info("비동기 작업"); // traceId 없음
});
// ✅ 올바른 예
executor.submit(MDCContext.wrap(() -> {
log.info("비동기 작업"); // traceId 유지됨!
}));2.4.3 AsyncExecutor 자동화
파일: LoggerAutoConfiguration.java
@Bean
@ConditionalOnMissingBean
public AsyncExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setTaskDecorator(new MDCTaskDecorator()); // ← 자동으로 MDC 전달
executor.setThreadNamePrefix("async-");
executor.initialize();
return new AsyncExecutor(executor);
}파일: MDCTaskDecorator.java
public class MDCTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
return MDCContext.wrap(runnable); // 자동으로 감싸줌
}
}별도 처리 없이 비동기 작업에서도 TraceID가 자동으로 유지됩니다.