여기부터는 Spring을 이해하는 마지막 핵심 퍼즐이라고 생각하면 된다.
많은 사람들이 ThreadLocal을
"스레드마다 변수를 하나씩 가지는 것"
정도로 외운다.
하지만 이렇게 외우면 Spring Security, @Transactional, MyBatis, 로그 MDC가 왜 동작하는지 절대 이해할 수 없다.
이번 글에서는 JVM 메모리 구조부터 실제 Spring 내부 동작까지 연결해서 설명하겠다.
ThreadLocal 원리
ThreadLocal은 "변수를 저장하는 객체"가 아니라 "현재 Thread를 기준으로 값을 저장하고 조회하는 Map"이다.
먼저 왜 필요한가?
웹 서버에는 동시에 수많은 요청이 들어온다.
예를 들어
사용자 A
↓
로그인
↓
주문 조회
동시에
사용자 B
↓
로그인
↓
결제
도 수행된다.
Spring Boot(Tomcat)는 보통
Thread-1
↓
사용자 A 처리
Thread-2
↓
사용자 B 처리
처럼 각각 다른 Thread가 처리한다.
사용자 정보를 어디에 저장할까?
예를 들어 로그인하면
User user = login(...);
를 얻었다.
Service에서도
OrderService
PaymentService
ProductService
모두
현재 로그인한 사용자를 알아야 한다.
매번
service.save(user);
처럼 전달해야 할까?
Controller
↓
Service
↓
Repository
↓
DAO
↓
Util
↓
Logger
모든 메서드에 user를 전달해야 한다.
엄청 불편하다.
static 변수에 저장하면?
public class UserContext {
static User currentUser;
}
사용자 A
currentUser = Kim
동시에 사용자 B
currentUser = Lee
그러면
Kim
↓
Lee로 덮어쓰기
가 발생한다.
즉,
모든 Thread가 같은 변수 사용
↓
동시성 문제
가 생긴다.
ThreadLocal 등장
ThreadLocal은
Thread-1
↓
Kim
Thread-2
↓
Lee
를 각각 따로 저장한다.
그림
ThreadLocal
│
┌───────────────┼───────────────┐
│ │
Thread-1 Thread-2
│ │
currentUser=Kim currentUser=Lee
많이 하는 오해
많은 사람들이
ThreadLocal
↓
value 저장
이라고 생각한다.
실제로는
Thread
↓
ThreadLocalMap
↓
(ThreadLocal → value)
이다.
내부 구조
Thread 안에는
실제로 이런 필드가 있다.
class Thread {
ThreadLocalMap threadLocals;
}
즉,
Thread
------------------------
ThreadLocalMap
------------------------
을 가지고 있다.
ThreadLocalMap
Map 형태이다.
Thread-1
↓
ThreadLocalMap
----------------------------
ThreadLocalA → User(Kim)
ThreadLocalB → Connection
ThreadLocalC → Locale(KR)
----------------------------
Thread-2
ThreadLocalMap
----------------------------
ThreadLocalA → User(Lee)
ThreadLocalB → Connection
ThreadLocalC → Locale(US)
----------------------------
Thread마다 Map이 따로 존재한다.
set()
ThreadLocal<User> holder =
new ThreadLocal<>();
holder.set(user);
실제로는
현재 Thread 찾기
↓
Thread.threadLocals
↓
Map.put(holder, user)
를 수행한다.
get()
User user = holder.get();
실제로는
현재 Thread
↓
Thread.threadLocals
↓
Map.get(holder)
↓
user 반환
이다.
왜 Thread마다 다른 값이 나올까?
코드는 동일하다.
holder.get();
하지만
Thread-1
Thread.currentThread()
↓
ThreadLocalMap
↓
Kim
Thread-2
Thread.currentThread()
↓
ThreadLocalMap
↓
Lee
항상
현재 실행 중인 Thread 기준으로 조회하기 때문이다.
Spring Security도 이것을 사용한다.
로그인
Authentication
↓
SecurityContext
↓
ThreadLocal 저장
Controller
SecurityContextHolder
.getContext()
.getAuthentication()
어떻게 user를 전달하지 않았는데도
현재 로그인 사용자를 알 수 있을까?
현재 Thread
↓
ThreadLocal
↓
Authentication 반환
하기 때문이다.
@Transactional도 동일하다.
Controller
↓
Service
@Transactional
public void save() {
}
Spring 내부
Transaction 시작
↓
Connection 생성
↓
ThreadLocal 저장
Repository
DataSourceUtils.getConnection()
↓
ThreadLocal 조회
↓
같은 Connection 반환
그래서
Service
↓
Repository1
↓
Repository2
↓
Repository3
모두 같은 Connection을 사용한다.
MyBatis도 동일하다.
SqlSession
↓
ThreadLocal 저장
DAO
↓
Mapper
↓
Executor
모두
ThreadLocal
↓
SqlSession 조회
를 수행한다.
MDC(Logback)도 동일하다.
로그
requestId
↓
ThreadLocal 저장
로그 출력
현재 Thread
↓
ThreadLocal
↓
requestId 조회
↓
로그 출력
그래서
[requestId=123]
로그인
주문
결제
가 자동으로 출력된다.
ThreadLocal은 static과 같이 사용된다.
많이 보는 코드
private static final ThreadLocal<User>
HOLDER =
new ThreadLocal<>();
여기서
static
↓
객체 하나
이다.
하지만
값은
Thread
↓
ThreadLocalMap
↓
저장
이므로
Thread마다 완전히 별도
이다.
그래서
static인데
사용자마다 값이 다름
이 가능한 것이다.
그림으로 전체 흐름
static ThreadLocal
│
▼
Thread-1 ------------ ThreadLocalMap
│
▼
User(Kim)
Thread-2 ------------ ThreadLocalMap
│
▼
User(Lee)
실무 예제
현재 로그인 사용자
public class LoginUserHolder {
private static final ThreadLocal<User>
HOLDER =
new ThreadLocal<>();
public static void set(User user) {
HOLDER.set(user);
}
public static User get() {
return HOLDER.get();
}
}
Controller
LoginUserHolder.set(loginUser);
Service
User user =
LoginUserHolder.get();
Repository
User user =
LoginUserHolder.get();
인자를 전달하지 않아도
같은 Thread에서는 동일한 사용자를 얻는다.
하지만 치명적인 문제가 있다.
Tomcat은
Thread를 매 요청마다 새로 만들지 않는다.
ThreadPool
Thread-1
Thread-2
Thread-3
재사용
한다.
사용자 A
Thread-1
↓
ThreadLocal
↓
Kim 저장
요청 종료
하지만
remove()
안 함
다음 요청
사용자 B
Thread-1 재사용
↓
ThreadLocal 조회
↓
Kim 발견
사용자 정보가 섞이는 심각한 버그가 발생한다.
그래서 반드시 remove()
try {
holder.set(user);
...
} finally {
holder.remove();
}
를 수행해야 한다.
면접 포인트
ThreadLocal이란?
현재 Thread를 기준으로 독립적인 값을 저장하고 조회할 수 있도록 지원하는 클래스이다.
실제 값은 어디에 저장되는가?
많은 사람들이
ThreadLocal
↓
value
라고 생각하지만,
실제로는
Thread
↓
ThreadLocalMap
↓
(ThreadLocal → value)
형태로 저장된다.
Spring에서 어디에 사용되는가?
- Spring Security (SecurityContextHolder)
- @Transactional
- MyBatis SqlSession
- TransactionSynchronizationManager
- Log MDC
가장 중요한 실무 주의사항은?
ThreadPool 환경에서는 Thread가 재사용되므로 ThreadLocal.remove()를 호출하지 않으면 이전 요청의 데이터가 다음 요청에 남아 심각한 메모리 누수와 데이터 오염이 발생할 수 있다.
다음 글 추천
다음은 ThreadLocal Memory Leak이다.
이 글에서는
- 왜 WeakReference를 사용하는지
- remove()를 안 하면 정확히 어떤 메모리 누수가 발생하는지
- Tomcat ThreadPool에서 실제로 어떻게 장애가 나는지
- Spring이 왜 finally에서 ThreadLocal을 정리하는지
까지 JVM 내부 구조와 함께 깊이 있게 이어서 설명할 수 있다.
'language > java' 카테고리의 다른 글
| Reflection + Annotation + Proxy + ThreadLocal (0) | 2026.06.16 |
|---|---|
| ThreadLocal Memory Leak (0) | 2026.06.16 |
| BigDecimal을 사용하는 이유 (0) | 2026.06.16 |
| StringBuilder vs StringBuffer (0) | 2026.06.16 |
| Integer Cache (0) | 2026.06.16 |
댓글