소스:
spring-tx/src/main/java/org/springframework/transaction/support/핵심 클래스:AbstractPlatformTransactionManager,TransactionSynchronizationManager,TransactionSynchronization,DefaultTransactionStatus
1장의 PlatformTransactionManager는 인터페이스일 뿐이다. "전파에 따라 트랜잭션을 합류시키거나 중단·재개하고, 커밋 직전 콜백을 돌리고, 롤백 전용 마킹을 점검한다" 같은 모든 자원에 공통인 복잡한 흐름은 누군가 한 번만 구현하면 된다. 그 한 곳이 AbstractPlatformTransactionManager(이하 APTM)다.
APTM은 getTransaction/commit/rollback을 final로 구현해 흐름을 고정하고, 자원에 의존하는 부분(begin/commit/rollback/suspend/resume 등)만 doXxx 템플릿 메서드로 남긴다. DataSourceTransactionManager(spring-jdbc), JpaTransactionManager(spring-orm), JtaTransactionManager(이 모듈)는 모두 이 골격을 상속하고 자원별 doBegin/doCommit만 채운다. 즉 전파·중첩·동기화 로직은 단 한 벌만 존재한다. 이 장이 그 한 벌을 해부한다.
PlatformTransactionManager ConfigurableTransactionManager
└──────────┬───────────────────┘
▼
AbstractPlatformTransactionManager (추상)
┌───────────────────────────────────────────────────────────┐
│ final getTransaction() / commit() / rollback() ← 흐름 고정 │
│ private handleExistingTransaction(), processCommit(), ... │
│ protected suspend()/resume() ← 동기화·자원 중단/재개 │
├───────────────────────────────────────────────────────────┤
│ 템플릿 메서드 (하위 클래스가 채움): │
│ abstract doGetTransaction() doBegin() doCommit() doRollback() │
│ hook isExistingTransaction() doSuspend() doResume() │
│ doSetRollbackOnly() doCleanupAfterCompletion() ... │
└───────────────────────────────────────────────────────────┘
│ 사용
┌──────────────┼──────────────────┐
▼ ▼ ▼
DefaultTransactionStatus TransactionSynchronizationManager TransactionSynchronization
(진행 상태 + 중단자원 홀더) (스레드 바운드 자원/동기화 레지스트리) (커밋 전후 콜백)
APTM의 동반자. transaction/support/TransactionSynchronizationManager.java는 static 메서드만 가진 유틸로, 여러 개의 ThreadLocal을 관리한다.
private static final ThreadLocal<Map<Object,Object>> resources; // 자원 핸들(키=팩토리)
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations;
private static final ThreadLocal<String> currentTransactionName;
private static final ThreadLocal<Boolean> currentTransactionReadOnly;
private static final ThreadLocal<Integer> currentTransactionIsolationLevel;
private static final ThreadLocal<Boolean> actualTransactionActive;두 가지 책임을 한다.
- 자원 바인딩 —
bindResource(key, value)로 현재 스레드에 자원을 묶는다. 보통 키는DataSource, 값은 그 트랜잭션의Connection. 같은 스레드에서getResource(dataSource)를 부르면 항상 같은 커넥션이 나온다. 이것이 "같은 트랜잭션 안에서는 같은 커넥션을 공유"하는 메커니즘의 핵심이다.DataSourceUtils.getConnection()이 내부에서 이걸 조회한다. - 동기화 등록 — 동기화가 활성일 때(
isSynchronizationActive()) 자원 관리 코드가registerSynchronization(sync)로 콜백을 등록한다. 트랜잭션 완료 시 APTM이 이 콜백들을 일괄 실행한다.
initSynchronization()/clearSynchronization()은 트랜잭션 매니저만 호출한다(begin 시 활성화, cleanup 시 비활성화). 일반 애플리케이션 코드는 건드리지 않는다.
transaction/support/TransactionSynchronization.java. Ordered와 Flushable을 상속하고 전부 default 메서드라 필요한 것만 오버라이드한다.
트랜잭션 생명주기에서 호출되는 순서:
suspend()/resume() ─ 중단·재개 시
beforeCommit(readOnly) ─ 커밋 직전 (롤백 결정 아직 가능, 여기서 세션 flush)
beforeCompletion() ─ 커밋/롤백 직전, 예외 던져도 무시(자원 닫기)
afterCommit() ─ 커밋 성공 직후
afterCompletion(status)─ 완료 후(커밋/롤백 무관), STATUS_COMMITTED/ROLLED_BACK/UNKNOWN
대표 용도: JDBC/Hibernate가 트랜잭션에 묶은 커넥션·세션을 afterCompletion에서 닫는다. @TransactionalEventListener(6장)도 내부적으로 이 콜백을 등록해 "커밋 후 이벤트 발행"을 구현한다.
getTransaction()은 final이며 전체가 전파 규칙의 분기다. 먼저 doGetTransaction()(하위 클래스)으로 현재 트랜잭션 객체를 얻고, isExistingTransaction()으로 "이미 진행 중인가"를 묻는다.
getTransaction(def)
│
├─ doGetTransaction() → 자원별 트랜잭션 객체
│
├─ isExistingTransaction(tx)?
│ │
│ ├─ 예 ──▶ handleExistingTransaction(def, tx) [아래 흐름 2]
│ │
│ └─ 아니오 ── 진행 중 트랜잭션 없음 ──┐
│ ▼
│ propagation == MANDATORY → IllegalTransactionStateException ("없는데 MANDATORY")
│ propagation == REQUIRED
│ | REQUIRES_NEW
│ | NESTED → suspend(null) 후 startTransaction(): 새 트랜잭션 시작
│ 그 외(SUPPORTS/NOT_SUPPORTED/NEVER)
│ → "빈 트랜잭션": 실제 tx 없이 동기화만(설정에 따라)
└─
새 트랜잭션을 실제로 시작하는 startTransaction()의 알맹이:
private TransactionStatus startTransaction(definition, transaction, nested, debug, suspendedResources) {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, ...);
this.transactionExecutionListeners.forEach(l -> l.beforeBegin(status));
try {
doBegin(transaction, definition); // ← 자원별: setAutoCommit(false) 등
} catch (RuntimeException | Error ex) {
this.transactionExecutionListeners.forEach(l -> l.afterBegin(status, ex));
throw ex;
}
prepareSynchronization(status, definition); // ← TSM에 이름/격리/readOnly/활성 노출 + initSynchronization
this.transactionExecutionListeners.forEach(l -> l.afterBegin(status, null));
return status;
}prepareSynchronization()이 TransactionSynchronizationManager에 현재 트랜잭션의 메타데이터(이름·격리·읽기전용·활성 여부)를 노출하고 동기화를 활성화한다. 이때부터 같은 스레드의 자원 관리 코드가 TSM.isActualTransactionActive()/isSynchronizationActive()로 트랜잭션 존재를 감지할 수 있다.
여기가 전파의 진짜 묘미다. 진행 중 트랜잭션이 있을 때 전파별로 다르게 행동한다.
handleExistingTransaction(def, tx)
│
├ NEVER → IllegalTransactionStateException ("있는데 NEVER")
│
├ NOT_SUPPORTED → suspend(tx) : 기존 트랜잭션 중단, 비트랜잭션으로 실행
│
├ REQUIRES_NEW → suspend(tx) : 기존 중단 후 startTransaction(): 완전히 새 트랜잭션
│ 완료 시 cleanupAfterCompletion에서 기존 트랜잭션 resume()
│
├ NESTED → nestedTransactionAllowed 아니면 NestedTransactionNotSupportedException
│ useSavepointForNestedTransaction()==true (기본):
│ status.createAndHoldSavepoint() ← JDBC 세이브포인트, 새 동기화 안 만듦
│ false (주로 JTA):
│ startTransaction(nested=true) ← 중첩 begin
│
└ REQUIRED / SUPPORTS / MANDATORY → 기존 트랜잭션에 "합류"
(validateExistingTransaction==true면 격리/readOnly 호환성 검증)
prepareTransactionStatus(newTransaction=false): 새 status지만 실제 begin 없음
합류(participation)의 의미가 중요하다. REQUIRED로 들어온 안쪽 호출이 이미 트랜잭션이 있으면 newTransaction=false인 DefaultTransactionStatus를 받는다. 이 상태로 나중에 commit해도 processCommit은 "새 트랜잭션이 아니다"를 보고 실제 커밋을 생략한다 — 진짜 커밋은 맨 바깥 트랜잭션이 한다. 반대로 안쪽이 rollback하면 실제 롤백 대신 기존 트랜잭션을 롤백 전용으로 마킹한다(doSetRollbackOnly). 이래서 안쪽 메서드 하나가 실패하면 바깥 전체가 롤백되는 것이다.
suspend(transaction)은 두 가지를 백업한다. (1) 현재 등록된 동기화 목록을 suspend() 호출 후 떼어내고, (2) 자원은 doSuspend()(하위 클래스)로 떼어낸 뒤, 이름·readOnly·격리·활성 플래그까지 SuspendedResourcesHolder에 담아 반환한다. 이 홀더는 새 TransactionStatus에 보관되고, 완료 시 cleanupAfterCompletion()이 resume()으로 전부 복원한다. 덕분에 REQUIRES_NEW 안쪽이 끝나면 바깥 트랜잭션과 그 자원이 정확히 되살아난다.
commit()도 final. 먼저 가드를 친다.
public final void commit(TransactionStatus status) {
if (status.isCompleted()) throw new IllegalTransactionStateException("이미 완료됨");
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) { // 코드가 setRollbackOnly() 호출함
processRollback(defStatus, false); // → 커밋 대신 롤백
return;
}
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
processRollback(defStatus, true); // 전역 롤백 마킹됨 → 롤백 + unexpected
return;
}
processCommit(defStatus);
}processCommit()의 콜백 순서(정상 경로):
prepareForCommit(status)
triggerBeforeCommit(status) → TransactionSynchronization.beforeCommit(readOnly)
triggerBeforeCompletion(status) → TransactionSynchronization.beforeCompletion()
├ status.hasSavepoint()? → releaseHeldSavepoint() (NESTED 세이브포인트)
├ status.isNewTransaction()? → doCommit(status) (실제 자원 커밋)
└ 합류 상태면 실제 커밋 생략
unexpectedRollback 이면 → UnexpectedRollbackException
triggerAfterCommit(status) → TransactionSynchronization.afterCommit()
triggerAfterCompletion(STATUS_COMMITTED) → afterCompletion(0)
finally: cleanupAfterCompletion(status) ← 동기화 정리, 자원 정리, 중단 자원 resume
triggerAfterCommit에서 예외가 나도 트랜잭션은 이미 커밋된 것으로 간주하고 호출자에 예외만 전파한다(finally에서 afterCompletion은 반드시 실행).
processRollback()의 핵심 분기:
triggerBeforeCompletion(status)
├ hasSavepoint()? → rollbackToHeldSavepoint() (NESTED: 세이브포인트로 롤백)
├ isNewTransaction()? → doRollback(status) (실제 자원 롤백)
└ 합류 상태:
isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()(기본 true)
→ doSetRollbackOnly(status) ← 바깥 트랜잭션을 롤백 전용으로 마킹
아니면 → "originator가 롤백 결정하도록" 아무것도 안 함
triggerAfterCompletion(STATUS_ROLLED_BACK)
finally: cleanupAfterCompletion(status)
여기서 globalRollbackOnParticipationFailure(기본 true)가 등장한다. 합류한 안쪽이 실패하면 기본적으로 전역 롤백 전용으로 마킹하므로, 바깥쪽이 나중에 커밋을 시도하면 commit()이 isGlobalRollbackOnly()를 감지해 롤백하고 UnexpectedRollbackException을 던진다. 이게 "안쪽에서 예외를 잡아 삼켰는데도 전체가 롤백되더라"의 정체다. NESTED(세이브포인트)를 쓰면 안쪽만 세이브포인트로 되돌릴 수 있어 이 문제를 피한다.
doGetTransaction()(abstract) — 자원별 트랜잭션 객체 생성. 진행 중 트랜잭션 정보를 담아 반환.isExistingTransaction()(hook) — 합류 가능 여부 판정. 기본false(합류 미지원).doBegin()(abstract) — 실제 트랜잭션 시작(예:Connection.setAutoCommit(false)). 전파는 신경 쓸 필요 없음(이미 APTM이 처리).doCommit()/doRollback()(abstract) — 새 트랜잭션일 때만 호출됨. 합류·롤백전용 판단은 이미 끝난 상태.doSetRollbackOnly()(hook) — 합류 상태에서 롤백 시 바깥 트랜잭션 마킹. 합류 지원 매니저는 반드시 구현.suspend()/resume()(final) — 동기화+자원을 통째로 백업/복원.REQUIRES_NEW/NOT_SUPPORTED의 토대.useSavepointForNestedTransaction()(hook) — NESTED를 세이브포인트로 구현할지(기본 true, JDBC) begin/commit으로 구현할지(JTA).prepareSynchronization()—TransactionSynchronizationManager에 트랜잭션 메타데이터 노출 + 동기화 활성화.
- 템플릿 메서드 패턴 — 흐름(
getTransaction/commit/rollback)은final로 고정, 변하는 부분만doXxx로 위임. 자원별 매니저는 보통 5~6개 메서드만 채우면 된다. - 동작 튜닝 플래그 —
nestedTransactionAllowed,validateExistingTransaction,globalRollbackOnParticipationFailure,failEarlyOnGlobalRollbackOnly,rollbackOnCommitFailure,transactionSynchronization(ALWAYS/ON_ACTUAL_TRANSACTION/NEVER). 각 매니저가 자기 자원에 맞는 기본값을 세팅한다. ConfigurableTransactionManager+TransactionExecutionListener— begin/commit/rollback 시점을 관찰하는 훅(6.1+). APTM이beforeBegin/afterBegin/beforeCommit/afterCommit/beforeRollback/afterRollback을 호출해준다. 모니터링·메트릭에 사용.- 직렬화 가능 — APTM 상태가 직렬화되어, 트랜잭션 인터셉터를 품은 프록시와 함께 직렬화될 수 있다(
readObject로 transientlogger복구).
AbstractPlatformTransactionManager는 Spring 트랜잭션의 심장이다. 전파 7종 판정, 중단·재개, 세이브포인트 기반 중첩, 합류 시 커밋 생략과 롤백 마킹, 커밋 전후 동기화 콜백 — 이 모든 공통 흐름을 단 한 벌로 구현하고, 자원에 닿는 부분만 doBegin/doCommit/doRollback으로 남긴다. 그 동반자인 TransactionSynchronizationManager는 스레드별로 자원과 동기화를 보관해 "같은 트랜잭션 = 같은 커넥션"과 "완료 시 자원 정리"를 가능케 한다. 다음 장부터는 이 엔진을 누가 호출하는지 — 선언적 트랜잭션(@Transactional) 인프라를 본다.