이 글은 Spring 전체 챕터에서 가장 중요한 글이라고 생각한다.
왜냐하면 지금까지 배운 모든 개념이 여기서 하나로 연결되기 때문이다.
Reflection
↓
Bean 생성
↓
DI
↓
BeanPostProcessor
↓
Dynamic Proxy
↓
JDK Proxy / CGLIB
↓
★★★★★ @Transactional ★★★★★
↓
ThreadLocal
↓
Commit / Rollback
많은 사람들이 @Transactional을
"트랜잭션 걸어주는 Annotation"
정도로만 알고 있다.
하지만 실제로는 Spring + Java의 여러 기술이 함께 동작하는 결과물이다.
Java Proxy 기반 트랜잭션 처리
@Transactional 한 줄 뒤에서는 무슨 일이 일어날까?
다음 코드를 보자.
@Service
public class OrderService {
@Transactional
public void order() {
orderRepository.save();
paymentRepository.save();
}
}
우리는 단지
orderService.order();
를 호출했을 뿐이다.
그런데
Transaction Begin
↓
Order 저장
↓
Payment 저장
↓
Commit
이 자동으로 수행된다.
질문을 바꿔보자.
begin()은 누가 호출했을까?
commit()은 누가 호출했을까?
Connection은 어디에 저장되어 있을까?
만약 Spring이 없다면
개발자가 직접 작성해야 한다.
public void order() {
Connection connection = dataSource.getConnection();
try {
connection.setAutoCommit(false);
orderRepository.save(connection);
paymentRepository.save(connection);
connection.commit();
} catch (Exception e) {
connection.rollback();
} finally {
connection.close();
}
}
모든 Service마다
이 코드를 반복해야 한다.
Spring은 어떻게 해결했을까?
Spring은
비즈니스 코드와
트랜잭션 코드를 분리했다.
비즈니스 코드
@Transactional
public void order() {
orderRepository.save();
paymentRepository.save();
}
트랜잭션은
Proxy가 담당한다.
전체 흐름
가장 먼저 전체 그림을 보자.
Controller
↓
Transaction Proxy
↓
TransactionManager.begin()
↓
Connection 생성
↓
ThreadLocal 저장
↓
Real Service
↓
Repository
↓
ThreadLocal에서 Connection 조회
↓
Commit
↓
ThreadLocal 제거
1단계
Controller 호출
orderService.order();
많은 사람들이
Controller
↓
OrderService
라고 생각한다.
실제로는
Controller
↓
Transaction Proxy
↓
Real OrderService
이다.
2단계
Proxy가 먼저 실행된다.
Proxy는
메서드 호출을 가로챈다.
Proxy.invoke()
↓
Transaction Begin
↓
Real Method 실행
↓
Commit
여기까지가
이전 글에서 배운
Dynamic Proxy이다.
3단계
TransactionManager가 등장한다.
Proxy는
PlatformTransactionManager에게 요청한다.
transactionManager.getTransaction(...);
TransactionManager는
Connection 생성
↓
AutoCommit(false)
↓
Transaction 시작
을 수행한다.
4단계
Connection을 어디에 보관할까?
가장 중요한 부분이다.
Service
↓
Repository
↓
DAO
모두 같은 Connection을 사용해야 한다.
그렇다면
매번
save(connection);
처럼 전달해야 할까?
Spring은 그렇게 하지 않는다.
ThreadLocal 등장
TransactionManager는
Connection을
ThreadLocal에 저장한다.
Thread-1
↓
ThreadLocalMap
↓
Connection
이제
현재 Thread는
Connection을 하나 공유한다.
Repository에서는
orderRepository.save();
만 호출한다.
하지만 내부에서는
DataSourceUtils.getConnection();
이 실행된다.
그리고
현재 Thread
↓
ThreadLocal
↓
Connection 반환
을 수행한다.
Repository는
Connection을 전달받지 않았는데도
같은 Connection을 사용하게 된다.
그림으로 이해
Controller
↓
Transaction Proxy
↓
ThreadLocal(Connection 저장)
↓
OrderService
↓
OrderRepository
↓
ThreadLocal(Connection 조회)
↓
PaymentRepository
↓
ThreadLocal(Connection 조회)
두 Repository 모두
같은 Connection을 사용한다.
Commit은 언제 할까?
Real Service가
정상 종료되면
Proxy가 다시 실행된다.
Real Method 종료
↓
Commit
↓
Connection Close
↓
ThreadLocal.remove()
예외가 발생하면?
@Transactional
public void order() {
orderRepository.save();
throw new RuntimeException();
}
실행
Begin
↓
save()
↓
Exception
↓
Rollback
↓
Connection Close
↓
ThreadLocal.remove()
비즈니스 코드에는
rollback 코드가 하나도 없다.
왜 ThreadLocal.remove()가 중요할까?
Tomcat은
ThreadPool을 사용한다.
Request1
↓
Thread-1
↓
Connection 저장
↓
Request 종료
remove()를 하지 않으면
Thread-1
↓
이전 Connection 유지
다음 요청
Request2
↓
Thread-1 재사용
↓
이전 Connection 사용
심각한 버그가 발생한다.
그래서
Spring 내부는 항상
try {
begin();
...
} finally {
ThreadLocal.remove();
}
패턴으로 작성되어 있다.
실제 내부 흐름
Client
↓
Proxy.invoke()
↓
TransactionManager.begin()
↓
Connection 생성
↓
ThreadLocal 저장
↓
Real Service
↓
Repository
↓
ThreadLocal에서 Connection 조회
↓
Commit 또는 Rollback
↓
Connection Close
↓
ThreadLocal.remove()
이전 글들과 연결
지금까지 공부한 내용을 연결하면
Reflection
↓
Bean 생성
↓
DI
↓
BeanPostProcessor
↓
Dynamic Proxy 생성
↓
@Transactional 발견
↓
Transaction Proxy 생성
↓
Container 저장
↓
Controller 호출
↓
Proxy.invoke()
↓
TransactionManager
↓
ThreadLocal(Connection)
↓
Repository
↓
Commit / Rollback
↓
ThreadLocal.remove()
이것이 Spring Transaction의 전체 구조이다.
왜 @Transactional 내부 호출은 동작하지 않을까?
@Service
public class OrderService {
public void save() {
order();
}
@Transactional
public void order() {
}
}
실행
save()
↓
this.order()
↓
Real Object 직접 호출
↓
Proxy 안 거침
↓
Transaction 시작 안 됨
이 질문은
Spring 면접에서 가장 많이 등장한다.
실무에서 많이 사용하는 구조
Controller
↓
@Transactional Service
↓
Repository
↓
JDBC / JPA
↓
Database
Transaction은
Service 계층에 두는 것이 일반적이다.
면접 포인트
@Transactional은 어떻게 동작하는가?
Bean 생성 이후 BeanPostProcessor가 @Transactional을 발견하면 Transaction Proxy를 생성한다. 실제 메서드 호출은 Proxy를 통해 이루어지며, Proxy는 TransactionManager를 이용해 트랜잭션을 시작하고 Connection을 ThreadLocal에 저장한 후 실제 Service를 실행한다. 실행이 끝나면 Commit 또는 Rollback을 수행하고 마지막에 ThreadLocal을 정리한다.
Repository는 Connection을 어떻게 공유하는가?
Connection을 파라미터로 전달하는 것이 아니라 ThreadLocal에 저장하고, DataSourceUtils.getConnection()을 통해 현재 Thread의 Connection을 조회한다.
왜 ThreadLocal.remove()가 중요한가?
ThreadPool 환경에서는 Thread가 재사용되므로 remove를 하지 않으면 이전 요청의 Connection이 남아 Memory Leak이나 잘못된 트랜잭션 공유 문제가 발생할 수 있다.
다음 글 추천
다음은 ThreadLocal과 Spring Security를 추천한다.
이 글은 지금까지 배운 ThreadLocal을 활용하여
로그인
↓
Authentication 생성
↓
SecurityContextHolder
↓
ThreadLocal 저장
↓
Controller 어디서든 로그인 사용자 조회
↓
응답 종료
↓
ThreadLocal.remove()
과정을 설명할 수 있다.
그리고 이후 TransactionSynchronizationManager 글과도 자연스럽게 이어져,
Spring 내부에서 ThreadLocal이 얼마나 핵심적인 기술인지 독자가 체감할 수 있는 흐름이 완성된다.
'language > java' 카테고리의 다른 글
| ThreadLocal과 Spring Security (0) | 2026.06.24 |
|---|---|
| ThreadLocal과 TransactionSynchronizationManager (0) | 2026.06.24 |
| JDK Proxy vs CGLIB in Spring (0) | 2026.06.17 |
| Dynamic Proxy와 Spring AOP (0) | 2026.06.17 |
| 객체 생명주기와 Spring Bean (0) | 2026.06.17 |
댓글