본문 바로가기
Java/Spring

ThreadLocal을 사용하여 로깅하기 + MDCFilter

by oneny 2023. 10. 8.

ThreadLocal

ThreadLocal은 Java에서 멀티스레드 환경에서 스레드 간에 데이터를 공유하지 않고 각 스레드마다 독립적으로 데이터를 보관하고 접근할 수 있게 해주는 클래스이다. 이는 하나의 스레드가 힙 메모리에서 참조하는 객체는 다른 여러 스레드에서도 참조할 수 있기 때문에 멀티스레드 환경에서 컨텍스트 스위칭이 일어남에 따라 데이터를 공유하여 발생하는 문제를 해결할 수 있다.

 

ThreadLocal의 특징

  • 스레드 간 데이터 분리: ThreadLocal을 사용하면 각 스레드에서 독립적으로 데이터를 관리할 수 있다. 즉, 한 스레드에서 설정한 데이터는 다른 스레드에 영향을 미치지 않는다.
  • 초기화 및 접근: ThreadLocal 변수는 초기화될 때 기본값이나 초기값으로 설정된다. 각 스레드는 이 변수에 대한 별도의 복사본을 가지고 있으며, 해당 변수에 데이터를 설정하고 가져오는 메서드를 제공한다.
  • 스레드 안전성: ThreadLocal을 사용하면 각 스레드마다 별도의 내부 저장소를 제공하여 관리하기 때문에 스레드 간의 동기화나 경쟁 상태를 처리해야 하는 문제를 해결할 수 있다.

 

ThreadLocal 메서드

  • 값 저장: ThreadLocal.set()
  • 값 조회: ThreadLocal.get()
  • 값 제거: ThreadLocal.remove()
    • 해당 스레드가 ThreadLocal을 모두 사용하고 나면 해당 메서드를 호출해서 스레드 로컬에 저장된 값을 제거해줘야 한다. 만약 제거하지 않고 그냥 두면 스레드풀을 사용하는 경우 심각한 문제가 발생할 수 있다.

 

ThreadLocal을 사용하지 않은 로깅의 문제점

FieldLogTrace는 요청이 들어오면 Controller -> Service -> Repository -> Service -> Controller 순으로 로깅을 출력하는 클래스이다. 하지만 한 번에 3개의 요청이 들어왔을때 로깅이 출력되는 것을 확인하면 여러 스레드가 동일한 현재 스레드의 로깅 식별자인 TraceId를 공유함으로써 동시성 이슈가 있는 것으로 보인다. 이를 해결하기 위해서는 어떻게 해야 할까? 위에서 봤듯이 ThreadLocal을 사용하면 된다. ThreadLocal을 사용하면 스레드마다 독립적으로 데이터를 관리할 수 있어 동시성 이슈를 해결할 수 있다.

 

ThreadLocal 적용

ThreadLocal을 사용함으로써 각 스레드마다 각각의 TraceId가 만들어지고 로깅식별자가 스레드마다 다른 것을 결과를 통해 확인할 수 있다.

 

MDCFilter(Mapped Dianostic Context)

MDCFilter는 Java 웹 애플리케이션에서 로깅과 관련된 작업을 더욱 편리하게 수행하기 위한 필터(Filter)의 한 유형이다. MDC는 내부적으로 Map 자료구조를 통해 현재 실행중인 스레드에 메타정보를 넣고 관리하는 공간이다. 메타 정보를 스레드 별로 관리하기 위해 내부적으로 ThreadLocal을 사용하고 있다.

위에서 로그마다 식별자가 찍히는 것을 확인할 수 있는데 이는 멀티 스레드 환경에서 여러 동시에 요청이 처리되기 때문에 동일한 요청에 대한 로그가 연속적으로 쌓이는 것이 아니라 순서없이 쌓이기 때문에 어떤 스레드에 대한 로그인지 정상적으로 파악하기 힘들다. 따라서 이를 위해 요청별로 식별자를 부여하고, 로그에 함께 출력하여 요청에 대해 추적하는데 편리성을 줄 수 있다.

 

MDCFilter 적용

OncePerRequestFilter는 Spring에서 제공하는 필터 중 하나로, 각 HTTP 요청당 한 번만 실행되도록 보장하는 역할을 한다. 그리고 해당 필터는 MDC(Mapped Diagnostic Context)를 사용하여 각 HTTP 요청에 대한 고유한 요청 ID를 생성하고 로그에 포함시키는 역할을 한다.

  • @Component: Spring이 컴포넌트 스캔을 통해 스프링 빈으로 등록한다.
  • @Order(Ordered.HIGHEST_PRECEDENCE): 필터의 우선순위를 지정한다. 해당 옵션은 가장 높은 우선순위로 설정되어 다른 필터보다 먼저 실행되도록 보장한다.
  • doFilterInternal(): OncePerRequestFilter를 상속하여 해당 메서드가 각 HTTP 요청당 한 번만 실행되도록 보장한다.
  • MDC.put(): MDC는 Map 자료구조를 통해 (key, value) 형태로 메타정보를 관리하고 있다. request_id라는 키로 uuid.toString()을 값으로 설정하여 저장한다. 이를 통해 로그 메시지에 요청 ID를 포함시킬 수 있다
  • filterChain.doFilter(request, response): 요청을 다음 필터 또는 서블릿으로 전달한다. 이렇게 하면 다음 단계에서 요청을 처리할 수 있다.
  • MDC.clear(): 처리를 마친 후 MDC를 지워 요청 간에 MDC 컨텍스트가 유출되지 않도록 해야 한다.

 

logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%X{request_id:-startup}] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

위 필터만 추가한다고 요청 ID가 나오는 것이 아니다. resources/logback.xml을 만들고 위처럼 로그 패턴을 정의해줘야 한다.

 

결과

앞에 요청 ID를 같이 출력하도록 만들면 여러 스레드가 많이 쌓여도 추적하는데 편리성을 줄 수 있다,.

 

 

 

출처

스프링 핵심 원리 - 고급편

[Spring] 멀티쓰레드 환경에서 MDC를 사용해 요청 별로 식별 가능한 로그 남기기

 

 

 

 

 

 

 

 

'Java > Spring' 카테고리의 다른 글

프록시 팩토리를 통한 AOP  (1) 2023.10.16
Auto Configuration  (0) 2023.10.09
스프링 예외 추상화  (0) 2023.10.07
스프링 트랜잭션  (1) 2023.10.05
커넥션풀과 데이터소스 이해하기 with HikariCP  (1) 2023.10.03