Skip to content

Latest commit

 

History

History
159 lines (121 loc) · 11.6 KB

File metadata and controls

159 lines (121 loc) · 11.6 KB

05. 프로그래밍적·리액티브 트랜잭션

소스: 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

TransactionTemplateDefaultTransactionDefinition을 상속한다. 즉 자기 자신이 트랜잭션 정의다(전파·격리 등을 빈 프로퍼티로 설정). 동시에 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장 인터셉터와 동일한 패턴.

TransactionOperations — 추상화와 테스트

TransactionTemplate이 구현하는 인터페이스. execute(callback)과 람다 친화적 executeWithoutResult(Consumer<TransactionStatus>)을 제공한다. 두 가지 정적/유틸이 유용하다.

  • TransactionCallbackWithoutResult — 반환값 없는 콜백용 추상 클래스.
  • TransactionOperations.withoutTransaction() — 실제 트랜잭션 없이 콜백을 그냥 실행하는 구현(WithoutTransactionOperations). 테스트에서 매니저 없이 코드를 돌릴 때 유용(동작상 PROPAGATION_SUPPORTS + SYNCHRONIZATION_NEVER와 동등).

리액티브: ReactiveTransactionManager

1장에서 봤듯 ReactiveTransactionManager는 명령형의 거울상이되 모든 결과가 Mono다.

Mono<ReactiveTransaction> getReactiveTransaction(@Nullable TransactionDefinition definition);
Mono<Void> commit(ReactiveTransaction transaction);
Mono<Void> rollback(ReactiveTransaction transaction);

결정적 차이: 예외가 throw가 아니라 리액티브 파이프라인을 통해 전파된다. 그래서 호출자는 subscribe 시점에 에러 시그널로 받는다. AbstractReactiveTransactionManagerAbstractPlatformTransactionManager똑같은 워크플로(전파 판정, suspend/resume, 롤백전용 점검, 동기화 콜백)를 리액티브 버전으로 구현하고, doBegin/doCommit 등 템플릿 메서드를 Mono 반환으로 남긴다. R2dbcTransactionManager(spring-r2dbc) 등이 이를 상속한다.

TransactionContext — ThreadLocal 대체

명령형이 TransactionSynchronizationManagerThreadLocal에 자원/동기화/메타데이터를 담았다면, 리액티브는 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로 리액티브 트랜잭션

TransactionalOperatorTransactionTemplate의 리액티브 대응이다.

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) — 리액티브 파이프라인을 트랜잭션으로 감싸는 진입점.

설계 포인트 / 확장점

  • 템플릿 메서드 재사용TransactionTemplateDefaultTransactionDefinition을 상속해 "정의=템플릿"으로 합쳤다. 빈으로 등록해 전파·격리를 한 번 설정하고 재사용한다.
  • 명령형/리액티브 대칭 설계AbstractReactiveTransactionManagerAbstractPlatformTransactionManager의 워크플로를 그대로 옮겨, 두 세계가 같은 전파 의미·동기화 모델·ConfigurableTransactionManager 리스너 훅을 공유한다.
  • Context vs ThreadLocal — 리액티브는 ThreadLocal을 못 쓰므로 Reactor Context에 상태를 싣는다. 가변 TransactionContextHolder로 컨텍스트 전파 방향(아래→위)과 트랜잭션 스택(위→아래)의 불일치를 메운다.
  • CallbackPreferringPlatformTransactionManagergetTransaction/commit 분리 모델 대신 콜백 안에서 트랜잭션을 돌리길 선호하는 매니저(일부 JTA CMT). TransactionTemplate과 인터셉터 모두 이 분기를 지원한다.
  • 취소 처리 주의 — 리액티브 트랜잭션은 구독 취소 시 롤백된다. JavaDoc은 트랜잭션 Publisher에서 구독 취소를 피하라고 권고한다.

정리

프로그래밍적 트랜잭션은 프록시 없이 코드로 경계를 친다. 명령형은 TransactionTemplate.execute()가 2장 매니저를 직접 호출하되 모든 예외에 롤백한다는 점이 선언적과 다르다. 리액티브는 ThreadLocal을 쓸 수 없어 트랜잭션 상태를 TransactionContext에 담아 Reactor Context로 흘려보내고, AbstractReactiveTransactionManager가 명령형과 동일한 전파/동기화 워크플로를 Mono로 구현한다. TransactionalOperator가 그 프로그래밍적 진입점이며, @Transactional도 리액티브 매니저와 만나면 같은 메커니즘으로 동작한다. 마지막 장은 트랜잭션 단계에 연동되는 이벤트, 벤더 예외 변환, JTA 연동을 다룬다.