"스레드마다 독립적인 저장공간을 가진다" – 이 한 줄이 핵심입니다.
ThreadLocal<T>은 각 스레드마다 독립적인 값을 저장할 수 있게 해주는 Java 클래스입니다.
즉, 동일한 ThreadLocal 인스턴스를 여러 스레드가 공유하더라도 각 스레드는 자신만의 고유한 값을 갖습니다.
동시성 환경에서는 여러 스레드가 같은 변수에 접근할 경우 race condition이 발생합니다.
하지만 ThreadLocal을 사용하면 변수 자체를 스레드마다 격리시킬 수 있기 때문에 동기화 필요 없이 안전하게 사용할 수 있습니다.
Thread 객체는 내부에 ThreadLocalMap이라는 저장소를 갖고 있음ThreadLocal.set() → 현재 스레드의 ThreadLocalMap에 값 저장ThreadLocal.get() → 현재 스레드 기준의 값 반환ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello");
String value = threadLocal.get(); // 현재 스레드의 값만 반환
Spring 내부에서도 ThreadLocal은 아래와 같은 기능 구현에 널리 활용되고 있습니다.
| 사용처 | 설명 |
|---|---|
TransactionSynchronizationManager | 트랜잭션 컨텍스트 관리 |
SecurityContextHolder | 인증된 사용자 정보 보관 |
RequestContextHolder | HTTP 요청 스코프 데이터 저장 |
Logging MDC (Mapped Diagnostic Context) | 로그 추적 시, 요청 ID 등 컨텍스트 정보 저장 |
ThreadLocal 유출ThreadPool을 사용하는 경우, 스레드가 재사용되기 때문에 이전 작업의 ThreadLocal 값이 남아 있을 수 있습니다.
// 반드시 작업 끝나고 ThreadLocal 제거
threadLocal.remove();
정리하지 않으면 메모리 누수 또는 예기치 않은 값 공유 문제가 발생합니다.
예를 들어 @Async 메서드에서는 다른 스레드가 사용되므로, 이전 스레드의 ThreadLocal 값을 참조할 수 없습니다.
TaskDecorator 사용@Bean
public TaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new ContextCopyingDecorator());
return executor;
}
NamedThreadLocal로 디버깅 개선Spring에서는 NamedThreadLocal을 제공하여 ThreadLocal 변수에 이름을 부여할 수 있습니다.
private static final ThreadLocal<String> userContext =
new NamedThreadLocal<>("User Context");
디버깅이나 메모리 분석 시
"User Context"라는 이름이 보이기 때문에 추적이 훨씬 쉬워집니다.
| 대체 수단 | 장단점 |
|---|---|
| 메서드 인자 전달 | 명시적이지만 번거로움 |
@RequestScope Bean | HTTP 요청 단위 저장소, Spring에서 안전하게 관리됨 |
ConcurrentHashMap 등 동기화 자료구조 | ThreadLocal처럼 스레드 간 격리는 불가능 |
ThreadLocal은 매우 강력하지만, 동시에 신중하게 사용해야 하는 도구입니다.
트랜잭션, 인증 정보, 요청 컨텍스트 등 다양한 곳에 쓰일 수 있지만, 스레드풀과 비동기 환경에서는 반드시 수동 정리와 전파 전략이 필요합니다.
✔️ 쓰고 나면 꼭
remove()
✔️ 비동기 환경에서는TaskDecorator
✔️ 디버깅은NamedThreadLocal