티스토리 뷰
[Java] ThreadLocal 알아보기
hyeon.q 2025. 7. 29. 21:52
실무에서 가끔 ThreadLocal 자료구조를 사용하였고, 위 자료구조에 대해서 자세하게 공부를 해보았다
1. 동작 구조
ThreadLocal은 각 스레드마다 독립적인 변수 복사본을 제공하는 자료구조 이다
각 Thread 객체는 ThreadLocalMap 내부 Map<K,V> 를 가지고 있다.
즉 ThreadLocal 객체가 키(key)가 되고, 저장하려는 값이 값(value)이 된다
실제 사용은 Thread.currentThread().threadId() 통해 현재 스레드의 맵에 접근한다
// ThreadLocal.get() 메서드의 동작
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T)e.value;
}
}
return setInitialValue();
}
각 스레드가 ThreadLocal 변수에 접근할 때, 자신만의 독립적인 저장 공간에서 값을 가져오거나 설정한다.
위 자료구조를 사용하는 주된 이유는 캐싱을 목적으로 많이 사용할 것이라고 생각한다
멀티 스레드 환경에서 각 스레드당 독립적인 값을 보장해주니 편하게 사용하기 좋다고 생각한다.
2. 사용방법
public class ThreadLocalStorage {
public static final ThreadLocal<String> sessionId = new ThreadLocal<>();
public static final ThreadLocal<String> userId = new ThreadLocal<>();
}
필자는 위 ThreadLocalStorage 클래스에 ThreadLocal 변수를 static 으로 선언해두고 사용하는 편이다
그리고 실제로 특정 로직에서 ThreadLocal 에 set 을 한다면 set 을 요청한 스레드 간에 공유가 가능하다.
set, get 방법은 아래와 같다.
void test () {
ThreadLocalStorage.sessionId.set("123456789");
ThreadLocalStorage.userId.set("123456789-102313");
log.info("ThreadLocalSessionId : {}", ThreadLocalStorage.sessionId.get());
log.info("ThreadLocalUserId : {}", ThreadLocalStorage.userId.get());
}
2025-07-29T19:43:18.119+09:00 INFO 4111 --- [Thread-1] o.h.s.a.threads.ThreadLocalStorage: ThreadLocalSessionId : 123456789
2025-07-29T19:43:18.119+09:00 INFO 4111 --- [Thread-1] o.h.s.a.threads.ThreadLocalStorage: ThreadLocalUserId : 123456789-102313
요청 스레드간에 이제 자유롭게 사용이 가능하다
그리고 스레드 사용이 다 끝났다면 ThreadLocal 을 remove 해줘야 한다
remove 를 해주지 않으면 그 스레드에 대한 ThreadLocal 객체가 살아있어 GC 대상이 잡히지 않아 추후 메모리 누수로 이어질 수 있다.
ThreadLocalStorage.userId.remove();
ThreadLocalStorage.sessionId.remove();
위 처럼 간단하게 사용을 할 수 있다
그리고 메모리 누수를 방지하기 위해서 항상 try-finally 블록에서 실행하는 것이 좋다
public void start() {
try {
ThreadLocalStorage.sessionId.set("123456789");
ThreadLocalStorage.userId.set("123456789-102313");
// 비즈니스 로직
} finally {
ThreadLocalStorage.userId.remove();
ThreadLocalStorage.sessionId.remove();
}
}
3. 장점
1. 스레드 안전성 (Thread Safety)
- 각 스레드가 독립적인 변수 복사본을 가지므로 동기화 없어도 안전
- synchronized, lock 없이도 스레드 간 데이터 충돌 방지
2. 성능 향상
- 동기화 오버헤드가 없어 멀티스레드 환경에서 효율이 좋음
- os 레벨 스레드 간 컨텍스트 스위칭 비용 감소
3. 코드 간소화
- 메서드 파라미터로 값 전달이 필요 없고, 전역에서 편하게 접근 가능
- 깊은 호출 스택에서도 쉽게 데이터에 접근 가능
- -> 이 부분이 생각보다 엄청 유용합니다. 너무 많은 파라미터는 좋지 않으므로 ThreadLocal 사용을 통한 클린 코드를 만들 수 있음
4. 격리성 보장
- 각 스레드의 데이터가 완전히 격리되어 예측 가능한 동작을 보장합니다
4. 한계
1. 비동기 환경에서 사용 어려움
- 비동기 처리 필요 시 ThreadLocal 사용 불가
- CompletableFuture, @Async 사용 -> 새로운 작업 스레드 생성
- 원본 스레드와 비동기 작업 스레드가 다르므로 ThreadLocal 값에 접근 시 -> null 반환함
2. 메모리 누수 위험
- 명시적으로 remove() 를 해주지 않으면 메모리 누수 위험
ThreadLocalMap
의 Entry 는 ThreadLocal 인스턴스의WeakReference
를 키로 사용하고 실제 저장될 값은 강한 참조를 가지고 있다'- WeakReference 덕분에 ThreadLocal 객체 자체가 외부에서 더 이상 참조되지 않으면 GC 대상이 될 수 있지만 ThreadLocalMap 내의 값(value)은 스레드가 살아있는 한 강한 참조로 유지된다
ThreadLocal
객체가 GC 대상이 되더라도, 해당 스레드의 ThreadLocalMap 에는 여전히 null 키와 함께 값(value)이 남아있게 되어 메모리 누수가 발생하게 된다.- 그러므로 값을 지워주는
remove()
를 꼭 호출해야 한다 그래야 메모리 누수를 방지할 수 있다. - 특히 SpringBoot 에서 사용하는 내장 Tomcat 은 스레드 풀을 사용하여 스레드를 재사용 하기 때문에 메모리 누수에 취약할 수 있다.
5. 결론
ThreadLocal
은 요청별 변하지 않는 특정 값 ex) 사용자 세션 정보(예: sessionId
, userId
)를 저장해 메서드 파라미터 전달을 줄이는 데 유용하다
하지만 비즈니스 프로세스상 비동기 처리 또는 애플리케이션 전역 캐싱이 필요한 경우에는 ThreadLocal 이 아닌 다른 대안을 찾아야 한다
ex) API 응답 캐싱(전역캐싱) -> Caffeine Cache, 분산 환경 -> Redis