소스:
spring-tx/src/main/java/org/springframework/transaction/support/(Template) 및.../transaction/reactive/핵심:TransactionTemplate,TransactionOperations,TransactionCallback,ReactiveTransactionManager,AbstractReactiveTransactionManager,TransactionalOperator,TransactionContext(Manager)
선언적 트랜잭션(3·4장)은 편하지만 두 가지 한계가 있다. (1) 메서드 단위 경계라서 "한 메서드 안에서 일부 구간만 트랜잭션"이 어렵고, (2) 프록시 기반이라 동적으로 경계를 제어하기 까다롭다. 프로그래밍적 트랜잭션은 코드로 직접 execute(...) 블록을 감싸 이 유연성을 준다.
또한 명령형 트랜잭션은 ThreadLocal(2장의 TransactionSynchronizationManager)에 의존한다. WebFlux/R2DBC 같은 리액티브 스택에서는 한 요청이 여러 스레드를 오가므로 ThreadLocal이 통하지 않는다. 리액티브 트랜잭션은 스레드로컬 대신 **Reactor Context**에 트랜잭션 상태를 실어 이 문제를 푼다.
명령형 프로그래밍적:
TransactionOperations (execute / executeWithoutResult / withoutTransaction)
▲
TransactionTemplate extends DefaultTransactionDefinition
( = TransactionDefinition + PlatformTransactionManager 결합 )
execute(TransactionCallback<T>) : T
리액티브:
TransactionManager
▲
ReactiveTransactionManager getReactiveTransaction(def):Mono<RTx> commit/rollback:Mono<Void>
▲
AbstractReactiveTransactionManager ── 명령형 APTM의 리액티브 쌍둥이(전파/동기화 골격)
│ 사용
▼
TransactionalOperator (execute / transactional) ← 프로그래밍적 리액티브
TransactionContext / TransactionContextHolder / TransactionContextManager
└ Reactor Context 에 트랜잭션 상태 보관 (ThreadLocal 대체)
TransactionSynchronizationManager(reactive) ← Context 기반 자원 바인딩
TransactionTemplate은 DefaultTransactionDefinition을 상속한다. 즉 자기 자신이 트랜잭션 정의다(전파·격리 등을 빈 프로퍼티로 설정). 동시에 PlatformTransactionManager를 들고 있어 정의+실행을 한 객체로 합쳤다.
public class TransactionTemplate extends DefaultTransactionDefinition
implements TransactionOperations, InitializingBean {
private @Nullable PlatformTransactionManager transactionManager;
...
}핵심 execute()가 2장의 매니저를 직접 호출하며 try/catch로 경계를 친다.
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager cpptm) {
return cpptm.execute(this, action); // JTA CMT 등 콜백 선호 매니저
}
TransactionStatus status = this.transactionManager.getTransaction(this); // this=정의
T result;
try {
result = action.doInTransaction(status); // 사용자 콜백
} catch (RuntimeException | Error ex) {
rollbackOnException(status, ex); // 예외 → 롤백
throw ex;
} catch (Throwable ex) {
rollbackOnException(status, ex); // 체크 예외도 롤백!
throw new UndeclaredThrowableException(ex, ...);
}
this.transactionManager.commit(status); // 정상 → 커밋
return result;
}3장의 선언적 트랜잭션과 롤백 정책이 다르다: TransactionTemplate은 콜백이 던지는 모든 Throwable(체크 예외 포함)에 롤백한다. 반면 @Transactional 기본은 언체크/Error만 롤백한다. DefaultTransactionAttribute.rollbackOn JavaDoc도 이 차이를 명시한다. 콜백 안에서 예외 없이 롤백하려면 status.setRollbackOnly()를 쓴다.
rollbackOnException()은 롤백 중 발생한 TransactionSystemException에 원래 애플리케이션 예외를 initApplicationException()으로 실어 "롤백 예외에 가려진 원인"을 보존한다 — 3장 인터셉터와 동일한 패턴.
TransactionTemplate이 구현하는 인터페이스. execute(callback)과 람다 친화적 executeWithoutResult(Consumer<TransactionStatus>)을 제공한다. 두 가지 정적/유틸이 유용하다.
TransactionCallbackWithoutResult— 반환값 없는 콜백용 추상 클래스.TransactionOperations.withoutTransaction()— 실제 트랜잭션 없이 콜백을 그냥 실행하는 구현(WithoutTransactionOperations). 테스트에서 매니저 없이 코드를 돌릴 때 유용(동작상PROPAGATION_SUPPORTS+SYNCHRONIZATION_NEVER와 동등).
1장에서 봤듯 ReactiveTransactionManager는 명령형의 거울상이되 모든 결과가 Mono다.
Mono<ReactiveTransaction> getReactiveTransaction(@Nullable TransactionDefinition definition);
Mono<Void> commit(ReactiveTransaction transaction);
Mono<Void> rollback(ReactiveTransaction transaction);결정적 차이: 예외가 throw가 아니라 리액티브 파이프라인을 통해 전파된다. 그래서 호출자는 subscribe 시점에 에러 시그널로 받는다. AbstractReactiveTransactionManager는 AbstractPlatformTransactionManager와 똑같은 워크플로(전파 판정, suspend/resume, 롤백전용 점검, 동기화 콜백)를 리액티브 버전으로 구현하고, doBegin/doCommit 등 템플릿 메서드를 Mono 반환으로 남긴다. R2dbcTransactionManager(spring-r2dbc) 등이 이를 상속한다.
명령형이 TransactionSynchronizationManager의 ThreadLocal에 자원/동기화/메타데이터를 담았다면, 리액티브는 TransactionContext 객체에 같은 정보를 담고 그 객체를 **Reactor Context**에 싣는다. TransactionContextManager가 이 컨텍스트를 등록/조회하는 정적 헬퍼다.
public static Mono<TransactionContext> currentContext() {
return Mono.deferContextual(ctx -> {
if (ctx.hasKey(TransactionContext.class)) return Mono.just(ctx.get(TransactionContext.class));
if (ctx.hasKey(TransactionContextHolder.class)) { ... }
return Mono.error(new NoTransactionInContextException());
});
}TransactionContextHolder가 필요한 이유가 미묘하다. Reactor의 구독자 컨텍스트는 **아래→위(down to top)**로 가시성이 전파되는데, 트랜잭션 생성/해제는 위→아래(top to down) 스택이 필요하다. 그래서 가변 홀더(ArrayDeque 기반)를 컨텍스트에 한 번 심어두고, 그 안에서 컨텍스트 스택을 push/pop한다. getOrCreateContextHolder()/getOrCreateContext()가 이 설정을 담당한다.
reactive 패키지에는 별도의 TransactionSynchronizationManager(명령형 것과 이름은 같지만 다른 클래스)가 있어, 이 TransactionContext를 통해 자원을 바인딩/조회한다. R2DBC 커넥션이 같은 리액티브 파이프라인 안에서 공유되는 원리다.
TransactionalOperator는 TransactionTemplate의 리액티브 대응이다.
TransactionalOperator op = TransactionalOperator.create(reactiveTxManager);
Mono<User> result = repository.save(user)
.then(repository.updateStats())
.as(op::transactional); // 이 파이프라인 전체를 한 트랜잭션으로transactional(Mono)/transactional(Flux)은 내부적으로 execute(callback)에 위임하고, 구현체 TransactionalOperatorImpl이 매니저를 호출한다. 흐름(개념):
op.transactional(pipeline)
│
▼ contextWrite 로 TransactionContext(Holder) 등록
TransactionContextManager.currentContext()
│
├ rtm.getReactiveTransaction(def) : Mono<ReactiveTransaction> (begin)
├ 사용자 pipeline 실행 (자원은 reactive TSM 통해 Context에서 공유)
│ 정상 → rtm.commit(rtx) : Mono<Void>
│ 에러 → rollbackOn 판단 후 rtm.rollback(rtx) : Mono<Void>
│ 취소 → rtm.rollback(rtx) (구독 취소 시 롤백)
└ 결과 시그널을 호출자에게
이 같은 로직을 선언적으로도 쓸 수 있다. 3장 TransactionAspectSupport.invokeWithinTransaction()이 매니저가 ReactiveTransactionManager이고 반환 타입이 Mono/Flux/코루틴이면 ReactiveTransactionSupport 경로로 분기해, Mono.usingWhen/Flux.usingWhen으로 begin→proceed→commit/rollback을 리액티브하게 엮는다. 즉 @Transactional이 리액티브 매니저와 만나면 자동으로 리액티브 경계 설정이 된다. 단 @Transactional JavaDoc 경고대로, 리액티브 매니저로 구동되는 트랜잭션 메서드는 반드시 리액티브 타입을 반환해야 한다(void/일반 타입은 명령형 매니저로).
TransactionTemplate.execute(callback)— 정의(자기 자신)로getTransaction→ 콜백 실행 → 커밋/롤백. 모든Throwable에 롤백.TransactionOperations.withoutTransaction()— 무트랜잭션 실행 구현(테스트용).ReactiveTransactionManager.getReactiveTransaction()—Mono<ReactiveTransaction>반환, 예외는 파이프라인으로 전파.TransactionContextManager.currentContext()— Reactor Context에서 현재 트랜잭션 컨텍스트 조회(없으면 에러 시그널).TransactionalOperator.transactional(Mono/Flux)— 리액티브 파이프라인을 트랜잭션으로 감싸는 진입점.
- 템플릿 메서드 재사용 —
TransactionTemplate은DefaultTransactionDefinition을 상속해 "정의=템플릿"으로 합쳤다. 빈으로 등록해 전파·격리를 한 번 설정하고 재사용한다. - 명령형/리액티브 대칭 설계 —
AbstractReactiveTransactionManager가AbstractPlatformTransactionManager의 워크플로를 그대로 옮겨, 두 세계가 같은 전파 의미·동기화 모델·ConfigurableTransactionManager리스너 훅을 공유한다. - Context vs ThreadLocal — 리액티브는
ThreadLocal을 못 쓰므로 ReactorContext에 상태를 싣는다. 가변TransactionContextHolder로 컨텍스트 전파 방향(아래→위)과 트랜잭션 스택(위→아래)의 불일치를 메운다. CallbackPreferringPlatformTransactionManager—getTransaction/commit분리 모델 대신 콜백 안에서 트랜잭션을 돌리길 선호하는 매니저(일부 JTA CMT).TransactionTemplate과 인터셉터 모두 이 분기를 지원한다.- 취소 처리 주의 — 리액티브 트랜잭션은 구독 취소 시 롤백된다. JavaDoc은 트랜잭션 Publisher에서 구독 취소를 피하라고 권고한다.
프로그래밍적 트랜잭션은 프록시 없이 코드로 경계를 친다. 명령형은 TransactionTemplate.execute()가 2장 매니저를 직접 호출하되 모든 예외에 롤백한다는 점이 선언적과 다르다. 리액티브는 ThreadLocal을 쓸 수 없어 트랜잭션 상태를 TransactionContext에 담아 Reactor Context로 흘려보내고, AbstractReactiveTransactionManager가 명령형과 동일한 전파/동기화 워크플로를 Mono로 구현한다. TransactionalOperator가 그 프로그래밍적 진입점이며, @Transactional도 리액티브 매니저와 만나면 같은 메커니즘으로 동작한다. 마지막 장은 트랜잭션 단계에 연동되는 이벤트, 벤더 예외 변환, JTA 연동을 다룬다.