MDC란 무엇인가 그리고 어떻게 설정할 수 있을까?
- 개발
- 2026. 1. 6. 22:09
MDC는 ThreadLocal에 로그 컨텍스트를 저장해두고, logframwork가 자동으로 꺼내 쓰게 만드는 장치라고 합니다.

위의 메시지로 그려보면 위와 같은 그림이 그려집니다. 그렇다면 우리는 어떤것을 학습해야 할까요?
1. Thread Local이란 무엇인가?
2. 무엇을 보고 logFramwork는 ThreadLocal에서 꺼내게 만드는가?
요런것들이 학습되어야 하지 않나 생각이 듭니다. MDC가 비동기에서 어떻게 동작하는지 실제로 설정은 어떻게 하는지 등등을 학습하면 될거 같습니다.
Thread Local이란 무엇이고 어떻게 저장하는가?
Thread Local은 쓰레드마다 가지고있는 저장소라고 합니다. 그렇다면, 각각 쓰레드는 어떻게 ThreadLocal에 저장할까요?
ThreadLocal<String> t = new ThreadLocal<>();
t.set("aaa");
저장은 이런식으로 되어집니다. 어떻게 저장이 되는지 내부를 확인해보겠습니다.
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
map.put(this, value); // this = ThreadLocal 객체
}
순수 자바를 사용한다고 한다면, 직접 쓰레드를 제어를 해야겠지만 스프링 부트같은 프레임워크를 사용한다고 한다면, service나 레파지토리등 일반적인 클래스에 평문으로 사용해도 직접 쓰레드를 제어할 필요는 없습니다.
이 부분은 스프링이 알아서 할겁니다. 더 정확하게 말하면 톰켓(웹 서버)에서 어떤 쓰레드에 저장할지 결정한다는 특징을 가지고 있습니다.
그렇다면, ThreadLocal에 무분별하게 저장해도 괜찮을까요? 결론부터 말씀드리자면 ThreadLocal은 스레드 풀 환경에서는 반드시 수명 관리를 하지 않으면 메모리 누수와 컨텍스트 오염을 일으킨다고합니다. 그러면 어떻게 관리할 수 있을까요? 일정 주기에 ThreadLocal에 저장된 데이터를 지우는 작업이 필요합니다.
그러면 이것을 사용하는 MDC도 마냥 안전하지 않은 기술인거 같은데요. 어떻게 하는지 살펴보도록 하겠습니다.
MDC는 ThreadLocal 기반이지만, 보통 Filter에서 세팅하고 finally에서 clear 하도록 구조를 만든다고 합니다.
무엇을 보고 logFramwork는 ThreadLocal에서 꺼내게 만드는가?
일단 우리가 알고가야 할질문은 ThreadLocal과 MDC의 관계라고 생각합니다. 사실 MCD == ThreadLocal입니다.
이말이 무슨말이냐하면 MDC를 사용한다는건 ThreadLocal을 사용이 된다는 의미입니다.
MDC 자체가 내부적으로 ThreadLocal<Map<String, String>> 로 구현되어 있다.
그러면 MDC는 어떻게 사용할 수 있는걸까요? 다행히 logFramwork를 사용하게 되면 그 안에 MDC기능이 내장되어있다고 합니다.
물론, log.info(xx)를 사용해도 자동으로 MDC가 사용이 되어지는 것은 아닙니다. 가장 빠르게 사용하는 방법은
MDC.put("aa","aaaa");
직접 사용하는 방법이 가장 빠르게 사용할 수 있습니다. 하지만 이 방법은 위에서도 말했듯이 해제 작업을 해줘야합니다. 그렇지 않으면 메모리 누수와 컨텍스트 오염을 일으킨다고 합니다.
다시 돌아와서 그래서 무엇을보고 logFramwork는 ThreadLocal에서 꺼내게 만드는 걸까요?
로그 프레임워크는 로그 패턴(PatternLayout)에 있는 %X{key} 지시자를 보고, MDC(ThreadLocal)에서 값을 꺼냅니다.
패턴는 logback.xml에 저장해서 사용이 되어집니다.
예시로는 다음과 같이 작성한다고 합니다.
<pattern>
[%d{yyyy-MM-dd HH:mm:ss.SSS}]
[%-5level]
[%thread]
[traceId=%X{traceId}]
%logger %msg%n
</pattern>
또는
<pattern>
%d [%thread] %-5level [%X{traceId}] %logger - %msg%n
</pattern>
요런 방식들을 통해 정의를 한다고 합니다. 여기서 드는 의문점이 있습니다. 저희는 일반적으로 로그를 작성할때 log.info(XXX)이렇게 작성하지 MDC.put("","")이렇게 작성하지는 않습니다. 어디서 부터 잘못된걸까요?
사실 MDC는 로그를 작성하기 위해 작성하는 부분은 아닙니다.
MDC는
이 스레드에서 찍히는 모든 로그에 공통으로 붙일 정보를 세팅하는 코드
그렇다면 직접 세팅해서 사용이 되어지는 느낌으로 이해가 되어집니다.
결국, 자동으로 생기는 게 아니라, 누군가(우리 코드나 라이브러리)가 먼저 세팅해줘야 동작하는 구조입니다.
사용법
사용하는 방법은 2가지가 있습니다.
1. 직접 세팅하는 방법
- Filter / Interceptor 에서 위 코드를 입력하고 finally 에서 clear하는 방식으로 설정할 수 있습니다.
@Component
public class MdcLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String traceId = UUID.randomUUID().toString();
try {
// 1️⃣ 요청 시작: MDC 세팅
MDC.put("traceId", traceId);
// 2️⃣ 다음 필터 / 컨트롤러로 진행
filterChain.doFilter(request, response);
} finally {
// 3️⃣ 요청 종료: 반드시 정리
MDC.clear(); // 또는 MDC.remove("traceId")
}
}
Interceptor
@Component
public class MdcLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
MDC.clear();
}
}
등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private MdcLoggingInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor);
}
}
왜 하필 Filter를 사용하는가?
Filter, Interceptor, AOP 모두 공통적인 로직을 자동으로 처리하기 위한 수단이라는 점에서는 비슷한 역할을 한다. 그럼에도 불구하고 MDC와 같은 요청 단위 컨텍스트 관리에는 Filter를 사용하는 것이 정석으로 여겨집니다.
Filter를 사용하는 결정적인 이유는, 가장 바깥 레이어에서 요청을 감싸기 때문입니다.
Filter는 DispatcherServlet 이전 단계에서 실행되고 컨트롤러에 도달하지 못하는 요청까지 포함하며
예외가 발생하더라도 finally 블록을 통해 항상 정리 로직이 실행되는 구조를 보장합니다.
반면, Interceptor나 AOP는 컨트롤러 또는 특정 메서드 호출 이후 단계에 의존하기 때문에 요청이 그 지점까지 도달하지 못하는 경우 적용되지 않거나 예외 흐름에 따라 정리 로직이 누락될 수 있는 위험이 존재한다.
즉, MDC처럼 요청 전체 생명주기와 정확히 동일한 범위로 관리되어야 하는 컨텍스트는, 가장 바깥에서 요청을 감싸는 Filter 계층에서 관리하는 것이 구조적으로 가장 안전합니다. 이 글은 Filter, Interceptor, AOP를 상세히 비교하는 것이 목적이 아니므로,
여기서는 MDC 같은 요청 컨텍스트는 Filter에서 관리하는 것이 가장 안전하다는 결론만 짚고 넘어가겠습니다.
2. 라이브러리를 사용하는 방법
- Sleuth / OTel 같은 걸 쓰는 경우
Sleuth나 OpenTelemetry같은 트레이싱 라이브러리를 사용하는 경우, MDC의 세팅과 해제는 직접 개발자가 직접 관리하지 않아도 됩니다. 이 라이브러리들은 내부적으로 서블릿 필터를 통해 요청 시작 시 traceId/spanId를 생성하고 MDC에 세팅한 뒤,
요청 종료 시 자동으로 정리합니다.
또한 비동기 스레드로 실행 흐름이 넘어가는 경우에도 컨텍스트 전파를 전해주기 때문에
개발자가 단순히 log.info()만 호출해도 트레이스 정보가 포함된 로그를 얻을 수 있습니다.
다만, 라이브러리가 관리하지 않는 커스텀 MDC값은 여전히 개발자가 직접 생명주기를 관리해야합니다.
트레이싱 라이브러리 추가와 logback 패턴에 %X{traceId} 두개만 넣으면 바로 사용할 수 있습니다.
대략적으로 이렇게 사용하면 MDC를 사용할 수 있습니다.
마무리
지금까지 MDC에 대해 알아봤습니다. MDC는 멀티스레드 환경에서 어느 스레드에서 어떤 로그가 발생했는지 추적할 수 있게 해주는 도구로 볼 수 있습니다. 다만, 동기적인 환경에서는 사용하기 용이한 반면, 내부적으로 ThreadLocal 기반으로 동작하기 때문에 비동기나 논블로킹 환경에서는 컨텍스트 전파가 어렵고 사용하기 까다롭다는 한계를 가지고 있습니다.
초기/중기 프로젝트에서는 매우 좋은 선택이지만,
시스템이 비동기/리액티브로 진화하면 다른 컨텍스트 전파 방식(OTel Context 등) 을 고려해야 합니다.
'개발' 카테고리의 다른 글
| 캐시를 이용하는 방법들 (0) | 2026.01.09 |
|---|---|
| Lint는 무엇인가? (1) | 2026.01.08 |
| WAL: 분산 시스템에서 쓰기 경로를 중앙화하는 방법 (0) | 2026.01.03 |
| 멀티 모듈 개발기(1) (feat.grale) (0) | 2026.01.02 |
| 스쿼시, 머지, 리베이스 (0) | 2025.12.31 |