WebFlux에서 MDC 사용하기 – SpringBoot 3 버전
지난 글에서는 MDC를 데이터를 리액티브 환경에서 사용할때 생기는 이슈와 해결할 수 있는 방법에 대하여 알아보았습니다.
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 값도 관리되도록 설정할 수 있습니다.