@Transactional이 실제로 어떤 흐름을 가지고 동작하는지 디버깅하면서 확인한 내용을 정리한 글입니다.
실험 코드
간단하게 실험해볼 코드는 아래와 같습니다.
@Service
public class TestService {
@Transactional
public void doSomething() {
System.out.println("Let's go");
}
}
다음과 같은 테스트 코드를 통해 @Transactional의 실제 동작 순서를 알아보겠습니다.
#1. Service 클래스를 Proxy객체로
브레이크 포인트를 지정하고, 테스트코드를 돌리면 TestService 클래스가 CGLIB 프록시 객체로 만들어집니다.
Spring AOP에 의해서 프록시에 감싸지고, 이 프록시에는 트랜잭션을 적용하기 위한 Advisor가 포함됩니다.
Advisor는 내부적으로 TransactionInterceptor를 Advice로 가지고 있어서 실제 트랜잭션의 시작, 커밋, 롤백은 모두 TransactionaInterceptor에서 처리됩니다.
디버깅 화면을 보면, 프록시 객체 내부의 advisors 항목에 BeanFactoryTransactionAttributeSourceAdvice가 포함되어 있고,
이 어드바이저는 TransactionInterceptor를 트랜잭션 관련 Advice로 갖고 있음을 확인할 수 있습니다.
#2. TransactionInterceptor 클래스의 invoke 메서드 호출
다음 단계를 디버깅으로 확인해보니, TransactionInterceptor의 invoke 메서드를 호출하게 됩니다.
#3. invokeWithinTransaction 메서드 호출
invoke 메서드 내부의 invokeWithTransaction 메서드를 호출합니다. 내부 코드가 너무 길어서 일부만 캡쳐했습니다.
invokeWithinTransaction 메서드의 흐름은 아래와 같습니다.
1. 트랜잭션 메타 정보 가져오기
TransactionAttributeSource tas = this.getTransactionAttributeSource();
TransactionAttribute txAttr = tas != null ? tas.getTransactionAttribute(method, targetClass) : null;
TransactionManager tm = this.determineTransactionManager(txAttr, targetClass);
- @Transactional의 속성(propagation, isolation, rollbackFor 등)을 파싱해서 txAttr에 담습니다.
- 어떠한 트랜잭션 매니저(DataSourceTransactionManager, JpaTransactionManager 등)을 결정합니다.
2. Reactive 트랜잭션 처리
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
...
return txSupport.invokeWithinTransaction(...);
}
- Mono, Flux, suspend fun 등 리액티브/코루틴 기반이면 조건문 내부로 분기처리 됩니다.
- 그렇지 않은 경우 else문을 통해 전통적인 트랜잭션 방식으로 분기처리 됩니다.
3. 일반적인 트랜잭션 처리(PlatFormTransactionManager 기반)
PlatformTransactionManager ptm = this.asPlatformTransactionManager(tm);
String joinpointIdentification = this.methodIdentification(method, targetClass, txAttr);
- 트랜잭션 매니저를 PlatformTransactionManger로 변환합니다.
4. Callback 방식 트랜잭션 처리
if (txAttr != null && ptm instanceof CallbackPreferringPlatformTransactionManager cpptm) {
result = cpptm.execute(txAttr, (status) -> {
...
});
}
- 트랜잭션 매니저가 콜백 기반을 선호하는 경우엔 이 방식을 사용하게 됩니다.
5. 일반 트랜잭션 처리
콜백 기반을 선호하지 않는 경우엔 else문을 통해 일반 트랜잭션 처리를 하게 됩니다. 실제 코드는 훨씬 길기 때문에, 내부의 핵심 흐름만 보겠습니다.
TransactionInfo txInfo = this.createTransactionIfNecessary(...);
try {
retVal = invocation.proceedWithInvocation(); // 실제 비즈니스 메서드 실행
} catch (Throwable ex) {
this.completeTransactionAfterThrowing(txInfo, ex); // 롤백 처리
throw ex;
} finally {
this.cleanupTransactionInfo(txInfo); // 상태 초기화
}
this.commitTransactionAfterReturning(txInfo); // 커밋 처리
- createTransactionIfNecessary를 통해서 트랜잭션을 시작합니다.
- proceedWithInvocation로 실제 메서드를 호출합니다.
- completeTransactionAfterThrowing()로 예외가 발생하는 경우 트랜잭션을 롤백합니다.
- commitTransactionAfterReturning()로 성공하는 경우엔 트랜잭션을 커밋합니다.
- cleanupTransactionInfo()로 트랜잭션 관련 정보를 초기화합니다.
정리
1. AOP Proxy 생성
2. Proxy → TransactionInterceptor 호출
3. invokeWithinTransaction 호출
4. 트랜잭션 속성 조회 및 트랜잭션 매니저 선택
5. 트랜잭션 시작 → 비즈니스 로직 실행 → 커밋 or 롤백
6. 트랜잭션 관련 정보 초기화
'Spring' 카테고리의 다른 글
[Spring DB] Connection Pool (0) | 2025.04.25 |
---|---|
[Spring Docs] DispatcherServlet #2 (0) | 2025.03.16 |
빈 후처리 (0) | 2025.03.13 |
어드바이저 (0) | 2025.03.12 |
프록시 팩토리 (0) | 2025.03.11 |