MDC의 개념과 쓰레드와의 관계

업데이트:

| MDC와 쓰레드


1. MDC란?

Mapped Diagnostic Context의 약자로, 쓰레드 단위로 특정 데이털르 읽고/쓸 수 있는 저장소입니다. 로깅 프레임워크에서 사용하는 기능입니다. 로깅 프레임워크에서는 로그를 출력할 때, 로그의 내용을 출력하는 것 뿐만 아니라, 로그의 내용을 더 자세하게 출력하기 위해 추가적인 정보를 출력할 수 있습니다. 이때, MDC를 사용하면 로그의 내용을 더 자세하게 출력할 수 있습니다.


MDC 기본구조

public class MDC {
    private static final ThreadLocal<Map<String, String>> contextMap 
        = new InheritableThreadLocal<Map<String, String>>() {
            @Override
            protected Map<String, String> initialValue() {
                return new HashMap<>();
            }
        };

    public static void put(String key, String value) {
        contextMap.get().put(key, value);
    }

    public static String get(String key) {
        return contextMap.get().get(key);
    }

    public static void remove(String key) {
        contextMap.get().remove(key);
    }

    public static void clear() {
        contextMap.get().clear();
    }
}


2. MDC의 특징

  • MDC는 실행 쓰레드들에 공통값을 주입하여 의미있는 정보를 추가해 로깅할 수 있도록 제공합니다.
  • 로깅할때는 멀티 쓰레드 환경에서 실행되는 task는 로그가 섞여 제대로 확인하기 힘든데, 실행되는 쓰레드마다 고유한 값을 주입하여 실행 흐름을 트래킹할 수 있습니다.
  • MDC는 THREAD_LOCAL을 사용하여 구현되어 있습니다.


3. MDC의 사용법

  • MDC.put(“key”, “value”) : MDC에 key와 value를 저장합니다.
  • MDC.get(“key”) : MDC에 저장된 key의 value를 가져옵니다.
  • MDC.remove(“key”) : MDC에 저장된 key를 제거합니다.
  • MDC.clear() : MDC에 저장된 모든 key와 value를 제거합니다.

ThreadLocal을 사용하여 구현되어 있기 때문에, 따로 ThreadLocal을 선언할 필요가 없습니다.



import java.io.IOException;
import java.util.UUID;

import org.jboss.logging.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class MDCLoggingFilter implements Filter {

  private static final String REQUEST_ID_MDC_KEY = "request_id";
  private static final String REQUEST_ID_HEADER = "X-Request-Id";

  @Override
  public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
    final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    final HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

    String requestId = httpServletRequest.getHeader(REQUEST_ID_HEADER);

    if (requestId == null || requestId.isEmpty()) {
      requestId = UUID.randomUUID().toString();
    }

    try {
      // MDC에 request_id 및 기타 정보를 추가
      MDC.put(REQUEST_ID_MDC_KEY, requestId);

      // 요청 속성에 request_id 추가
      httpServletRequest.setAttribute(REQUEST_ID_MDC_KEY, requestId);

      // 응답 헤더에 request_id 추가
      httpServletResponse.setHeader(REQUEST_ID_HEADER, requestId);

      filterChain.doFilter(servletRequest, servletResponse);
    } finally {
      // MDC에서 사용한 모든 키 제거
      MDC.remove(REQUEST_ID_MDC_KEY);
    }
  }
}

ThreadLocal에 request_id라는 key 값으로 UUID를 넣어 각 요청(스레드) 별 로그를 구분할 수 있게 되었습니다.
사용된 Thread는 ThreadPool에 다시 돌아가기에 반드시 설정한 상태 값은 제거해주어야 합니다.


4. MDC에서 remove를 해주는 이유

  • MDC에 저장된 key와 value는 쓰레드마다 고유한 값이기 때문에, 쓰레드가 종료되면 MDC에 저장된 key와 value를 제거해주어야 합니다. 그렇지 않으면, 다음에 실행되는 쓰레드에서 이전 쓰레드의 MDC에 저장된 key와 value를 사용할 수 있습니다.


  • 쉽게 말해 MDC는 THREAD_LOCAL을 사용하여 구현되어 있기 때문에,각 요청이 끝날 때 MDC 값을 제거하지 않으면
        Thread-1: Request A -> Request C -> Request E
        Thread-2: Request B -> Request D -> Request F
    
    • 정보 유출: Request A의 정보가 Request C에 유출될 수 있습니다.
    • 잘못된 로깅: Request C의 로그에 Request A의 정보가 포함될 수 있습니다.
    • 예기치 않은 동작: Request C의 코드가 Request A의 MDC 값을 기반으로 동작할 수 있습니다

잘못된 예시

반납하지 않음

  // Request A 처리
  MDC.put("requestId", "A-123");
  MDC.put("userId", "user1");
  // ... Request A 처리 ...
  // MDC.clear(); // 이 부분이 없다면?
  
  // Request C 처리 (같은 스레드에서)
  String requestId = MDC.get("requestId"); // "A-123"이 반환됨
  String userId = MDC.get("userId"); // "user1"이 반환됨
  // Request C는 잘못된 정보로 처리됨

올바른 예시


try {
    MDC.put("requestId", generateRequestId());
    MDC.put("userId", getCurrentUserId());
    // 요청 처리
} finally {
    MDC.clear(); // 모든 MDC 값을 제거
}




5. MDC와 ThreadLocal의 차이

MDC와 ThreadLocal은 밀접한 관계가 있지만, 완전히 같지는 않습니다.
MDC는 ThreadLocal을 기반으로 구축된 더 높은 수준의 추상화로, 로깅에 특화된 기능을 제공합니다. MDC를 사용하면 ThreadLocal의 이점을 누리면서도, 로깅에 더 특화된 편리한 기능을 활용할 수 있습니다.

1.용도:
ThreadLocal은 일반적인 목적으로 사용되는 반면, MDC는 주로 로깅을 위해 사용됩니다.

2.구현:
MDC는 ThreadLocal을 기반으로 구현되어 있지만, 추가적인 기능을 제공합니다.

3.API:
MDC는 로깅에 특화된 메서드(put, get, remove, clear 등)를 제공합니다.

4.데이터 구조:
ThreadLocal은 단일 값을 저장하지만, MDC는 key-value 쌍의 맵을 저장합니다.

참고사이트


https://lion-king.tistory.com/entry/MDCMapped-Diagnostic-Context

댓글남기기