[Spring] 비동기(@Async) 환경에서 MDC와 SecurityContext가 사라지는 이유와 해결책

Jun_k·2026년 4월 12일

Spring

목록 보기
8/9
post-thumbnail

왜 정보가 사라질까?

ThreadLocal (스레드 지역 변수)

  • 특정 스레드 내에서만 공유되는 변수

  • 다른 스레드가 접근할 수 없으므로 동기화 문제 없이
    데이터를 안전하게 보관할 수 있다.

  • MDC & SecurityContext: 두 개념 모두 내부적으로
    ThreadLocal을 사용하여 정보를 저장한다.

  • MDC: 로그에 찍을 추적 ID(Trace ID) 등을 스레드별로 저장.

    • "해당 스레드가 살아있는 동안, 어디서든 꺼내 쓸 수 있는 사물함"

    • 입구 (필터): 요청이 들어오면 서버는 무작위 UUID를 생성하여
      MDC 사물함에 넣는다.

    • 실행 중 (서비스/리포지토리): 로그 찍을 때, 내가 직접 ID 입력 안해도
      로깅 라이브러리(Logback 등)가 자동으로 MDC 사물함 뒤져서
      ID를 로그 앞에 붙여준다.

    • Finally: 요청 처리가 끝나면 다음 사람을 위해 사물함을 비운다.

    • 로그용 ID를 따로 넘길 필요 없기에 코드가 깔끔해지고,
      데이터 오염도 방지해주고, 각 스레드마다 독립적인 MDC 사물함을
      쓰기 때문에, A의 ID가 B의 로그에 찍힐 걱정이 없다.

  • SecurityContext: 현재 로그인한 사용자 정보를 스레드별로 저장.

  • @AsyncCompletableFuture를 사용하면 작업은
    기존 스레드(Parent)가 아닌 새로운 스레드(Worker)에서 실행된다.
    ThreadLocal은 스레드 단위로 데이터가 격리되어 있기 때문에,
    부모 스레드가 가진 정보를 자식 스레드는 알 방법이 없다.


해결 전략: 컨텍스트 전파

  • 비동기 환경에서 정보를 유지하려면
    "부모 스레드의 정보를 복사해서 자식 스레드에 직접 주입"하는 과정이 필요하다.

  • 단계 정리

    1. Capture: 부모 스레드에서 실행 직전 컨텍스트 정보를 읽어온다.

    2. Setup: 자식 스레드가 작업을 시작하기 전, 읽어온 정보를 설정한다.

    3. Execute: 비동기 비즈니스 로직 실행한다.

    4. Clear: 작업 완료 후, 자식 스레드의 컨텍스트를 무조건 비운다.
      (스레드 풀 환경에서 데이터 오염 방지)

  • Spring Boot에서 가장 권장되는 방식인 TaskDecorator를 활용한 설정
    (MDC 전달을 위한 TaskDecorator 구현)

import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import java.util.Map;

public class MdcTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        // [1] 부모 스레드의 MDC 맵을 복사 (Capture)
        Map<String, String> contextMap = MDC.getCopyOfContextMap();

        return () -> {
            try {
                // [2] 자식 스레드에 설정 (Setup)
                if (contextMap != null) {
                    MDC.setContextMap(contextMap);
                }
                // [3] 실제 작업 수행 (Execute)
                runnable.run();
            } finally {
                // [4] 작업 완료 후 정리 (Clear) - 스레드 풀 재사용 대비
                MDC.clear();
            }
        };
    }
}

ThreadExecutor 설정

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        // 위에서 만든 데코레이터 설정
        executor.setTaskDecorator(new MdcTaskDecorator());
        executor.initialize();
        return executor;
    }
}

Spring Security Context 전달

  • SecurityContext는 보안상의 이유로 좀 더 엄격한 관리가 필요하다.
    이를 위해 전용 래퍼 클래스를 제공한다.

DelegatingSecurityContextAsyncTaskExecutor 사용

@Bean
public Executor securityAwareExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.initialize();
    
    // SecurityContext를 자식 스레드에 복사해주는 래퍼 사용
    return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
구분해결 방법비고
MDCTaskDecorator 구현커스텀 필드 전파에 유리
SecurityContextDelegatingSecurityContext... 사용보안 정보 전파에 최적화
공통 주의사항반드시 finally에서 clear() 호출스레드 풀 환경에서 데이터 누수 방지

profile
개발을 즐겨보자.

0개의 댓글