[Java/Spring] 비동기 처리 시 ThreadContext 를 관리하는 방법
웹 애플리케이션을 개발하다 보면 하나의 클라이언트 요청에 대해 애플리케이션 전반에 걸친 특정 Context를 유지
해야 할 필요성이 생긴다. 대표적으로 로깅 정보나 사용자 정보가 있다. 이 때 흔히 사용하는 방안은 ThreadLocal을 활용
하는 것이다.
스프링에서 Web Request
가 오게 되면 하나의 쓰레드를 할당해서 해당 작업을 처리하게 된다. 이때 Thread
에 대한 정보를 ThreadLocal
에 저장하게 되면 해당 작업이 끝날 때 까지 모든 상황에서 context
를 유지하고 저장하고 찾아볼 수 있다.
ThreadLocal은 쓰레드의 로컬 컨텍스트 변수로 Thread 가 존재하는 한 계속해서 남아 있는 변수이다. 작업 요청이 들어왔을때 하나의 쓰레드가 생성이 되고 작업이 끝나면 쓰레드가 없어진다고 할 때, 쓰레드가 살아있을 동안에 계속 유지되는 변수이다. 즉 쓰레드가 생성되어 있는 동안에, 쓰레드 안에서 계속 참고해서 쓸수 있는 쓰레드 범위의 글로벌 변수라고 볼 수 있다.
하지만 ThreadLocal은 이름 그대로 Thread별로 값을 유지하기 때문에 비동기 호출 시 해당 값이 유지되지 않는다. 비동기 처리를 위해 TaskExecutor
를 활용하기 시작하면 해당 executor
는 새로운 Thread
를 생성한다. 즉, 기존 쓰레드의 context
가 넘어가지 않고 새로운(빈) 쓰레드를 사용하게 되는 것이다.
비동기 처리 시 기존 쓰레드의 값을 유지하려면 어떻게 해야할까?
Spring 4.3
이상부터 제공되는 TaskDecorator
를 이용해서 비동기처리하는 taskExecutor
생성 시 커스터마이징이 가능하다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UserIdHolder {
private final static ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();
public static Long getUserId() {
return USER_ID_HOLDER.get();
}
public static void setUserId(Long userId) {
USER_ID_HOLDER.set(userId);
}
public static void clear() {
USER_ID_HOLDER.remove();
}
}
---
public class ClonedTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable task) {
long userId = UserIdHolder.getUserId(); // 기존 thread에서 userId를 가져온다.
return () -> {
try {
UserIdHolder.setUserId(userId); // 새로운 thread에서 가져온 userId를 세팅한다.
runnable.run();
} finally {
UserIdHolder.clear(); // 새로운 thread 작업이 완료되면 ThreadLocal 값을 초기화한다.
}
};
}
}
---
@Bean(name = "test")
public Executor testThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(5000);
executor.setThreadNamePrefix("default-thread-");
executor.setTaskDecorator(new ClonedTaskDecorator());
executor.initialize();
return executor;
}