MDC 넌 누구냐

겔로그·2024년 8월 16일
0

오늘은 Logback에서 빠질 수 없는 MDC라는 개념에 대해 알아보는 시간을 가져보았습니다.

MDC란?

MDC란, Mapped Diagnostic Context의 약어로 로그에 추가적인 컨텍스트 정보를 제공하는 기능입니다.

각 스레드마다 별도의 컨텍스트 정보를 유지하고 로그 메세지에 추가적인 정보를 포함시키는데 사용됩니다. 우선 MDC class의 코드를 확인해 보겠습니다.

MDC.java

public class MDC {
    static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
    private static final String MDC_APAPTER_CANNOT_BE_NULL_MESSAGE = "MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA";
    static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
    static MDCAdapter mdcAdapter;

    private MDC() {
    }

    public static void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        } else if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
        } else {
            mdcAdapter.put(key, val);
        }
    }

    public static String get(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        } else if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
        } else {
            return mdcAdapter.get(key);
        }
    }
... 이하 생략
}

MDC 코드를 볼 경우 가장 눈에띄는 부분은 다음과 같습니다.

  • 모든 변수 및 메소드가 static으로 구성되어 있습니다.
  • mdcAdapter을 통한 처리를 진행하는 것을 알 수 있습니다.
  • MDC의 mdcAdapter에 데이터를 넣고(put), 가져오고(get), 삭제할(remove/clear) 수 있습니다.
  • 데이터는 key value 로 관리됩니다.

좀 더 자세하게 알아보고자 mdcAdapter를 확인하겠습니다.

MDCAdapter.class

public interface MDCAdapter {
    void put(String var1, String var2);

    String get(String var1);

    void remove(String var1);

    void clear();

    Map<String, String> getCopyOfContextMap();

    void setContextMap(Map<String, String> var1);
}

MDCAdapter는 인터페이스였네요... 그럼 구현체를 확인해보도록 하겠습니다. logback 라이브러리에 구현된 구현체를 확인해보겠습니다.

LogbackMDCAdapter.java

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> readWriteThreadLocalMap = new ThreadLocal();
    final ThreadLocal<Map<String, String>> readOnlyThreadLocalMap = new ThreadLocal();
    private final ThreadLocalMapOfStacks threadLocalMapOfDeques = new ThreadLocalMapOfStacks();

    public LogbackMDCAdapter() {
    }

    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        } else {
            Map<String, String> current = (Map)this.readWriteThreadLocalMap.get();
            if (current == null) {
                current = new HashMap();
                this.readWriteThreadLocalMap.set(current);
            }

            ((Map)current).put(key, val);
            this.nullifyReadOnlyThreadLocalMap();
        }
    }

    public String get(String key) {
        Map<String, String> hashMap = (Map)this.readWriteThreadLocalMap.get();
        return hashMap != null && key != null ? (String)hashMap.get(key) : null;
    }

    public void remove(String key) {
        if (key != null) {
            Map<String, String> current = (Map)this.readWriteThreadLocalMap.get();
            if (current != null) {
                current.remove(key);
                this.nullifyReadOnlyThreadLocalMap();
            }

        }
    }
... 이하 생략
}

logback 구현체를 보니 ThreadLocal이라는 클래스를 사용해 데이터를 관리하고 있는 것을 확인하실 수 있습니다. 그럼 ThreadLocal이 뭐길래 MDC에서 사용되는 걸까요?

ThreadLocal

ThreadLocal 사용법과 활용의 일부 내용을 추가정리 및 요약하였습니다.

ThreadLocal은 자바 1.2 버전부터 제공된 클래스입니다. 쓰레드 단위로 로컬 변수를 할당하는 기능을 제공하고 있으며 변수를 쓰레드 영역에 설정함으로서 특정 쓰레드가 실행하는 모든 코드에서 설정된 변수 값을 사용할 수 있는 특징이 있습니다.

아래 예시를 확인해보겠습니다.

public class Context {
    public static ThreadLocal<Date> local = new ThreadLocal<Date>();
}

class A {
    public void a() {
        Context.local.set(new Date());
       
        B b = new B();
        b.b();

        Context.local.remove();
    }
}

class B {
    public void b() {
        Date date = Context.local.get();

        C c = new C();
        c.c();
    }
}

class C {
    public void c() {
        Date date = Context.local.get();
    }
}

public static void main(String[] args) {
	A a = new A();
    a.a();
}

main 메소드 호출시 A클래스에서 Date를 생성한 정보를 C 클래스에서 가져올 수 있음을 알 수 있습니다.

위 그림을 보실 경우 좀 더 이해가 잘 될텐데요. 같은 쓰레드에서 동작하는 로직이기 때문에 메소드로 인자를 전달하지 않더라도 공용으로 사용할 수 있음을 알 수 있습니다.

MDC에서는 ThreadLocal의 동일 쓰레드에서 정보를 공유할 수 있는 특징을 활용하여 한 쓰레드에서 추가적인 정보를 저장 및 공유하여 다양한 정보를 로깅할 수 있도록 도움을 줍니다.

MDC 활용법

MDC를 통해 관리할 수 있는 정보는 다양합니다.

sprign boot3에서는 micrometer로 통일된 Spring Cloud Sleuth가 대표적인 예시일텐데요. 로깅 정보에 traceId를 넣어 분산 시스템에서 트래픽의 처리 흐름을 시각적으로 확인할 수 있도록 도움을 줬습니다.

내가 활용한 방법

현재 제가 진행중인 신규 프로젝트에서는 Header를 통한 API 인증 처리를 수행하고 있습니다.

X-APPKEY: 인증 키
X-Uid: 사용자 정보

인증 키와 사용자 정보를 통해 해당 API를 사용할 수 있는 권한이 있는지를 확인하고 처리하는데요. 이 정보를 MDC에 저장해 로그로 남겨보았습니다.

API 요청은 HttpServletRequest으로 들어오기 때문에 Filter를 구현하여 인증 키와 사용자 정보를 MDC 정보에 넣었습니다.


private const val X_UID = "X-Uid"
private const val X_APPKEY = "X-APPKEY"

@Component
class VerificationHeaderLoggingFilter : Filter {
    override fun doFilter(
        request: ServletRequest,
        response: ServletResponse,
        chain: FilterChain,
    ) {
        try {
            val httpRequest = request as HttpServletRequest
            MDC.put(X_APPKEY, httpRequest.getHeader(X_APPKEY) ?: "")
            MDC.put(X_UID, httpRequest.getHeader(X_UID) ?: "")

            chain.doFilter(request, response)
        } finally {
            MDC.clear()
        }
    }
}

이 후, 해당 정보를 로깅하기 위해 아래와 같이 logback 설정을 수정하였습니다.

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{dd HH:mm:ss.SSS} %level %X{traceId:-} %X{spanId:-} %thread %logger{30}.%M\(%line\) - %X{X-APP-KEY} %X{X-uid} %msg%n</pattern>
        </layout>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="console"/>
    </root>

추가로, 로그 이벤트를 prometheus와 같은 모니터링 툴로 전송할 경우 Appender를 활용하여 해당 인증 정보를 포함한 정보를 전송할 수 있습니다.

주의사항

logback과 Spring Cloud Sleuth 에서는 MDC를 사용할 때 다음과 같은 주의사항을 사용자들에게 공유합니다.

Remember that adding entries to MDC can drastically decrease the performance of your application!

Otherwise, the MDC will contain stale values for certain keys. We would recommend that whenever possible, remove() operations be performed within finally blocks, ensuring their invocation regardless of the execution path of the code.

MDC는 ThreadLocal을 사용하고 있고 일반적으로 thread는 리소스를 아끼기 위해 thread pool을 생성하여 관리합니다.

이 과정에서 MDC의 컨텍스트 정보를 초기화하지 않고 사용한다면, 예상치 못한 문제가 발생할 수 있습니다.

메모리가 부족하거나 컨텍스트 증가로 인한 성능 저하, 예상치 못한 데이터 노출 등이 있을텐데 꼭 사용을 마친 이후 자원을 해제하시고, 너무 많은 데이터를 MDC에서 관리하려고 하지 않는것을 권장드립니다.

결론

MDC는 동일 쓰레드에서 공통적으로 사용되는 데이터를 관리하기에 적합한 개념입니다. 다만, 과한 사용은 성능 문제를 발생시킬 수 있으니 적당한 수준의 사용을 권장하며 최종적으로는 리소스 해제를 하여 불필요한 메모리 소모를 최소화하는 것을 권장합니다.

감사합니다.

profile
Gelog 나쁜 것만 드려요~

0개의 댓글