ThreadLocal이란?
ThreadLocal은 Java에서 각 스레드마다 독립적인 변수 공간을 제공하는 클래스입니다. 멀티스레드 환경에서 공유 자원으로 인한 동시성 문제를 해결하는 핵심 도구 중 하나로, 각 스레드가 자신만의 데이터를 안전하게 관리할 수 있도록 합니다.
일반적인 static 변수나 인스턴스 변수는 여러 스레드가 공유하기 때문에 synchronized 블록이나 Lock을 사용해야 하지만, ThreadLocal은 각 스레드별로 독립된 저장소를 제공하여 동기화 없이도 thread-safe한 프로그래밍을 가능하게 합니다.
Spring Framework에서는 ThreadLocal을 활용하여 트랜잭션 관리, 보안 컨텍스트 관리, 웹 요청별 데이터 관리 등 다양한 기능을 구현하고 있어, 백엔드 개발에서 필수적으로 이해해야 할 개념입니다.
핵심 개념
1. ThreadLocal 동작 원리
ThreadLocal의 핵심은 각 Thread가 자신만의 ThreadLocalMap을 갖는다는 점입니다:
public class ThreadLocalExample {
private static ThreadLocal<String> userContext = new ThreadLocal<>();
public static void setUser(String user) {
userContext.set(user);
}
public static String getUser() {
return userContext.get();
}
public static void main(String[] args) throws InterruptedException {
// 스레드 1
Thread thread1 = new Thread(() -> {
setUser("Alice");
System.out.println("Thread 1: " + getUser()); // Alice
});
// 스레드 2
Thread thread2 = new Thread(() -> {
setUser("Bob");
System.out.println("Thread 2: " + getUser()); // Bob
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
각 스레드는 ThreadLocal 인스턴스를 키로 사용하여 자신의 ThreadLocalMap에 값을 저장합니다. 따라서 같은 ThreadLocal 객체를 사용하더라도 스레드별로 다른 값을 가질 수 있습니다.
2. Spring에서의 ThreadLocal 활용
Spring은 ThreadLocal을 활용하여 다양한 컨텍스트를 관리합니다:
// 트랜잭션 동기화 관리
public class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
}
// 보안 컨텍스트 관리
public class SecurityContextHolder {
private static final ThreadLocal<SecurityContext> contextHolder =
new ThreadLocal<>();
public static void setContext(SecurityContext context) {
contextHolder.set(context);
}
public static SecurityContext getContext() {
return contextHolder.get();
}
}
// 웹 요청 컨텍스트 관리
public class RequestContextHolder {
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
public static void setRequestAttributes(RequestAttributes attributes) {
requestAttributesHolder.set(attributes);
}
}
이러한 구조 덕분에 Service 계층에서 별도의 파라미터 전달 없이도 현재 사용자 정보나 트랜잭션 상태에 접근할 수 있습니다.
3. ThreadLocal 주의사항과 메모리 누수 방지
ThreadLocal 사용 시 가장 중요한 것은 적절한 정리(cleanup)입니다:
public class ThreadLocalMemoryLeakExample {
private static final ThreadLocal<List<String>> dataHolder = new ThreadLocal<>();
public void processRequest() {
try {
// ThreadLocal에 데이터 설정
List<String> data = new ArrayList<>();
data.add("important data");
dataHolder.set(data);
// 비즈니스 로직 수행
performBusinessLogic();
} finally {
// 반드시 정리해야 함
dataHolder.remove();
}
}
private void performBusinessLogic() {
List<String> data = dataHolder.get();
// 비즈니스 로직 수행
}
}
스레드풀 환경에서는 스레드가 재사용되기 때문에, 이전 요청의 ThreadLocal 값이 남아있을 수 있습니다. 이를 방지하기 위해 반드시 remove() 메서드를 호출해야 합니다.
4. 비동기 처리와 ThreadLocal
비동기 처리에서 ThreadLocal은 예상대로 동작하지 않을 수 있습니다:
@Service
public class AsyncService {
private static final ThreadLocal<String> userContext = new ThreadLocal<>();
public void processWithAsync() {
userContext.set("currentUser");
// 이 비동기 메서드는 새로운 스레드에서 실행됨
asyncMethod(); // userContext.get()은 null을 반환
}
@Async
public void asyncMethod() {
String user = userContext.get(); // null!
System.out.println("Current user: " + user);
}
}
이 문제를 해결하기 위해 Spring에서는 TaskDecorator를 제공합니다:
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.initialize();
return executor;
}
public static class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
return () -> {
try {
// 비동기 스레드에 컨텍스트 복사
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(currentUser, null));
runnable.run();
} finally {
SecurityContextHolder.clearContext();
}
};
}
}
}
정리
| 측면 | 설명 |
|---|---|
| 핵심 개념 | 각 스레드별 독립적인 변수 저장소 제공 |
| 동작 원리 | Thread마다 ThreadLocalMap 보유, ThreadLocal을 키로 값 저장 |
| 장점 | - 스레드 안전성 보장- 동기화 불필요- 컨텍스트 정보 전역 접근 |
| Spring 활용 | - TransactionSynchronizationManager- SecurityContextHolder- RequestContextHolder |
| 주의사항 | - 메모리 누수 방지를 위한 remove() 호출 필수- 스레드풀 환경에서 값 재사용 위험- 비동기 처리 시 컨텍스트 전파 문제 |
| 대안 | - 메서드 파라미터 전달- ConcurrentHashMap 활용- @RequestScope 어노테이션 |
ThreadLocal은 강력한 도구이지만 적절한 관리가 필요합니다. 특히 웹 애플리케이션에서는 요청 처리 완료 후 반드시 정리하고, 비동기 처리 시에는 컨텍스트 전파 방안을 고려해야 합니다.