MDC: 로그에서 길을 잃지 않는 방법

“이 로그가 어떤 사용자 요청에서 발생한 거지?”

멀티스레드 환경에서 로그를 분석해본 개발자라면 한 번쯤 이런 좌절을 경험했을 것입니다. 오늘은 1997년에 제안되어 현재까지 업계 표준으로 자리잡은 MDC (Mapped Diagnostic Context) 패턴에 대해 이야기해보겠습니다.


문제: 로그의 미아들

멀티스레드 서버에서 여러 요청을 동시에 처리할 때, 로그는 이렇게 보입니다:

10:23:45 [INFO] Processing order
10:23:45 [INFO] Processing order
10:23:46 [INFO] Payment completed
10:23:46 [INFO] Processing order
10:23:46 [ERROR] Payment failed

다섯 개의 로그가 있지만, 어떤 사용자의 요청인지, 어떤 주문인지 전혀 알 수 없습니다. 마치 이름표 없이 떠다니는 미아들 같죠.

기존 해결책들의 한계

“클라이언트마다 별도 Logger를 만들면 되지 않나요?”

가능합니다. 하지만 동시 접속자가 1000명이라면? Logger가 1000개가 됩니다. 관리 비용이 폭발적으로 증가하죠.

“로그 메시지에 직접 ID를 넣으면 되잖아요?”

logger.info("[user=john][txn=1001] Processing order");

매번 수동으로요? 실수하기 딱 좋고, 일관성도 보장되지 않습니다.


해결책의 탄생: Neil Harrison의 통찰

1997년, Neil Harrison은 이 문제에 대한 우아한 해결책을 제안했습니다.

그의 논문 “Patterns for Logging Diagnostic Messages” (Pattern Languages of Program Design 3, Addison-Wesley)에서 세 가지 패턴을 소개했는데, 그 중 두 번째가 바로 Diagnostic Context입니다.

핵심 아이디어는 간단합니다:

“물고기에 태그를 달아 이동을 추적하듯, 로그 이벤트에 공통 태그를 달아 트랜잭션 흐름을 추적하자”

이것을 Fish Tagging이라고 부릅니다.


MDC란 무엇인가?

**MDC (Mapped Diagnostic Context)**는 스레드별로 key-value 쌍을 저장하는 dictionary입니다.

┌─────────────────────────────────────┐
│  MDC = ThreadLocal<Map<Key, Value>> │
└─────────────────────────────────────┘

각 스레드가 자신만의 “이름표 주머니”를 가지고 있다고 생각하면 됩니다. 로그를 찍을 때마다 Logger가 자동으로 이 주머니에서 이름표들을 꺼내 로그에 붙여줍니다.

사용 예시

// 요청 시작 시 컨텍스트 설정
MDC.put("user", "john");
MDC.put("txn", "1001");
 
// 이제 그냥 로깅하면...
logger.info("Processing order");
logger.info("Payment completed");
 
// 출력:
// 10:23:45 [user=john txn=1001] Processing order
// 10:23:46 [user=john txn=1001] Payment completed
 
// 요청 끝나면 정리
MDC.clear();

마법처럼 모든 로그에 컨텍스트가 자동으로 붙습니다!


NDC에서 MDC로: 진화의 역사

Neil Harrison의 원래 제안은 스택 기반이었습니다. 이것이 **NDC (Nested Diagnostic Context)**입니다.

// NDC: 스택 기반 (push/pop)
NDC.push("user=john");
NDC.push("txn=1001");
// ... 작업 ...
NDC.pop();
NDC.pop();

하지만 스택은 순서에 의존적이라 유연하지 못했습니다. 그래서 Map 기반의 MDC가 등장했죠.

// MDC: Map 기반 (put/get/remove)
MDC.put("user", "john");
MDC.put("txn", "1001");
// 순서 상관없이 개별 제거 가능
MDC.remove("txn");

타임라인

연도이벤트
1997Neil Harrison, Diagnostic Context 패턴 제안
1999Log4j 1.x, NDC 구현 (스택 기반)
2006Log4j 1.2, MDC 추가 (Map 기반)
2010SLF4J/Logback, MDC 표준화
2014Log4j 2.x, Thread Context로 NDC+MDC 통합

현재 MDC는 사실상 업계 표준이 되었습니다.


다양한 언어에서의 MDC

MDC는 Java에서 시작했지만, 이제 거의 모든 언어에서 비슷한 패턴을 제공합니다.

Java (SLF4J/Logback)

MDC.put("requestId", "abc-123");
logger.info("Processing");
MDC.remove("requestId");

Go (Zap)

logger := zap.L().With(zap.String("requestId", "abc-123"))
logger.Info("Processing")

Python (structlog)

log = log.bind(request_id="abc-123")
log.info("Processing")

C++ (spdlog)

spdlog::mdc::put("requestId", "abc-123");
spdlog::info("Processing");
spdlog::mdc::remove("requestId");

패턴은 같습니다: 컨텍스트를 설정하고, 로깅하고, 정리한다.


주의사항: Thread-Local의 함정

MDC는 thread_local 저장소를 사용합니다. 이것이 강점이자 약점입니다.

강점

  • 스레드 간 간섭 없음 (thread-safe)
  • 동기화 오버헤드 없음

약점: Cross-Thread 전파 불가

Thread A                      Thread B
┌─────────┐                  ┌─────────┐
│ MDC:    │    ────X────▶    │ MDC:    │
│ user=A  │   (전파 안됨!)    │ (비어있음) │
└─────────┘                  └─────────┘

비동기 작업이나 Thread Pool을 사용할 때, 컨텍스트가 자동으로 전파되지 않습니다.

해결 방법

// 스레드 전환 전에 수동으로 복사
Map<String, String> context = MDC.getCopyOfContextMap();
 
executor.submit(() -> {
    MDC.setContextMap(context);  // 새 스레드에서 복원
    try {
        doWork();
    } finally {
        MDC.clear();  // 반드시 정리!
    }
});

spdlog 문서에도 명시되어 있습니다:

“MDC is not supported in asynchronous mode due to its reliance on thread-local storage.”


현대적 발전: Distributed Tracing

MDC는 단일 프로세스 내에서 훌륭하게 동작합니다. 하지만 마이크로서비스 환경에서는?

서비스 A → 서비스 B → 서비스 C로 요청이 흘러갈 때, MDC만으로는 전체 흐름을 추적할 수 없습니다.

이를 해결하기 위해 Distributed Tracing이 등장했습니다:

MDC (단일 프로세스)
    ↓
Correlation ID (서비스 간 HTTP 헤더로 전파)
    ↓
Distributed Tracing (OpenTelemetry, Jaeger, Zipkin)

OpenTelemetry는 MDC 개념을 확장하여, 서비스 경계를 넘어 컨텍스트를 자동으로 전파합니다.


정리: 언제 MDC를 사용할까?

상황MDC 적합성
단일 스레드 요청 처리최적
멀티스레드, 동기 처리적합
비동기 작업, Thread Pool수동 전파 필요
마이크로서비스 간 추적Distributed Tracing 권장

MDC는 27년이 지난 지금도 여전히 유효한 패턴입니다. 복잡한 시스템에서 로그를 추적 가능하게 만드는 가장 간단하고 효과적인 방법이니까요.


마치며

Neil Harrison이 1997년에 제안한 “Diagnostic Context” 패턴은 현대 로깅 시스템의 근간이 되었습니다.

핵심은 단순합니다:

“로그에 컨텍스트를 자동으로 붙여라. 그러면 로그가 길을 잃지 않는다.”

다음에 멀티스레드 환경에서 로그 분석에 어려움을 겪는다면, MDC를 떠올려보세요. 27년 된 해결책이지만, 여전히 가장 우아한 답일 수 있습니다.


참고 자료