비동기 환경에서 MDC & SecurityContext 📃

Lzhtk·2025년 8월 16일

오늘은 비동기 환경에서 MDC와 Spring SecurityContext같은 컨텍스트 정보를 전파해야 하는 경우 어떻게 처리해야하는지 알아보자 ❗


1 . 📃 컨텍스트 정보

  • 운영( Operational ) 문맥 : 로그 상관관계를 위한 traceId, spanId, 요청 경로, 테넌, ID 등 -> MDC가 대표한다.
  • 보안 문맥 : 현재 사용자, 권한, 인증 수단 등. -> SecurityContext가 대표한다.
  • 공통점으로는 애플리케이션 코드가 명시적으로 인수로 넘기지 않아도 접근 가능한 ambient상태로 다뤄지는 경우가 많다.

2 . 🤷‍♀️ 왜 전파가 필요할까? - 물리적 실행 맥락 vs 논리 맥락

  • ThreadLocal 기반 컨텍스트는 "현재 OS 스레드"에 묶인다.
  • 비동기/리액티브에서는 요청 처리 중 스레드가 바뀌는 것이 정상이다.(스레드풀 재사용, 스케줄러 호핑)
  • 그래서 논리적 요청의 흐름은 이어지지만, 실행 스레드가 바뀌면 ThreadLocal 컨텍스트가 끊어진다.
  • 그렇기에 비동기 경계(작업 제출/콜백등)마다 논리 맥락을 다시 연결하는 메커니즘이 필요하다.

3 . 📡 컨텍스트 전파의 4가지 모델

1️⃣ Ambient(ThreadLocal) 모델

  • 장점 : 기존 동기 코드와 친화적이며 접근이 단순하다.
  • 단점 : 스레드가 바뀌면 단절되며 비동기/리액티브에선 별도의 연결장치가 필요하다.

2️⃣ 명시적 전달 모델

  • 컨텍스트 객체를 함수인자나 메시지 헤더로 직접 전달
  • 장점 : 투명/예측 가능, 리액티브/분산 경계에서도 안전.
  • 단점 : 함수 시그니처 오염, 적용 비용.

3️⃣ 데코레이션/래핑 모델

  • 작업 제출 시 현재 컨텍스트를 캡처하고, 실행 시 복원한 뒤 정리하는 공통 장치
    ( ex : Task 데코레이터, 실행기 래퍼 )
  • 장점 : 애플리케이션 코드 변경을 최소화
  • 단점 : 제출 지점을 반드시 통과해야한다는 전제가 필요.

4️⃣ 리액티브 컨텍스트 모델

  • 스레드가 아닌 신호의 흐름에 컨텍스트를 담는다.
  • 장점 : 스레드 호핑에도 안전, 흐름 단위로 일관성 유지
  • 단점 : 한계 ThreadLocal 기반 라이브러리와는 브리지가 필요

4 . 🏷️ 전파 시점의 분류

  • 작업 제출 경계 : 스레드풀에 Runnable/Callable 제출, @Async 호출, CompletableFuture 체이닝 시작 등.
    -> 여기서 캡처를 진행하여야 한다.
  • 콜백/후속 실행 경계 : thenApply/handle, 스케줄러 이동, 타임아웃 콜백 등.
    -> 여기서 복원 후 실행 -> 즉시 정리가 필요하다.
  • 리액티브 구독 경계 : 구독 시점, 신호 처리등
    -> 흐름 컨텍스트를 Reactor Context에 싣고, 로그 직전 MDC에 순간 반영했다가 해제.

5 . 🔄 처리 흐름

비동기 환경에서는 컨텍스트 전파는 결국 다음 3단계로 요약할 수 있다.

1️⃣ 캡처

  • 작업 제출 시점에 현재 스레드의 MDC와 SecurityContext를 스냅샷으로 보관한다.

2️⃣ 복원

  • 새로운 스레드에서 작업을 실행하기 전에 해당 스냅샷을 복원한다.
  • 이 상태에서 로그를 찍으면 traceId, userId가 정상적으로 기록된다.

3️⃣ 정리

  • 실행이 끝나면 반드시 컨텍스트를 비우거나 원래 상태로 되돌려, 다른 요청과 섞이지 않도록 한다.

-> 리액티브 환경에서는 이 과정을 Reactor Context가 대신 담당하며, 로그 출력 시점에만 Reactor Context -> MDC로 브리지를 걸어준다.


6 . Spring @Async 환경에서의 컨텍스트 전파 ( 실무 예제 )

Spring에서 @Async를 사용하면, 해당 메서드는 별도의 스레드풀에서 실행된다.
이때 MDC나 SecurityContext와 같은 ThreadLocal 기반 컨텍스트는 자동으로 전파되지 않는다.
즉, 요청 스레드에서 설정한 다음 정보들이 비동기 메서드에서는 사라진다.

  • MDC의 traceId / userId
  • SecurityContextHolder의 Authentication
    이를 해결하기 위해 Spring은 비동기 실행 경계에서 컨텍스트를 연결할 수 있도록 TaskDecorator 인터페이스를 제공한다.

6 - 1 . TaskDecorator의 역할

TaskDecorator는 비동기 작업을 실행하기 전에

1️⃣ 현재 스레드의 컨텍스트를 캡처
2️⃣ 새 스레드에서 실행 직전 복원
3️⃣ 작업 종료 후 반드시 정리(clean-up)
하는 역할을 한다.

이는 앞에서 설명한 캡처 -> 복원 -> 정리 패턴을 그대로 구현한 것이다.

6 - 2 . MDC 전파용 TaskDecorator 구현 예제

public class MdcTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String, String> contextMap = MDC.getCopyOfContextMap(); // 캡처

        return () -> {
            try {
                if (contextMap != null) {
                    MDC.setContextMap(contextMap); // 복원
                }
                runnable.run();
            } finally {
                MDC.clear(); // 정리
            }
        };
    }
}
  • getCopyOfContextMap() : 현재 스레드의 MDC 스냅샷
  • setContextMap() : 새 스레드에 MDC 주입
  • clear() : 스레드풀 재사용으로 인한 오염 방지

6 - 3 . ThreadPoolTaskExecutor에 적용

@Bean
public ThreadPoolTaskExecutor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(20);
    executor.setQueueCapacity(100);
    executor.setTaskDecorator(new MdcTaskDecorator());
    executor.initialize();
    return executor;
}

이렇게 설정하면, @Async로 실행되는 모든 작업은 자동으로 MDC 컨텍스트가 전파된다.

6 - 4 . SecurityContext 전파는 어떻게 할까?

Spring Security의 SecurityContextHolder 역시 ThreadLocal 기반이므로 동일한 문제가 발생한다.

실무에서는 다음 중 하나를 선택한다.

  • MDC + SecurityContext를 같이 전파하는 커스텀 TaskDecorator
  • DelegatingSecurityContextAsyncTaskExecutor 사용
  • Spring Security 5.8+에서 제공하는 Context Propagation 지원 활용

핵심은 SecurityContext도 자동 전파되지 않는다는 점이다.


마무리 🔚

비동기 환경에서의 컨텍스트 전파는 단순한 기술적 디테일이 아니라 로그 추적성과 보안 일관성을 보장하는 핵심 요소이다.
동기 코드에서는 ThreadLocal로만 충분했지만, 오늘 알아본 비동기/리액티브 환경에서는 캡처 -> 복원 -> 정리의 흐름을 체계적으로 다뤄야한다.
이렇게 오늘 알아본 컨텍스트 전파를 토대로 환경에 맞는 전략을 선택하여 로그 상관관계와 보안 컨텍스트가 요청의 전 과정을 따라가며 안정적이게 추적 가능한 시스템을 운영해보자 💯

0개의 댓글