Spring

WebFlux에서 MDC 사용하기 – SpringBoot 3 버전

긍.응.성 2024. 6. 16. 14:27
반응형

지난 글에서는 MDC를 데이터를 리액티브 환경에서 사용할때 생기는 이슈와 해결할 수 있는 방법에 대하여 알아보았습니다.

https://ckddn9496.tistory.com/179

Reactor With MDC - SpringBoot 2

그리고 Reactor도 이러한 이슈가 있다는것을 알고 있었는지, Reactor 3.5 버전부터 자동으로 ThreadLocal의 값을 Context를 통하여 전파할 수 있도록 Automatic Context Propagation을 제공하게 되었습니다.

이번 글에서는 SpringBoot 3 버전 WebFlux에서 Automatic Context Propagation를 통해 MDC를 다루는 법에 대하여 알아보겠습니다.

1. Reactor With MDC (springboot 3)

Spring Boot 3 버전부터 Reactor 3.5.0 버전이 적용됩니다.

Reactor 3.5 버전 이후로부터 자동으로 Context를 전파하기 위해 먼저 main 함수에 아래의 훅을 추가합니다.

@SpringBootApplication
public class Boot3Application {

	public static void main(String[] args) {
		SpringApplication.run(Boot3Application.class, args);
		Hooks.enableAutomaticContextPropagation(); // add hook
	}
}

enableAutomaticContextPropagation 메서드를 확인해보면 해당 메서드는 ThreadLocal에 대해 전역적인 자동 Context 전파가 가능하도록 함을 알 수 있습니다. 하지만 추가적인 조건이 있는데요, 이를 사용하기 위해서는 context-propagation 라이브러리가 클래스 패스에 존재해야합니다.

Reactor 3.5.0 부터 Reactor Context는 micrometer에서 만든 context-propagation 라이브러리와 통합하여 Context를 전파할 수 있는 여러 방법을 지원받습니다. context-propagation 의존성을 프로젝트에 추가합니다.

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>context-propagation</artifactId>
    <version>1.0.4</version>
</dependency>

1.1. context-propagation 라이브러리

이 라이브러리는 ThreadLocal, Reactor Context 등에 대하여 context(데이터) 전파를 위한 기능을 제공합니다.

라이브러리에서 가장 중요한 클래스는 ContextRegistry, ThreadLocalAccessor, ContextAccessor, ContextSnapshot 입니다.

  • ThreadLocalAccessor - ThreadLocal 값에 대한 처리를 위한 클래스
  • ContextAccessor - Map 형태의 context 데이터에 대한 처리를 위한 클래스
  • ContextRegistry -  ThreadLocalAccessor , ContextAccessor 를 저장하는 레지스트리
  • ContextSnapshot - Context 전파를 및 스냅샷 캡처 등을 지원하기 위한 클래스

현재 ThreadLocal 기반으로 동작하는 MDC 데이터에 대한 처리가 필요한 상황이기에 ThreadLocalAccessor 를 구현합니다. 그리고 ContextRegistry에 등록합니다.

ThreadLocalAccessor

public interface ThreadLocalAccessor<V> {
	Object key();

	V getValue();

	void setValue(V value);

	default void setValue() {
		reset();
	}
	...
}

ThreadLocalAccessor는 ThreadLocal 값에 대하여 어떻게 Context에 따라 전파할지를 명시한 인터페이스입니다.

  • key() : ContextSnapshot 내에서 ThreadLocal에 관리를 위한 키값을 반환하는 메서드
  • getValue() : ThreadLocal 값을 가져올 때 사용할 메서드
  • setValue(V value) : ThreadLocal 값을 세팅할 때 사용할 메서드
  • setValue() : ThreadLocal 값을 초기화 할 때 사용할 메서드

MdcThreadLocalAccessor

public class MdcThreadLocalAccessor implements ThreadLocalAccessor<Map<String, String>> {

	private static final String MDC_KEY = "_MDC_KEY_";

	@Override
	public Object key() {
		return MDC_KEY;
	}

	@Override
	public Map<String, String> getValue() {
		return MDC.getCopyOfContextMap();
	}

	@Override
	public void setValue(Map<String, String> value) {
		MDC.setContextMap(value);
	}

	@Override
	public void setValue() {
		MDC.clear();
	}
}

이제 MDC 데이터를 위한 ThreadLocalAccessor를 만들어봅시다. 먼저 MDC_KEY를 선언하고 ContextSnapshot에서 관리될 MDC 데이터의 키로 등록합니다.

MDC 값을 가져오기 위한 getValue()는 MDC.getCopyOfContextMap() 로, MDC를 세팅하기 위한 setValue(Map<String, String> value) 메서드는 MDC.setContextMap(value) 로, MDC 값 초기화를 위한 setValue() 메서드엔 MDC.clear() 로 구현합니다.

ContextRegistry에 등록

ContextRegistry.getInstance().registerThreadLocalAccessor(new MdcThreadLocalAccessor())

이제 생성했던 MdcThreadLocalAccessor를 ContextRegistry에 등록합니다.
ContextRegistry는 static한 싱글턴 객체로 관리되며 getInstance 메서드를 통해 가져올 수 있습니다. registerThreadLocalAccessor를 통해 MdcThreadLocalAccessor를 등록합니다.

ThreadLocalAccessor 를 구현하고 인스턴스를 파라미터로 넘기는것 보다 더 간단한 방법이 있습니다. registerThreadLocalAccssor에는 여러 getter, setter, resetter 를 위한 함수형 인터페이스를 파라미터로 받는 메서드도 제공합니다. 이를 이용하면 아래와 같이 간단히 MDC를 위한 ThreadLocalAccessor를 등록할 수 있습니다.

ContextRegistry.getInstance().registerThreadLocalAccessor(
		MDC_KEY,
		MDC::getCopyOfContextMap,
		MDC::setContextMap,
		MDC::clear);

ContextWrite

이제 MDC 데이터를 초기 세팅하는 부분을 살펴봅시다. 앞선 글에서 WebFilter를 등록하여 사용자 요청에 대한 유니크한 식별자를 Context에 등록하였습니다. 이젠 context-propagataion 라이브러리에 따라 Context의 값을 MDC로 자동으로 옮겨주게 될텐데요, 조건으로는 ContextRegistry에 등록했던 키값에 맞추어 주어야 Conext가 전파됩니다.

전체 코드입니다.

@Component
public class MdcLoggingFilter implements WebFilter {

    private static final String MDC_KEY = "_MDC_KEY_";

    @PostConstruct
    public void setUp() {
    	ContextRegistry.getInstance().registerThreadLocalAccessor(
            MDC_KEY,
            MDC::getCopyOfContextMap,
    	    MDC::setContextMap,
            MDC::clear);
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange)
	    .contextWrite(Context.of(MDC_KEY, Map.of("requestId", genRequestId())));
    }
}

2. Automatic Context Propagation in SpringBoot 2

만약 Spring Boot 2 버전을 사용하고 있는데, 위와 같은 automatic context propagation를 사용하고 싶다면 Reactor 버전 3.5.x 로 올려서 적용해볼 수 있습니다.

설정 시 Hooks.enableAutomaticContextPropagation 메서드 사용을 위해 reactor-core 버전을 3.5.3 이상으로 업그레이드 하고 context-propagation 라이브러리를 추가합니다.

<!-- tested on boot 2.7.14 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.5.3</version>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>context-propagation</artifactId>
    <version>1.0.4</version>
</dependency>

그리고 main 함수에 훅 설정을 추가 후 MdcLoggingFilter를 등록합니다. 이때 추가로 작업할 것은 reset 메서드가 구현된 ThreadLocalAccessor를 등록하여야 합니다.

public class MdcThreadLocalAccessor implements ThreadLocalAccessor<Map<String, String>> {

    // ...	

    @Override
    public void reset() {
        MDC.clear();
    }
}

reactor-core 버전에 따라 ThreadLocalAccessor에서 스레드 초기화 시 reset 메서드를 호출할 수 있습니다. reset 메서드는 deprecated 예정이며 이를 대신해서 빈 파라미터로 넘어오는 setValue() 호출해야하는데요, 낮은 버전의 reactor-core는 여전히 setValue() 대신 reset() 을 사용하기에 이를 위해 구현해주어야 합니다.

위의 방법 대로 설정을 완료하였다면 Spring Boot 2버전에서도 automatic context propagation이 동작하는것을 확인할 수 있습니다.

위 버전으로도 automatic context propagation을 적용 시 MDC가 정상적으로 남는것을 확인하였지만
Spring boot 2버전대에서의 호환되는 reactor-core 버전은 3.4.31 버전입니다.
실무에 적용 시 이 부분은 참고하시길 바랍니다.

3. 정리

Reactor 3.5.0 부터 context-propagation 라이브러리를 통해 Reactor Context처럼 ThreadLocal 값의 전파를 지원하게 되었습니다. 이를 위해 간단히 자동 컨텍스트 전파를 위한 훅을 등록하고 ContextRegistry에 필요한 종류의 Accessor를 등록하는것으로 MDC 데이터를 포함하여 타 ThreadLocal 값도 관리되도록 설정할 수 있습니다.

4. 참고 자료

반응형