1. ThreadLocal이란?
ThreadLocal
은 스레드별로 독립적인 변수를 제공하는 자바 클래스입니다. 일반적인 변수는 모든 스레드가 공유하지만, ThreadLocal
로 선언된 변수는 각 스레드가 자신만의 값을 가질 수 있습니다. 이를 통해 스레드 간 데이터 간섭을 방지하고, 스레드 안전성을 확보할 수 있죠.
쉽게 말해, ThreadLocal
은 "스레드마다 개인 사물함을 주는" 개념이라고 생각하면 됩니다. 각 스레드가 자신의 사물함에 데이터를 넣고 꺼낼 수 있지만, 다른 스레드의 사물함에는 접근할 수 없습니다.
1.1. 주요 특징
- 스레드 격리: 각 스레드는 자신만의
ThreadLocal
값을 가집니다. - 간단한 사용법:
set()
,get()
,remove()
메서드로 쉽게 관리 가능. - 메모리 누수 주의: 사용 후 정리하지 않으면 메모리 누수가 발생할 수 있음.
1.2. 동시성 발생 예시
ThreadLocal을 사용하지 않고 일반 변수를 공유하면, 멀티스레드 환경에서 동시성 문제가 발생할 수 있습니다. 아래는 그 예제입니다.
<code />
public class NoThreadLocalExample {
private static String sharedValue = ""; // 모든 스레드가 공유하는 변수
public static void main(String[] args) {
Runnable task = () -> {
sharedValue = Thread.currentThread().getName() + " Value"; // 값 설정
try {
Thread.sleep(100); // 경쟁 조건을 강조하기 위해 지연 추가
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + sharedValue);
};
Thread thread1 = new Thread(task, "Thread-1");
Thread thread2 = new Thread(task, "Thread-2");
thread1.start();
thread2.start();
}
}
출력결과

2. ThreadLocal 동작 원리
ThreadLocal
은 내부적으로 Thread
객체 안에 ThreadLocalMap
이라는 맵을 유지합니다. 이 맵은 ThreadLocal
객체를 키로, 해당 스레드의 값을 값으로 저장합니다. 하지만 이 간단한 설명만으로는 어떻게 스레드별로 값이 격리되는지 완전히 이해하기 어렵죠. 좀 더 깊이 들어가 보겠습니다.
2.1. ThreadLocal의 내부 구조
Thread 클래스와의 관계: 자바에서 모든 스레드는 Thread 클래스의 인스턴스로 표현됩니다. Thread 클래스에는 ThreadLocal.ThreadLocalMap threadLocals라는 필드가 존재합니다. 이 필드는 각 스레드마다 고유한 ThreadLocalMap을 저장합니다.
ThreadLocalMap이란?: ThreadLocalMap은 ThreadLocal 객체와 해당 스레드의 값을 쌍으로 저장하는 해시맵과 유사한 자료구조입니다. 키는 ThreadLocal 인스턴스이고, 값은 해당 스레드가 설정한 데이터입니다.
격리 원리: threadLocals는 Thread 객체 내부에 속해 있으므로, 각 스레드는 자신만의 ThreadLocalMap을 갖게 됩니다. 즉, 스레드 A의 ThreadLocalMap과 스레드 B의 ThreadLocalMap은 완전히 별개의 객체입니다.
2.2. 동작 과정
2.2.1. 값 설정 (set 호출):
- threadLocal.set("Value")를 호출하면, 현재 스레드(Thread.currentThread())의 threadLocals 맵에 접근합니다.
만약 threadLocals가 null이라면 새로 ThreadLocalMap을 생성하고, ThreadLocal 객체(키)와 "Value"(값)를 맵에 저장합니다. - 이미 맵이 존재한다면, 해당 ThreadLocal 키에 새로운 값을 업데이트합니다.
2.2.2. 값 조회 (get 호출):
- threadLocal.get()을 호출하면, 현재 스레드의 threadLocals 맵에서 해당 ThreadLocal 키에 매핑된 값을 반환합니다.
- 값이 없으면 ThreadLocal의 initialValue() 메서드를 호출해 기본값을 설정합니다(기본적으로 null).
2.2.3. 값 제거 (remove 호출):
- threadLocal.remove()는 현재 스레드의 threadLocals 맵에서 해당 ThreadLocal 키와 값을 삭제합니다.
2.2.4. 스레드별 변수 보장의 핵심
- 스레드별 맵: ThreadLocalMap이 Thread 객체에 속해 있기 때문에, 각 스레드는 독립적인 저장소를 가집니다. 스레드 A가 ThreadLocal에 값을 설정해도 스레드 B의 ThreadLocalMap에는 영향을 주지 않습니다.
- 참조 관리: ThreadLocal 객체 자체는 모든 스레드가 공유하지만, 실제 데이터는 각 스레드의 ThreadLocalMap에 저장되므로 격리가 보장됩니다.
- WeakReference 사용: ThreadLocalMap의 키(ThreadLocal)는 WeakReference로 관리됩니다. 이는 ThreadLocal 객체가 더 이상 참조되지 않을 때 가비지 컬렉션에 의해 제거될 수 있게 해줍니다. 하지만 값은 강한 참조로 유지되므로, 스레드가 종료되고 remove()를 호출하지 않으면 메모리 누수가 발생할 수 있습니다.
2.3. 동작 예시
스레드 1과 스레드 2가 동일한 ThreadLocal 인스턴스를 사용할 때:
- 스레드 1: threadLocal.set("Value1") → 스레드 1의 ThreadLocalMap에 {threadLocal: "Value1"} 저장.
- 스레드 2: threadLocal.set("Value2") → 스레드 2의 ThreadLocalMap에 {threadLocal: "Value2"} 저장.
- 스레드 1: threadLocal.get() → "Value1" 반환.
- 스레드 2: threadLocal.get() → "Value2" 반환.
이처럼 각 스레드는 자신의 ThreadLocalMap만 참조하므로 값이 서로 간섭하지 않습니다.
3. 코드로 살펴보기
간단한 예제를 통해 ThreadLocal
의 사용법을 알아봅시다.
<code />
public class ThreadLocalExample {
private static final ThreadLocal threadLocal<String> = new ThreadLocal<>();
public static void main(String[] args) {
// 스레드 1
Thread thread1 = new Thread(() -> {
threadLocal.set("Thread-1 Value");
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
threadLocal.remove(); // 사용 후 제거
}, "Thread-1");
// 스레드 2
Thread thread2 = new Thread(() -> {
threadLocal.set("Thread-2 Value");
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
threadLocal.remove();
}, "Thread-2");
thread1.start();
thread2.start();
}
}

- threadLocal.set()으로 각 스레드가 자신의 값을 설정하고, get()으로 꺼냅니다.
- 스레드 1과 스레드 2의 값이 서로 간섭하지 않는 것을 확인할 수 있죠.
3.1. 사용자 세션 관리
웹 애플리케이션에서 요청별로 사용자 정보를 ThreadLocal에 저장하면, 스레드 풀 환경에서도 안전하게 사용자 데이터를 관리할 수 있습니다.
<code />
public class UserContext {
private static final ThreadLocal<String> userId = new ThreadLocal<>();
public static void setUser(String id) {
userId.set(id);
}
public static String getUser() {
return userId.get();
}
public static void clear() {
userId.remove();
}
}
3.2. 트랜잭션 컨텍스트 전달
Spring 프레임워크에서 TransactionSynchronizationManager는 ThreadLocal을 사용해 트랜잭션 상태를 스레드별로 관리합니다.
주의할 점: 메모리 누수
ThreadLocal을 사용할 때는 반드시 사용 후 remove()를 호출해야 합니다. 스레드 풀이 사용되는 환경(예: 웹 서버)에서 스레드가 재사용되면, 이전에 저장된 ThreadLocal 값이 남아 있을 수 있어 메모리 누수가 발생할 수 있습니다.
해결법
- 작업 종료 후 항상 remove() 호출.
- try-finally 블록으로 보장:
<code />
try {
threadLocal.set("Some Value");
// 작업 수행
} finally {
threadLocal.remove();
}
3.3. ThreadLocal vs 전역 변수 vs synchronized
