TransactionManager가 일하는 순서

YoonJuHo·2025년 4월 22일

오늘의 목표

스프링에서의 트랜잭션 추상화와 동작 순서 그리고 트랜잭션 매니저 종류에 대해 알아보겠습니다.

TransactionAutoConfiguration 그 후로...를 통해 최종적으로 아래와 같은 flowPlatformTransactionManager이 트랜잭션을 시작한다는 것을 알 수 있었습니다.

...MyService.method() 호출 → 프록시의 invoke()TransactionInterceptorinvokeWithinTransaction()PlatformTransactionManagergetTransaction() / commit() / rollback()

자동구성..근데-이제-Data-JPA를-곁들인 을 통해 JPA를 사용하게 되었을 때는 JpaTransactionManager가 등록된다는 것을 알 수 있었는데요.
마지막에 JpaTransactionManager가 아닌 PlatformTransactionManager 을 통해 transaction을 실행한다는 것이 조금은 어색하게 다가옵니다.

JpaTransactionManager

  • JPA 기반의 DataSource를 사용하는 경우에 트랜잭션을 관리해주는 스프링의 트랜잭션 매니저 구현체

DataSourceTransactionManager

  • JDBC 기반의 DataSource를 사용하는 경우에 트랜잭션을 관리해주는 스프링의 트랜잭션 매니저 구현체

AbstractPlatformTransactionManager

JpaTransactionManager 코드를 살펴보면 그 이유를 알 수 있는데요. JpaTransactionManagerAbstractPlatformTransactionManager을 상속하고 AbstractPlatformTransactionManagerPlatformTransactionManager를 확장한 형태인 것을 확인할 수 있습니다.

이를 통해 PlatformTransactionManagerJpaTransactionManager을 추상화한 인터페이스임을 알 수 있습니다.

이번 글에서는 스프링이 제공하는 이러한 트랜잭션 추상화 기능리소스 동기화에 대해 살펴보겠습니다.

  • 트랜잭션 추상화
  • 리소스 동기화 -> 다음글에서 다룰 예정

트랜잭션 추상화

트랜잭션을 시작하고, 커밋하거나 롤백하는 트랜잭션 제어 책임 객체 -> Transaction Manager (=DataSourceTransactionManager, JpaTransactionManager 등)

구글에 TransactionManager를 검색하게 된다면 관련 검색어로 PlatformTransactionManager이 먼저 나오는 것을 알 수 있습니다.

PlatformTransactionManager

스프링은 애플리케이션에서 트랜잭션을 보다 쉽게 관리할 수 있도록 하기 위해, 다양한 데이터 접근 기술(JDBC, JPA 등)에 종속되지 않는 트랜잭션 추상화를 제공합니다. 이 추상화의 핵심 인터페이스가 바로 PlatformTransactionManager입니다.

왜 트랜잭션 추상화를 할까?

초기에는 JDBC를 사용해 직접 트랜잭션을 제어했다가 나중에 JPA를 도입하는 경우, 서비스 계층의 코드가 데이터 접근 기술에 맞춰 수정되어야 하는 문제를 겪게 됩니다. 하지만 스프링의 PlatformTransactionManager를 사용하면, 해당 기술별 구현체(DataSourceTransactionManager or JpaTransactionManager 등)를 선택하여 적용할 수 있으므로, 서비스 계층에서는 동일한 트랜잭션 관리 코드를 사용할 수 있습니다.
즉, 데이터 접근 기술이 변경되더라도 애플리케이션 코드의 변경 없이 트랜잭션 관리를 유지할 수 있게 됩니다.

JpaTransactionManager 등록 - JPA DataSourceTransactionManager 등록 - JDBC
transactionManager() 메서드에 동일하게 @ConditionalOnMissingBean(TransactionManager.class)가 존재합니다.
이건 PlatformTransactionManager 타입 빈이 존재하지 않을 때만 등록하라는 뜻으로 내부적으로 TransactionManager.class = PlatformTransactionManager.class 라고 생각해도 무방합니다.

내 프로젝트에는 어떤 TransactionManager가 일을하고 있을까?

Data-JDBC일 경우 -> JdbcTransactionManager

  • implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

    현재 JdbcTransactionManagerTransactionManager의 구현체로 등록이 되어있는데 JdbcTransactionManagerSpring 6.0 부터 DataSourceTransactionManager를 확장하여 거의 모든 기능을 그대로 사용하면서 이름을 더 직관적으로 바꾼 것이라고 보면 됩니다.
    • JdbcTransactionManager

Data-JPA일 경우 -> JpaTransactionManager

  • implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

JDBC vs Data JDBC vs JPA vs Data JPA

구분JDBCSpring Data JDBCJPA (with Hibernate)Spring Data JPA
핵심 목표DB와 직접 통신간단한 CRUD 자동화ORM 기반 객체-DB 매핑JPA 위에 추상화된 CRUD + 쿼리 자동화
추상화 수준가장 낮음 (SQL 직접 작성)중간 (도메인 매핑 + SQL 일부 자동화)높은 수준의 ORMORM + 자동 Repository 구현
사용 방식Connection, Statement, ResultSet 또는 JdbcTemplate 사용JdbcTemplate 기반 + Spring Data 철학에 따라 Repository 자동 생성
➡️ Repository는 인터페이스로서, DAO(데이터 접근 객체) 역할을 대신함
EntityManager를 사용해 엔티티 관리, 연관관계 매핑도 포함JpaRepository 상속만으로 CRUD 구현, @Query로 복잡 쿼리도 가능
연관관계 매핑직접 JOIN SQL 작성제한적 (1:1 정도만, 깊은 연관 매핑은 불가능)@Entity, @OneToMany, @ManyToOne 등을 통해 객체 간 연관관계 선언JPA의 모든 매핑 기능 지원, 연관관계 탐색과 자동 조회 가능
복잡 쿼리 처리SQL 직접 작성SQL 기반 @Query 작성JPQL (객체 지향 쿼리 언어), Criteria APIJPQL, @Query, QueryDSL, Specification 등 다양한 방식 지원
학습 난이도낮음 (SQL 익숙하면 쉽게 시작)중간 (Spring과 도메인 중심 모델 이해 필요)높음 (ORM 개념, 연관관계, 영속성 컨텍스트 이해 필요)중간 (Spring Data의 추상화 개념만 익히면 활용 쉬움)
주요 구현체JDBC 표준 (Oracle, MySQL, H2 등 DB 드라이버)Spring Data JDBC 모듈Hibernate, EclipseLink, TopLink 등Hibernate + Spring Data JPA 조합

트랜잭션 매니저 동작 방식

데이터 접근 기술이 변경되더라도 애플리케이션 코드의 변경 없이 트랜잭션 관리를 유지할 수 있게끔 PlatformTransactionManager이 사용된다는 것을 알 수 있었습니다. 이제는 PlatformTransactionManager의 구현체인 JpaTrnasactionManager을 사용하였을때 어떠한 방식으로 메서드 호출이 일어나는지 알아보겠습니다.

...MyService.method() 호출 → 프록시의 invoke()TransactionInterceptorinvokeWithinTransaction()PlatformTransactionManagergetTransaction() / commit() / rollback()
  1. 우선 PlatformTransactionManager의 추상메서드 getTransaction() 이 시작되면

  2. 해당 메서드를 정의한 AbstractPlatformTransactionManagergetTransaction()이 호출되게 됩니다.

    내부를 자세히 살펴보면 아래의 2가지 메서드 호출이 존재합니다.

1. Object transaction = this.doGetTransaction();
2. return this.handleExistingTransaction(def, transaction, debugEnabled);

this.doGetTransaction();


    protected Object doGetTransaction() {
        JpaTransactionObject txObject = new JpaTransactionObject();
        txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
        EntityManagerHolder emHolder = (EntityManagerHolder)TransactionSynchronizationManager.getResource(this.obtainEntityManagerFactory());
        if (emHolder != null) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Found thread-bound EntityManager [" + emHolder.getEntityManager() + "] for JPA transaction");
            }

            txObject.setEntityManagerHolder(emHolder, false);
        }

        if (this.getDataSource() != null) {
            ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.getDataSource());
            txObject.setConnectionHolder(conHolder);
        }

        return txObject;
    }
  • doGetTransaction()은 추상화 메서드로 현재 쓰레드에서 이미 시작된 트랜잭션이 있는지 확인하고, 트랜잭션 객체(여기서는 JpaTransactionObject)를 생성해 반환합니다.
  • 이 메서드는 트랜잭션의 시작 전 준비단계로서, 이미 쓰레드에 바인딩된 EntityManagerConnection이 있으면 그것을 활용하고 없으면 이후 doBegin() 단계에서 새로 트랜잭션을 시작하도록 도와주는 역할을 합니다.

this.handleExistingTransaction(def, transaction, debugEnabled);

  • 이 메서드는 현재 쓰레드에 이미 존재하는 트랜잭션이 있을 때의 처리 흐름을 담당합니다. 즉, 전파 속성(Propagation)에 따라 트랜잭션을 어떻게 이어받거나 새로 만들지를 결정하는 중심 메서드입니다.
  • 해당 메서드를 통해 startTransaction() 이 호출되는데, handleExistingTransaction() 자체는 조건에 따라 startTransaction()을 호출하지 않기도 합니다.
  • 전파 속성에 따라 기존 트랜잭션에 참여하게 된다면 prepareTransactionStatus() 을 호출하게 됩니다.

여기서는 새롭게 트랜잭션을 만든다고 가정하고 진행하겠습니다. -> startTransaction()

  1. AbstractPlatformTransactionManager.handleExistingTransaction() -> AbstractPlatformTransactionManager.startTransaction() -> 구현체.doBegin()

    private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean nested, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {
        boolean newSynchronization = this.getTransactionSynchronization() != 2;
        DefaultTransactionStatus status = this.newTransactionStatus(definition, transaction, true, newSynchronization, nested, debugEnabled, suspendedResources);
        this.transactionExecutionListeners.forEach((listener) -> {
            listener.beforeBegin(status);
        });
    
        try {
            this.doBegin(transaction, definition);
        } catch (Error | RuntimeException var9) {
            Throwable ex = var9;
            this.transactionExecutionListeners.forEach((listener) -> {
                listener.afterBegin(status, ex);
            });
            throw ex;
        }
    
        this.prepareSynchronization(status, definition);
        this.transactionExecutionListeners.forEach((listener) -> {
            listener.afterBegin(status, (Throwable)null);
        });
        return status;
    }
    
    
    protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
        if (status.isNewSynchronization()) {
            TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
            TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(definition.getIsolationLevel() != -1 ? definition.getIsolationLevel() : null);
            TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
            TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
            TransactionSynchronizationManager.initSynchronization();
        }
    
    }
    

startTransaction() 에서 주의깊게 보아야 할 단계는 크게 3가지입니다.

단계역할설명
1번째. 트랜잭션 상태 객체 생성상태 저장DefaultTransactionStatus 객체를 생성하여, 현재 트랜잭션이 새 트랜잭션인지, 중첩 트랜잭션인지 등의 상태 정보를 저장합니다. 이 객체는 이후 커밋/롤백 판단 시 사용됩니다.
2번째. 실제 트랜잭션 시작트랜잭션 열기JpaTransactionManager#doBegin() 메서드에서 EntityManager.getTransaction().begin() 을 호출하여 JPA 트랜잭션을 시작합니다. 이때 EntityManager는 트랜잭션 동기화 매니저에 바인딩됩니다.
3번째. 동기화 설정컨텍스트 바인딩TransactionSynchronizationManager를 통해 현재 쓰레드에 트랜잭션 이름, 읽기 전용 여부, 격리 수준 등을 바인딩합니다. 이후 동작하는 DAO나 Repository는 이 정보를 참조하여 동일 트랜잭션에 참여할 수 있습니다.
  • 1번째에서 DefaultTransactionStatus는 트랜잭션의 현재 상태(신규 여부, 읽기 전용 여부, 저장점 보유 여부 등)를 표현하고 관리하는 객체입니다.

  • 3번째에서 TransactionSynchronizationManager를 통해 현재 쓰레드에 트랜잭션 정보를 바인딩하게 되는데, 이때 주의할 점은 Connection을 현재 쓰레드에 바인딩하는 작업은 startTransaction()이 아니라 2번째 doBegin() 내부에서 수행된다는 것입니다.

startTransaction() 은 구현체의 doBegin()을 호출합니다.

  1. JpaTransactionManager.doBegin()

    protected void doBegin(Object transaction, TransactionDefinition definition) {
        JpaTransactionObject txObject = (JpaTransactionObject)transaction;
        if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            throw new IllegalTransactionStateException("Pre-bound JDBC Connection found! JpaTransactionManager does not support running within DataSourceTransactionManager if told to manage the DataSource itself. It is recommended to use a single JpaTransactionManager for all transactions on a single DataSource, no matter whether JPA or JDBC access.");
        } else {
            try {
                EntityManager em;
                if (!txObject.hasEntityManagerHolder() || txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
                    em = this.createEntityManagerForTransaction();
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Opened new EntityManager [" + em + "] for JPA transaction");
                    }

                    txObject.setEntityManagerHolder(new EntityManagerHolder(em), true);
                }

                em = txObject.getEntityManagerHolder().getEntityManager();
                int timeoutToUse = this.determineTimeout(definition);
                Object transactionData = this.getJpaDialect().beginTransaction(em, new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
                txObject.setTransactionData(transactionData);
                txObject.setReadOnly(definition.isReadOnly());
                if (timeoutToUse != -1) {
                    txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse);
                }

                if (this.getDataSource() != null) {
                    ConnectionHandle conHandle = this.getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
                    if (conHandle != null) {
                        ConnectionHolder conHolder = new ConnectionHolder(conHandle);
                        if (timeoutToUse != -1) {
                            conHolder.setTimeoutInSeconds(timeoutToUse);
                        }

                        if (this.logger.isDebugEnabled()) {
                            this.logger.debug("Exposing JPA transaction as JDBC [" + conHandle + "]");
                        }

                        TransactionSynchronizationManager.bindResource(this.getDataSource(), conHolder);
                        txObject.setConnectionHolder(conHolder);
                    } else if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because JpaDialect [" + this.getJpaDialect() + "] does not support JDBC Connection retrieval");
                    }
                }

                if (txObject.isNewEntityManagerHolder()) {
                    TransactionSynchronizationManager.bindResource(this.obtainEntityManagerFactory(), txObject.getEntityManagerHolder());
                }

                txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
            } catch (TransactionException var9) {
                TransactionException ex = var9;
                this.closeEntityManagerAfterFailedBegin(txObject);
                throw ex;
            } catch (Throwable var10) {
                Throwable ex = var10;
                this.closeEntityManagerAfterFailedBegin(txObject);
                throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex);
            }
        }
    }

해당 doBegin() 메서드는 JpaTransactionManager에서 JPA 트랜잭션을 실제로 시작하는 로직의 핵심입니다.
이 코드 하나로 트랜잭션의 연결, 동기화, 커넥션 노출까지 모두 처리되기 때문에, 아주 중요한 코드입니다.
하나씩 살펴보겠습니다.

  1. JDBC 커넥션 충돌 검사
	if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
    	throw new IllegalTransactionStateException(...);
	}

Spring에서 JPAJDBC를 동시에 같은 DataSource에서 사용하려 할 때 충돌이 발생할 수 있습니다
JpaTransactionManager를 쓰는 경우엔 DataSourceTransactionManager를 병행하면 안 됨

  1. EntityManager 생성 또는 재사용
if (!txObject.hasEntityManagerHolder() || ...) {
    em = this.createEntityManagerForTransaction();
    txObject.setEntityManagerHolder(new EntityManagerHolder(em), true);
}

없거나 재사용하면 안 되는 상황이면 새로 만들게 됩니다.
EntityManager는 실제로 JPA 트랜잭션을 제어할 핵심 객체로, true는 이 EntityManager가 새로 만들어졌다는 표시입니다.

  1. 리소스 바인딩
TransactionSynchronizationManager.bindResource(this.obtainEntityManagerFactory(), txObject.getEntityManagerHolder());

트랜잭션 범위 안에서 동작하는 DAORepository들이 EntityManagerConnection을 자동으로 참조할 수 있도록 현재 쓰레드에 리소스를 등록합니다
주의 포인트: TransactionSynchronizationManager는 쓰레드 로컬 기반 → 반드시 커밋/롤백 이후 unbind 필요 (Spring이 자동 정리)

  1. 트랜잭션 동기화 완료 표시
txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);

EntityManager는 현재 트랜잭션과 연결돼 있음을 명시합니다.

마지막 doBegin()을 호출하는 것으로 실제 트랜잭션이 시작되며 전체 흐름을 요약하면 아래와 같습니다.

🔁 전체 흐름 요약: @Transactional 적용 후의 트랜잭션 처리 사이클

@Transactional 메서드 호출
   ↓
AOP 프록시: TransactionInterceptor.invoke()
   ↓
invokeWithinTransaction() → 트랜잭션 전처리
   ↓
createTransactionIfNecessary()
   ↓
→ PlatformTransactionManager.getTransaction()
   ↓
→ AbstractPlatformTransactionManager.getTransaction()
   ↓
→ doGetTransaction() + handleExistingTransaction()
   ↓
→ startTransaction()
   ↓
→ ✅ doBegin() ← 실제 트랜잭션 시작
   ↓
비즈니스 로직 실행 (invocation.proceedWithInvocation())
   ↓
정상 종료 → commitTransactionAfterReturning()
         → PlatformTransactionManager.commit()
         → AbstractPlatformTransactionManager.commit()
         → doCommit() ← 진짜 커밋
OR
예외 발생 → completeTransactionAfterThrowing()
         → PlatformTransactionManager.rollback()
         → AbstractPlatformTransactionManager.rollback()
         → doRollback() ← 진짜 롤백
   ↓
TransactionInfo cleanup → ThreadLocal 해제

마무리하며

지금까지 @Transactional이 동작하는 내부 구조를 따라가며, 트랜잭션이 어떻게 시작되고, 어떤 과정을 거쳐 커밋 또는 롤백되며, 마지막에 어떻게 정리되는지까지의 전체 흐름을 살펴보았습니다.

정리하자면, PlatformTransactionManager라는 추상화 덕분에 데이터 접근 기술이 변경되더라도 애플리케이션 코드를 수정하지 않고도 동일한 방식으로 트랜잭션을 관리할 수 있다는 점을 확인할 수 있었습니다.
또한 실제 코드를 따라가면서 트랜잭션 시작부터 종료까지 어떤 메서드가 어떤 순서로 호출되는지도 명확히 이해할 수 있었습니다.

다음 글에서는 이번 글에서 다루지 못한 리소스 동기화를 담당하는 TransactionSynchronizationManager의 역할과
트랜잭션 종료 시 실행되는 후처리 작업들에 대해 이어서 정리해보겠습니다.

0개의 댓글