스프링 트랜잭션 전파

이정원·2025년 1월 3일

1.트랜잭션 프록시

트랜잭션을 사용할때 커넥션을 얻고 로직 수행과 반납에 대한 과정을 확인해보자.

double_commit Test

 @Test
    void double_commit(){
        log.info("트랜잭션1 시작");
        TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());

        log.info("트랜잭션1 커밋");
        txManager.commit(tx1);

        log.info("트랜잭션2 시작");
        TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute());

        log.info("트랜잭션2 커밋");
        txManager.commit(tx2);

        log.info("트랜잭션 커밋 완료");
    }


결과를 보면 트랜잭션1을 시작하고 커넥션 풀에서 conn0을 획득하고 반납했다. 마찬가지로 트랜잭션2에서도 conn0을 획득하고 반납한다. 결과적으로 둘은 다른 프록시 커넥션 객체로 인지된다.
Hikari 커넥션 풀에서 커넥션을 획득할 때, 내부에 실제 커넥션이 포함된 프록시 커넥션을 반환한다.
주소를 확인해보면 트랜잭션1과 트랜잭션2에서 서로 다른 프록시 객체를 사용하지만, 실제로는 동일한 물리적 커넥션(conn0)을 재사용하는 것을 알 수 있다.

  • 트랜잭션1: [HikariProxyConnection@379666550]
  • 트랜잭션2: [HikariProxyConnection@1472719920]

2.트랜잭션 전파

트랜잭션 사용중에 그 안에서 또다른 트랜잭션을 사용하는 복잡한 상황들에 대해 스프링이 어떻게 문제를 해결하는지 알아보자.

2-1.외부,내부 트랜잭션

외부 트랜잭션(상대적으로 밖에 있음)이 수행중일때, 내부 트랜잭션(외부 트랜잭션 수행 도중 호출)이 수행되는 상황이 있다. 이럴때 물리 트랜잭션과 논리 트랜잭션이라는 개념을 나눈다. 외부,내부 트랜잭션들은 각각 논리 트랜잭션이라 불리고 물리 트랜잭션은 실제 커넥션을 통해서 트랜잭션을 시작하고 커밋,롤백하는 단위이다.

원칙

  • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
  • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.

inner_commit Test

@Test
void inner_commit(){
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer=txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction()={}",outer.isNewTransaction());

        log.info("내부 트랜잭션 시작");
        TransactionStatus inner=txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("inner.isNewTransaction()={}",inner.isNewTransaction());
        log.info("내부 트랜잭션 커밋");
        txManager.commit(inner);

        log.info("외부 트랜잭션 커밋");
        txManager.commit(outer);
    }

외부 트랜잭션 수행중일때 내부 트랜잭션을 추가로 수행한다. 이때 외부 트랜잭션(isNewTransaction=true)이고 내부 트랜잭션은 외부 트랜잭션에 참여 한다고 한다. 다른 관점으로 외부 트랜잭션이 내부 트랜잭션까지 넓어지는데 하나의 물리 트랜잭션으로 묶이는 과정이다. 내부 트랜잭션은 신규 트랜잭션이 아니기 때문에 (isNewTransaction=false)로 나온다. 내부 트랜잭션은 DB 커넥션을 통해 물리 트랜잭션을 커밋하면 전체 트랜잭션은 끝나기 때문에 외부 트랜잭션의 다음 로직이 반영이 안된다.(외부 트랜잭션이 마지막에 실제 커밋을 진행) 결과적으로 외부 트랜잭션이 물리 트랜잭션을 관리하는 역할을 한다.


트랜잭션 요청

트랜잭션 응답

2-2.(상황1)외부 롤백

내부 트랜잭션은 커밋되고 외부 트랜잭션이 롤백되는 상황을 알아보자. 결과적으로 전체가 롤백된다.

2-3.(상황2)내부 롤백

이번에는 트랜잭션 내부에선 롤백되지만 외부에서 커밋되는 경우를 알아보자. 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않기 때문에 단순하지 않다. 앞서 배운대로 스프링은 전체를 롤백해야 되는데 어떻게 진행되는지 알아보자.

inner_rollback Test

@Test
    void inner_rollback(){
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer=txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction()={}",outer.isNewTransaction());

        log.info("내부 트랜잭션 시작");
        TransactionStatus inner=txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("inner.isNewTransaction()={}",inner.isNewTransaction());
        log.info("내부 트랜잭션 롤백);
        txManager.rollback(inner);

        log.info("외부 트랜잭션 커밋");
        txManager.commit(outer);
    }


결과를 보면 내부에서 기존 트랜잭션에 rollback-only를 마킹 했다는 표시가 있다.

내부 트랜잭션에서 rollback이 되면 트랜잭션 동기화 매니저에 rollback-only를 True로 표시하고, 외부 트랜잭션으로 로직이 넘어간다. 마지막 외부 트랜잭션에서 커밋을 시도하면 UnexpectedRollbackException 런타임 예외를 던져 롤백 발생을 명확하게 알려준다.

다음은 논리 트랜잭션을 분리해서 각각 커밋,롤백하는 방법을 알아보자.

3. REQUIRES_NEW


이렇게 물리 트랜잭션을 분리하려면 내부 트랜잭션에서 REQUIRES_NEW 옵션을 사용하면 된다. 별도의 물리 트랜잭션을 사용한다는 뜻은 별도의 DB 커넥션을 가진다는 뜻과 동일하다. REQUIRES_NEW는 신규 트랜잭션을 만든다.
REQUIRES_NEW Test

@Test
void inner_rollback_requires_new(){
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer=txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction()={}",outer.isNewTransaction());

        log.info("내부 트랜잭션 시작");
        DefaultTransactionAttribute definition=new DefaultTransactionAttribute();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus inner=txManager.getTransaction(definition);
        log.info("inner.isNewTransaction()={}",inner.isNewTransaction());

        log.info("내부 트랜잭션 롤백");
        txManager.rollback(inner); // 롤백

        log.info("외부 트랜잭션 커밋");
        txManager.commit(outer); // 커밋
    }


그림과 같이 conn1,conn2는 트랜잭션 동기화 매니저에 의해 관리되고 내부 트랜잭션이 실행될때 conn0는 보류된다. 두 커넥션 isNewTransaction()를 확인해보면 둘다 True로 나온다.

하나의 Thread(Http 요청)에서 다수의 DB 커넥션을 사용하기 때문에 리소스 사용 관점에서 주의가 필요하다.

4.트랜잭션 전파 옵션

스프링은 다양한 트랜잭션 전파 옵션을 사용한다. 실무에서 대부분 별도의 설정이 없는 REQUIRED가 기본으로 사용되고 가끔 REQUIRES_NEW가 사용된다.

1.REQUIRED
현재 실행 중인 외부에 트랜잭션이 있으면 그 트랜잭션에 참여한다. 실행 중인 트랜잭션이 없으면 새 트랜잭션을 시작한다.

2.REQUIREDS_NEW
항상 새로운 트랜잭션을 시작한다.(실행 중인 트랜잭션이 있으면 일시적으로 보류하고 새 트랜잭션을 시작한다.)

3.SUPPORT
현재 실행 중인 트랜잭션이 있으면 그 트랜잭션에 참여하고 없으면 트랜잭션 없이 로직을 실행한다.

4.NOT_SUPPORT
실행 중인 트랜잭션이 있으면 일시적으로 중단한 후, 비트랜잭션 상태로 실행한다.(예: 대용량 배치 작업이나 성능 최적화)

5.MANDATORY
현재 실행 중인 로직에 트랜잭션이 반드시 있어야 하고, 트랜잭션이 없으면 IllegalTransactionStateException 예외가 발생한다.

6.NEVER
트랜잭션 없이 실행하고 실행중인 트랜잭션이 있으면 IllegalTransactionStateException 예외가 발생한다.

0개의 댓글