이번 글은 Spring 내부를 이해하는 핵심 글이다.
사실 많은 개발자가
"ThreadLocal에 Connection이 저장된다."
까지만 알고 있다.
하지만 Spring은 직접 ThreadLocal을 여기저기 사용하지 않는다.
그 역할을 하는 중앙 관리자(Central Manager)가 있다.
바로
TransactionSynchronizationManager(TSM)
이다.
이 글을 이해하면
- @Transactional
- MyBatis
- JPA(Hibernate)
- Spring Data
가 어떻게 같은 Connection을 공유하는지 이해할 수 있다.
ThreadLocal과 TransactionSynchronizationManager
Spring은 ThreadLocal을 직접 사용할까?
이전 글에서는
Spring Transaction이
ThreadLocal
↓
Connection 저장
↓
Repository에서 조회
↓
Commit
↓
ThreadLocal.remove()
구조로 동작한다고 배웠다.
이 설명은 개념적으로는 맞다.
하지만 실제 Spring 내부는 조금 더 정교하게 설계되어 있다.
Spring은
ThreadLocal<Connection>
를 여기저기 직접 사용하는 것이 아니라,
모든 리소스를 하나의 중앙 관리자에게 맡긴다.
그 객체가 바로
TransactionSynchronizationManager(TSM)
이다.
먼저 질문 하나
하나의 요청에서
Controller
↓
Service
↓
MyBatis
↓
JPA
↓
JdbcTemplate
가 모두 실행된다면
Connection은 어디서 관리해야 할까?
각각이
ThreadLocal<Connection>
를 만든다면
관리하기 어렵다.
그래서 Spring은
TransactionSynchronizationManager
↓
ThreadLocal(Map)
↓
모든 리소스 관리
구조를 사용한다.
내부 구조
실제로는 단순한 Connection 하나가 아니다.
대략 다음과 같은 구조이다.
Thread-1
↓
ThreadLocal
↓
TransactionSynchronizationManager
↓
resources(Map)
↓
DataSource → Connection
SqlSessionFactory → SqlSession
EntityManagerFactory → EntityManager
즉,
Thread마다
하나의 리소스 저장소(Map)를 가진다.
그림으로 보면
Thread-1
↓
ThreadLocal
↓
──────────────────────────
resources
--------------------------
DataSource
↓
Connection
--------------------------
SqlSessionFactory
↓
SqlSession
--------------------------
EntityManagerFactory
↓
EntityManager
──────────────────────────
왜 Map일까?
예를 들어
하나의 요청에서
MyBatis
+
JPA
+
JdbcTemplate
를 함께 사용할 수도 있다.
Connection만 저장하면
Connection 하나
↓
끝
이다.
하지만 실제로는
Connection
SqlSession
EntityManager
Transaction 정보
Synchronization 정보
등을 함께 관리해야 한다.
그래서 Map 구조가 필요하다.
@Transactional 시작
Proxy가
@Transactional
을 발견한다.
실행 흐름
Proxy.invoke()
↓
TransactionManager.begin()
↓
Connection 생성
↓
TSM.bindResource()
↓
Real Service 실행
bindResource()
Spring 내부에서는
대략 이런 코드가 실행된다.
TransactionSynchronizationManager
.bindResource(
dataSource,
connection
);
결과
resources
--------------------------
DataSource
↓
Connection
--------------------------
이 저장된다.
Repository에서는?
Repository는
Connection을 전달받지 않는다.
jdbcTemplate.query(...);
또는
sqlSession.selectList(...);
만 호출한다.
그런데 내부에서는
DataSourceUtils.getConnection(dataSource);
가 실행된다.
그리고
TransactionSynchronizationManager
↓
resources 조회
↓
Connection 반환
을 수행한다.
Repository는
Connection을 몰라도
같은 Connection을 사용하게 된다.
전체 흐름
Controller
↓
Transaction Proxy
↓
TransactionManager.begin()
↓
TSM.bindResource()
↓
OrderService
↓
OrderRepository
↓
TSM.getResource()
↓
Connection 반환
↓
DB
JPA도 동일하다.
JPA는
Connection 대신
EntityManager를 사용한다.
하지만 구조는 같다.
TransactionSynchronizationManager
↓
EntityManagerFactory
↓
EntityManager
Service
↓
Repository
↓
EntityManager 조회
↓
동일 EntityManager 사용
MyBatis도 동일하다.
TransactionSynchronizationManager
↓
SqlSessionFactory
↓
SqlSession
결국
MyBatis
JPA
JdbcTemplate
모두
같은 방식으로 동작한다.
Synchronization은 무엇일까?
TSM 이름을 보면
Transaction
Synchronization
Manager
이다.
ResourceManager가 아니라
SynchronizationManager인 이유가 있다.
Transaction 종료 시
해야 할 일이 많다.
Commit
↓
Event 발생
↓
Connection Close
↓
ThreadLocal 제거
이 작업들을
순서대로 실행하기 위해
Synchronization 객체들을 등록한다.
registerSynchronization()
Spring 내부에서는
TransactionSynchronizationManager
.registerSynchronization(...);
를 수행한다.
Commit 시
beforeCommit()
↓
afterCommit()
↓
afterCompletion()
이 자동으로 호출된다.
@TransactionalEventListener도 여기서 동작한다.
@TransactionalEventListener
public void sendMail(...) {
}
이 이벤트는
메서드가 끝나는 즉시 실행되지 않는다.
Commit 성공
↓
afterCommit()
↓
Event 실행
이다.
이 역시
TSM의 Synchronization 기능이다.
ThreadLocal과 비교
우리가 직접 사용한다면
ThreadLocal<Connection>
이다.
Spring은
ThreadLocal
↓
TransactionSynchronizationManager
↓
Map
↓
Connection
SqlSession
EntityManager
Synchronization
를 사용한다.
훨씬 확장성이 좋다.
Thread 종료
Request 종료
↓
Commit
↓
Connection Close
↓
TSM.unbindResource()
↓
ThreadLocal.remove()
모든 리소스가 정리된다.
remove가 중요한 이유
Tomcat은
ThreadPool을 사용한다.
Request1
↓
Thread-1
↓
Connection 저장
정리하지 않으면
Request2
↓
Thread-1 재사용
↓
이전 Connection 사용
그래서 Spring은
항상
try {
...
}
finally {
TransactionSynchronizationManager
.clear();
}
패턴으로 동작한다.
이전 글들과 연결
지금까지의 흐름을 모두 연결하면
HTTP Request
↓
Security Filter
↓
ThreadLocal(Authentication)
↓
Controller
↓
Transaction Proxy
↓
TransactionManager
↓
TransactionSynchronizationManager
↓
ThreadLocal(Map)
↓
Connection
SqlSession
EntityManager
↓
Repository
↓
DB
↓
Commit
↓
Synchronization 실행
↓
ThreadLocal.clear()
↓
Response
Spring 내부에서 TSM을 사용하는 대표 기능
Transaction
↓
Connection 공유
----------------
JPA
↓
EntityManager 공유
----------------
MyBatis
↓
SqlSession 공유
----------------
@TransactionalEventListener
↓
afterCommit 실행
----------------
Synchronization Callback
실무에서는 직접 사용할까?
거의 사용하지 않는다.
하지만
TransactionSynchronizationManager
.isActualTransactionActive();
또는
TransactionSynchronizationManager
.getResource(...)
처럼
프레임워크 개발이나 라이브러리 개발에서는 자주 등장한다.
면접 포인트
TransactionSynchronizationManager란?
Spring이 트랜잭션과 관련된 리소스(Connection, EntityManager, SqlSession 등)를 ThreadLocal 기반으로 중앙 관리하는 클래스이다.
왜 ThreadLocal 하나가 아니라 Map을 사용하는가?
하나의 요청에서 Connection뿐 아니라 EntityManager, SqlSession, Synchronization 등 여러 리소스를 함께 관리해야 하기 때문이다.
Repository는 어떻게 같은 Connection을 사용하는가?
DataSourceUtils.getConnection()이 TransactionSynchronizationManager에서 현재 Thread에 바인딩된 Connection을 조회하기 때문이다.
@TransactionalEventListener는 언제 실행되는가?
TransactionSynchronizationManager에 등록된 Synchronization이 Commit 이후(afterCommit)에 실행하면서 호출된다.
다음 글 추천
이제 자연스럽게 ExecutorService와 @Async로 넘어가는 것을 추천한다.
그 이유는 바로 지금까지 배운 ThreadLocal의 한계를 보여줄 수 있기 때문이다.
Controller(Thread-1)
↓
@Transactional
↓
ThreadLocal(Connection)
↓
@Async
↓
Thread-2
↓
★★★★★ ThreadLocal이 사라진다 ★★★★★
즉,
왜 @Async 안에서 SecurityContext, Transaction, ThreadLocal이 전파되지 않는지,
그리고 Spring이 이를 어떻게 해결하는지까지 연결하면 독자가 Spring 내부를 훨씬 깊게 이해하게 된다.
'language > java' 카테고리의 다른 글
| ExecutorService와 @Async (0) | 2026.06.24 |
|---|---|
| ThreadLocal과 Spring Security (0) | 2026.06.24 |
| Java Proxy 기반 트랜잭션 처리 (0) | 2026.06.17 |
| JDK Proxy vs CGLIB in Spring (0) | 2026.06.17 |
| Dynamic Proxy와 Spring AOP (0) | 2026.06.17 |
댓글