Backend/SpringBoot

Task Decorator (@Async 에서 MDC, SecurityContextHolder 정보 활용)

Dean83 2026. 1. 2. 16:25

Task Decorator

  • ThreadLocal은 스레드 별로 값이 저장되므로, @Async를 이용할 경우 다음의 데이터가 공유되지 않는 문제가 있다.
    • MDC를 이용하여 저장한 항목
    • SecurityContext 에 저장된 인증된 사용자 정보
  • 이를 해결하기 위해 Task Decorator를 이용한다. 예는 다음과 같다.

 

  • ThreadLocal이 정의 되어 있는 클래스
public class UserContextHolder {

    private static final ThreadLocal<String> USER_ID =
            new ThreadLocal<>();

    public static void set(String userId) {
        USER_ID.set(userId);
    }

    public static String get() {
        return USER_ID.get();
    }

    public static void clear() {
        USER_ID.remove();
    }
}

 

  • Main 스레드에 있는 ThreadLocal을 이용하여 runnable의 ThreadLocal에 값을 복사하는 부분
import org.springframework.core.task.TaskDecorator;

import java.util.Map;
import org.slf4j.MDC;

public class ContextCopyingTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable task) {

        // 1. 부모 스레드의 컨텍스트 캡처
        String userId = UserContextHolder.get();
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();

        return () -> {
            try {
                // 2. 자식 스레드에 컨텍스트 설정
                if (userId != null) {
                    UserContextHolder.set(userId);
                }
                if (mdcContext != null) {
                    MDC.setContextMap(mdcContext);
                }

                // 3. 실제 작업 실행
                task.run();

            } finally {
                // 4. 반드시 clean
                UserContextHolder.clear();
                MDC.clear();
            }
        };
    }
}
  • decorate 메서드 에서 runnable을 리턴하기전, main에 있는 ThreadLocal 항목을 불러와 값을 저장한다. 여기까지는 메인 스레드 영역
  • return 에서 불러온 값을 다시 ThreadLocal에 추가 하고, runnable을 리턴한다. 여기부터 runnable의 실행인 새 스레드 영역
  • 실행완료시 초기화 하는 코드를 넣는다.

 

번 Async 작업을 할 때마다 위에 정의한 클래스를 이용하라면 매우 번거롭다. 따라서 @Async 설정에서 일괄 적용하도록 할 수도 있다. 

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class AsyncExecutorConfig {

    @Bean
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");

        executor.setTaskDecorator(new ContextCopyingTaskDecorator());

        executor.initialize();
        return executor;
    }
}

 

이렇게 할 경우, Async 동작시 일괄적으로 적용되어서 이용 할 수 있게 된다.

혹은 AsyncConfigurer 를 이용해 비슷하게 설정도 가능 하다. (https://dean83.tistory.com/393)

'Backend > SpringBoot' 카테고리의 다른 글

서킷 브레이커  (0) 2026.01.05
MVC 환경에서 WebClient  (0) 2026.01.05
@Async 설정 및 사용  (0) 2026.01.02
[Spring Security] SessionID 관련 주요 필터, 이벤트처리 및 설정  (0) 2025.12.17
[Spring Security] CORS 설정  (0) 2025.12.16