문홍의 공부장

[Java/Spring] 비동기 처리 시 ThreadContext 를 관리하는 방법 본문

개발/Spring

[Java/Spring] 비동기 처리 시 ThreadContext 를 관리하는 방법

moonong 2024. 4. 7. 22:46
반응형

 

웹 애플리케이션을 개발하다 보면 하나의 클라이언트 요청에 대해 애플리케이션 전반에 걸친 특정 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;  
}

References.

반응형