왜 정보가 사라질까?
ThreadLocal (스레드 지역 변수)
특정 스레드 내에서만 공유되는 변수
다른 스레드가 접근할 수 없으므로 동기화 문제 없이
데이터를 안전하게 보관할 수 있다.
MDC & SecurityContext: 두 개념 모두 내부적으로
ThreadLocal을 사용하여 정보를 저장한다.
MDC: 로그에 찍을 추적 ID(Trace ID) 등을 스레드별로 저장.
"해당 스레드가 살아있는 동안, 어디서든 꺼내 쓸 수 있는 사물함"
입구 (필터): 요청이 들어오면 서버는 무작위 UUID를 생성하여
MDC 사물함에 넣는다.
실행 중 (서비스/리포지토리): 로그 찍을 때, 내가 직접 ID 입력 안해도
로깅 라이브러리(Logback 등)가 자동으로 MDC 사물함 뒤져서
ID를 로그 앞에 붙여준다.
Finally: 요청 처리가 끝나면 다음 사람을 위해 사물함을 비운다.
로그용 ID를 따로 넘길 필요 없기에 코드가 깔끔해지고,
데이터 오염도 방지해주고, 각 스레드마다 독립적인 MDC 사물함을
쓰기 때문에, A의 ID가 B의 로그에 찍힐 걱정이 없다.
SecurityContext: 현재 로그인한 사용자 정보를 스레드별로 저장.
@Async나 CompletableFuture를 사용하면 작업은
기존 스레드(Parent)가 아닌 새로운 스레드(Worker)에서 실행된다.
ThreadLocal은 스레드 단위로 데이터가 격리되어 있기 때문에,
부모 스레드가 가진 정보를 자식 스레드는 알 방법이 없다.
해결 전략: 컨텍스트 전파
비동기 환경에서 정보를 유지하려면
"부모 스레드의 정보를 복사해서 자식 스레드에 직접 주입"하는 과정이 필요하다.
단계 정리
Capture: 부모 스레드에서 실행 직전 컨텍스트 정보를 읽어온다.
Setup: 자식 스레드가 작업을 시작하기 전, 읽어온 정보를 설정한다.
Execute: 비동기 비즈니스 로직 실행한다.
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 전달
DelegatingSecurityContextAsyncTaskExecutor 사용
@Bean
public Executor securityAwareExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.initialize();
// SecurityContext를 자식 스레드에 복사해주는 래퍼 사용
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
| 구분 | 해결 방법 | 비고 |
|---|---|---|
| MDC | TaskDecorator 구현 | 커스텀 필드 전파에 유리 |
| SecurityContext | DelegatingSecurityContext... 사용 | 보안 정보 전파에 최적화 |
| 공통 주의사항 | 반드시 finally에서 clear() 호출 | 스레드 풀 환경에서 데이터 누수 방지 |
