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";
}

우선순위:

  1. X-Forwarded-For 헤더의 맨 왼쪽 IP (실제 클라이언트)
  2. 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가 자동으로 유지됩니다.