Cute Light Pink Flying Butterfly MDC를 이용한 로그트레이싱 | Slf4j MDC(Mapped Diagnostic Context) :: 놀면서 돈벌기
본문 바로가기
IT/Backend | All

MDC를 이용한 로그트레이싱 | Slf4j MDC(Mapped Diagnostic Context)

by esclife_ 2025. 11. 7.
반응형

 

1. 🤯 파라미터 헬

개발 과정에서 여러 서비스 파일이나 메서드를 넘나들며 로그 추적을 위해 필요한 공통 데이터(예: 스레드 이름, 요청 ID, 사용자 정보 등)를 계속해서 메서드의 파라미터로 전달해야 하는 상황을 겪게 됩니다. 이러한 상황을 흔히 파라미터 헬 이라고 부릅니다.

파라미터 헬과 MDC 기능을 사용하게 된 계기를 설명해볼께요.

 

  • 문제 상황 (파라미터 헬):
    • serviceA(ThreadName, ID, logObject, data1, data2) ➡️ serviceA에서 serviceB 호출
    • serviceB(ThreadName, ID, logObject, data3) ➡️ serviceB에서 serviceC 호출
    • serviceC(ThreadName, ID, logObject, data4)
    • 비즈니스 로직과 관련 없는 로그/추적용 파라미터가 모든 메서드 시그니처를 오염시켜 코드의 가독성을 떨어뜨리고, 로직이 깊어질수록 파라미터 목록이 길어져 유지보수가 극도로 어려워지는 결과를 초래합니다.
  • MDC 사용 계기:
    • 서비스에서 다른 서비스를 호출할 때, 전체적인 흐름을 로깅하기 위해서 하나의 스레드당 붙는 트랜잭션ID(API 호출 1건에 요청ID값), 스레드명을 로그마다 기록해야 했습니다. 그래서 첫 호출 서비스에 스레드명, 트랜잭션ID를 선언했으나 다른 서비스 클래스에서도 사용하려면 값을 넘겨줘야 했기때문에 초기에는 파라미터를 이용해서 전달했었어요 -!
    • 스레드 정보나 ID 값, 로그 객체와 같이 특정 요청의 맥락(Context)을 나타내는 데이터는 비즈니스 로직과는 별개로 해당 요청이 처리되는 스레드내에서만 공유되면 됩니다.
    • SLF4J MDC(Mapped Diagnostic Context)는 이러한 컨텍스트 정보를 스레드 단위로 안전하게 저장 및 조회할 수 있도록 해주어, 파라미터로 전달할 필요 없이 로깅 시점에 로그 패턴에 자동으로 포함시키는 해결책을 제시합니다.

2. 🔍 로그 트레이싱(Log Tracing)이란?

로그 트레이싱 또는 로그 상관관계 분석(Log Correlation)은 시스템에 들어온 단일 요청(Request)의 시작부터 끝까지, 그리고 해당 요청이 여러 모듈이나 마이크로서비스를 거쳐 처리되는 모든 과정에서 발생하는 로그들을 하나의 흐름으로 연결하여 추적하는 것을 의미합니다.

  • 필요성: 멀티스레드 환경이나 마이크로서비스 아키텍처에서는 여러 요청이 동시에 처리되면서 로그가 뒤섞여 나오기 때문에, 특정 요청에 대한 문제 발생 시 관련 로그를 한눈에 파악하기 어렵습니다.
  • 핵심: 각 요청에 고유한 트랜잭션 ID 또는 상관관계 ID (Correlation ID/Trace ID)를 부여하고, 해당 요청이 처리되는 모든 로그 라인에 이 ID를 함께 기록하는 것이 중요합니다.

3. 💡 MDC (Mapped Diagnostic Context) 란?

MDC는 SLF4J(Simple Logging Facade for Java)에서 제공하는 기능으로, 로그 메시지에 부가적인 컨텍스트 정보를 효율적으로 추가하기 위한 메커니즘입니다.

MDC는 현재 스레드에 고유한 데이터를 저장하여 멀티스레드 환경에서 로그의 맥락을 추적하고 분류하는 기능으로서, 이를 통해 요청이나 작업별로 로그를 식별할 수 있어 로그 분석 및 문제 해결이 용이해집니다.

주로 org.slf4j.MDC 클래스를 통해 사용하며, ThreadLocal을 사용하여 각 스레드별로 독립적인 Map을 관리합니다. 

 

 

  • 기능 및 원리:
    • MDC는 내부적으로 java.lang.ThreadLocal을 사용하여 Key-Value 형태의 맵(Map)을 현재 실행 중인 스레드와 연결하여 관리합니다.
    • 특정 스레드에서 MDC.put(key, value)로 값을 저장하면, 해당 스레드 내에서 발생하는 모든 로그 이벤트는 그 값을 사용할 수 있게 됩니다.
    • 다른 스레드는 이 값에 접근할 수 없으므로, 멀티스레드 환경에서 안전하게 컨텍스트 정보를 공유할 수 있습니다.
  • 장점:
    • 파라미터 헬 해결: 비즈니스 메서드 시그니처를 오염시키지 않고 요청 컨텍스트 정보를 공유할 수 있습니다.
    • 로그 가독성 향상: 로그 설정(Log Pattern)만 변경하여 트랜잭션 ID, 사용자 ID 등 중요한 컨텍스트 정보를 모든 로그 라인에 자동으로 포함시킬 수 있어 로그 트레이싱이 용이해집니다.

📝 참고: MDC는 스레드 로컬(ThreadLocal)을 사용하므로, 스레드 풀을 사용하는 Spring Boot 환경에서 요청 처리가 완료된 후 반드시 MDC.clear() 또는 MDC.remove(key)를 호출하여 값을 제거해 주어야 합니다. 그렇지 않으면 해당 스레드가 재사용될 때 이전 요청의 MDC 값이 남아있는 문제가 발생합니다.

예를 들면, try-catch 문 안에서 서비스로직 종료 시 finally문을 추가하여 마지막에 MDC.remove(key값);으로 제거하는 작업을 필수적으로 수행해주어야 합니다.

 

🛠️ MDC 사용법

Spring Boot + Logback 기준으로 MDC를 사용하는 방법을 예시로 보여드릴께요.

 

단계 1: MDC에 컨텍스트 정보 저장 (Filter 또는 Interceptor 활용)

HTTP 요청이 들어오는 가장 첫 단계인 Filter나 HandlerInterceptor에서 MDC에 트레이싱에 필요한 정보를 저장합니다. Filter를 사용하는 것이 일반적으로 권장됩니다.

import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

@Component
@Order(1) // 가장 먼저 실행되도록 우선순위 부여
public class MdcTracingFilter implements Filter {

    // Trace ID에 사용할 키
    private static final String TRACE_ID = "traceId";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        // 1. 요청 시작 시 MDC에 고유 ID(Trace ID) 저장
        // 실제로는 HTTP 헤더에서 Trace ID를 추출하거나, 없으면 새로 생성하여 사용합니다.
        MDC.put(TRACE_ID, UUID.randomUUID().toString().substring(0, 8));

        try {
            // 2. 다음 필터/핸들러 체인 실행 (비즈니스 로직 수행)
            chain.doFilter(request, response);
        } finally {
            // 3. 요청 처리 완료 후 MDC 정리 (매우 중요!)
            // 스레드 풀 재사용 시 이전 요청의 정보가 남는 것을 방지
            MDC.remove(TRACE_ID);
        }
    }
}

 

 

단계 2: 로그 패턴에 MDC 값 포함시키기 (Logback 설정)

src/main/resources/logback-spring.xml 또는 logback.xml 파일에서 로그 패턴을 수정하여 MDC에 저장된 값을 출력하도록 설정합니다.

traceId 로 호출하고, -NoTraceId 부분은 MDC의 키 이름이 아니라, 키에 대한 기본값(Default Value)을 지정하는 문법입니다.

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] **[%X{traceId:-NoTraceId}]** %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

 

단계 3: 로그 사용

이제 서비스 코드 내에서는 파라미터로 ID를 넘길 필요 없이 SLF4J Logger를 평소대로 사용하면 됩니다.

// MemberService.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// MDC.get()은 디버깅 목적으로만 사용하고, 로그에는 패턴을 통해 자동 포함됩니다.
// import org.slf4j.MDC; 

@Service
public class MemberService {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    public void registerMember() {
        String memberName = "동동이";
    
        // 이 로그 메시지에는 자동으로 Trace ID가 포함됩니다.
        log.info("회원 등록 요청 시작: {}", memberName); 
        
        // 비즈니스 로직...
        // ...

        log.info("회원 등록 로직 완료");
    }
}

 

혹은 단계 2에서 처럼 직접 로거내에 포함시키지 않고, 특정 서비스로그에만 요소를 쓰고싶다면 아래와같이 MDC.get으로 사용해도 된답니다.

// MemberService.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC; 

@Service
public class MemberService {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    public void registerMember() {
        String memberName = "동동이";
    
        // 이 로그 메시지에는 자동으로 Trace ID가 포함됩니다.
        log.info("TRACE-ID {}, 회원 등록 요청 시작: {}", MDC.get("traceId"), memberName); 
        
        // 비즈니스 로직...
        // ...

        log.info("회원 등록 로직 완료");
    }
}

 

✅ 결과 (콘솔 로그 예시)

2025-11-07 10:00:00.123 [http-nio-8080-exec-1] **[a3b5c7d9]** INFO  c.e.s.MdcTracingFilter - Filter 시작
2025-11-07 10:00:00.125 [http-nio-8080-exec-1] **[a3b5c7d9]** INFO  c.e.s.MemberService - 회원 등록 요청 시작: TestUser
2025-11-07 10:00:00.350 [http-nio-8080-exec-1] **[a3b5c7d9]** INFO  c.e.s.MemberService - 회원 등록 로직 완료

 


 

아래는 제가 파라미터로 넘겨서 작성하던 스레드명, 요청ID를 MDC를 이용하여 넘겨 사용한 결과인데요.

[2025-11-07 10:32:21,119][2518988][push-10] INFO  n.h.s.p.commons.logger.CallLogger - [FCM PUSH][308923e0-2d49-4721-8181-2bb37a606de3] 요청 시작 - Thread: push-10, ID: 162, Time: 1762479141119

 

이전에는 아주 지저분하게 메서드 호출 파라미터란에 4~5개씩 담아넘기던 값을 MDC로 전환해서 아주 깔끔해진 결과입니다.

코드를 짤 때는 기능도 기능이지만, 가독성 높게 객체지향적 설계를 해야하는 것 명심하기..!

반응형