Skip to Content
Backend비동기 워크플로에서 MDC가 사라지는 문제를 어떻게 잡았나
💻 Backend2025년 3월 10일

비동기 워크플로에서 MDC가 사라지는 문제를 어떻게 잡았나

#identity-platform#mdc#async#thread-pool#observability
Identity Platform · Series 2 · Admin Model & Observability7 / 15Backend
MDC가 비동기 스레드에서 유실되는 문제를 TaskDecorator로 해결한 과정.

구조화 로깅을 붙이고 나면 이전에는 보이지 않던 문제가 더 잘 보인다. 이번 경우가 그랬다. 동기 요청에서는 MDC에 넣어둔 userId, requestId, traceId 같은 필드들이 잘 남는데, 비동기 작업으로 넘어가면 갑자기 이 필드가 전부 비어 있었다. 로그 포맷 문제가 아니라 MDC 자체가 다른 스레드로 전달되지 않았던 것이다.

Note

MDC(Mapped Diagnostic Context)는 SLF4J/Logback이 제공하는 스레드 단위 키-값 저장소다. MDC.put("userId", "user123")처럼 값을 넣으면, 해당 스레드에서 찍히는 모든 로그에 자동으로 userId=user123이 포함된다. 로그 패턴에 %X{userId}를 넣거나 JSON 로깅에서 자동 필드로 출력할 수 있어, 요청 추적과 사용자 식별의 핵심 도구다. 내부적으로 Java의 ThreadLocal을 사용한다.

왜 이런 일이 생겼나

MDC가 ThreadLocal 기반이라는 것이 핵심이다. HTTP 요청 스레드에서 넣은 사용자 ID와 요청 ID는 그 스레드 안에서는 살아 있지만, @Async나 별도 executor를 통해 다른 스레드로 넘어가면 자동으로 복사되지 않는다.

상황MDC 값 상태
동기 요청 처리정상적으로 유지
새 스레드에서 실행되는 비동기 작업기본적으로 유실
스레드 풀 재사용 환경이전 값 오염 가능성까지 존재

문제는 “가끔 사라진다”가 아니라 아무 조치를 하지 않으면 사라지는 것이 기본 동작이라는 데 있었다.

어떤 방식으로 고쳤나

부모 스레드의 MDC 맵을 작업 제출 시점에 복사한다

비동기 작업이 executor에 제출될 때 현재 스레드의 MDC 스냅샷을 캡처한다.

자식 스레드가 시작될 때 그 값을 다시 세팅한다

새 스레드에서 작업이 실행되기 전에 캡처된 MDC 값을 복원한다.

작업이 끝나면 반드시 비운다

스레드 풀은 스레드를 재사용하기 때문에, 정리하지 않으면 이전 요청의 MDC 값이 다음 요청 로그에 섞일 수 있다.

핵심은 세 번째다. 복사보다 정리가 중요할 때가 많다.

TaskDecorator를 적용하면 MDC 전파와 정리가 executor 수준에서 자동으로 처리된다:

왜 TaskDecorator 패턴을 택했나

대안도 있었다. 요청 객체에서 필요한 값을 다시 꺼내거나, 비동기 메서드 인자로 직접 전달하는 방식도 생각할 수 있다. 하지만 그런 방식은 비동기 작업마다 같은 보일러플레이트를 반복하게 만든다.

아래 코드는 실제 구현을 그대로 옮긴 것이 아니라, 당시 구조를 설명하기 위한 개념 예시다.

java
conceptual/MDCPreservingTaskDecorator.java
public class MDCPreservingTaskDecorator implements TaskDecorator { @Override public Runnable decorate(Runnable runnable) { Map<String, String> contextMap = MDC.getCopyOfContextMap(); return () -> { try { if (contextMap != null) { MDC.setContextMap(contextMap); } runnable.run(); } finally { MDC.clear(); } }; } }

TaskDecorator는 스레드 풀 경계에서 한 번 처리하면, 그 executor를 쓰는 작업 전체에 동일한 규칙을 적용할 수 있다. 유틸 함수보다 실행 환경의 규칙으로 고치는 편이 낫다고 판단했다.

이 해결이 나중에 더 중요해진 이유

이 패턴은 이후 Audit Event의 비동기 발행에서도 그대로 재사용됐다. 요청을 처리한 사용자 맥락과 trace 정보를 이벤트 발행 로그까지 자연스럽게 이어 붙일 수 있었기 때문이다.

만약 여기서 MDC 전파를 잡아두지 않았다면, 나중에 비동기 이벤트 파이프라인을 설계할 때도 같은 문제를 다시 맞닥뜨렸을 가능성이 크다. 이 수정은 작은 트러블슈팅이 아니라 다음 단계 설계를 위한 선행 정리였다.

트레이드오프

  • 스레드 풀을 직접 관리하는 영역이 늘어나면 적용 누락 가능성도 생긴다.
  • MDC 필드를 너무 많이 전파하면 오히려 비용과 복잡성이 커질 수 있다.
  • 가상 스레드나 다른 실행 모델을 도입하면 접근 방식을 다시 검토해야 한다.

비동기 처리에서 MDC가 사라지는 문제는 로그 품질 문제처럼 보이지만, 실제로는 실행 모델과 책임 경계의 문제다. 전파 시점과 정리 시점을 명확히 잡아두고 나니, 이후 이벤트 발행과 성능 추적에서도 같은 MDC 값을 훨씬 안정적으로 재사용할 수 있었다.

다음 시리즈에서는 그 연장선에서 Audit Event 발행 구조를 어떻게 3단계에 걸쳐 진화시켰는지 이어서 본다.

Last updated on