ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 트랜잭션 전파(propagation)
    BackEnd/Spring DB 2023. 2. 15. 22:00
    반응형

    스프링 트랜잭션 전파 옵션

    REQUIRED

      전파 옵션에 별도의 설정을 하지 않으면 기본으로 사용되는 설정으로, 기존 트랜잭션이 없으면 생성하고 있으면 참여합니다.

    • 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
    • 기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.

     

    REQUIRES_NEW

      항상 새로운 트랜잭션을 생성합니다.

    • 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
    • 기존 트랜잭션 있음: 새로운 트랜잭션을 생성한다.

     

    SUPPORT

      트랜잭션을 지원한다는 뜻으로 기존 트랜잭션이 없으면 없는대로 진행하고, 있으면 참여합니다.

    • 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
    • 기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.

     

    NOT_SUPPORT

      트랜잭션을 지원하지 않는다는 의미입니다.

    • 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
    • 기존 트랜잭션 있음: 트랜잭션 없이 진행한다(기존 트랜잭션은 보류한다).

     

    MANDATORY

      트랜잭션이 반드시 있어야 하는 의무사항입니다. 기존 트랜잭션이 없으면 예외가 발생합니다.

    • 기존 트랜잭션 없음: IllegalTransactionStateException 예외 발생
    • 기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.

     

    NEVER

      트랜잭션을 사용하지 않는다는 의미입니다. 기존 트랜잭션이 있으면 예외가 발생합니다.

    • 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
    • 기존 트랜잭션 있음: IllegalTransactionStateException 예외 발생

     

    NESTED

      중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만, 중첩 트랜잭션은 외부에 영향을 주지 않습니다. 중첩 트랜잭션이 롤백 되어도 외부 트랜잭션은 커밋할 수 있고, 외부 트랜잭션이 롤백 되면 중첩 트랜잭션도 함께 롤백됩니다. 중첩 트랜잭션은 JPA에서는 사용할 수 없으며, JDBC savepoint 기능을 사용하기에 DB 드라이버에서 해당 기능을 지원하는지 확인이 필요합니다.

    • 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
    • 기존 트랜잭션 있음: 중첩 트랜잭션을 만든다.

     

    Note. 트랜잭션 전파와 옵션

      isolation, timeout, readOnly는 트랜잭션이 처음 시작될 때만 적용됩니다. 트랜잭션에 참여하는 경우에는 적용되지 않습니다.

     

    스프링 트랜잭션 전파- REQUIRED

      외부 트랜잭션(처음 시작된 트랜잭션)이 수행중인 상태에서 내부 트랜잭션이 수행되면, 스프링은 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션으로 만들어줍니다. 내부 트랜잭션이 외부 트랜잭션에 참여하는 것입니다. 이것이 기본 동작이고, 옵션을 통해 다른 동작방식도 선택할 수 있습니다.

     

    물리 트랜잭션, 논리 트랜잭션

    • 스프링은 이해를 돕기 위해 논리 트랜잭션과 물리 트랜잭션이라는 개념으로 나눕니다.
    • 물리 트랜잭션은 실제 데이터베이스에 적용되는 트랜잭션을 뜻합니다. 실제 커넥션을 통해 트랜잭션을 시작(setAutoCommit(false))하고, 실제 커넥션을 통해 커밋, 롤백하는 단위입니다.
    • 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위입니다.
    • 논리 트랜잭션은 하나의 물리 트랜잭션으로 묶이며, 이러한 논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타납니다. 단순히 트랜잭션이 하나인 경우에는 둘을 구분하지 않습니다.
    • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됩니다. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됩니다.

     

    커밋(commit)

      다음은 외부 트랜잭션 수행중 내부 트랜잭션이 추가로 수행되는 예제 코드입니다.

        @Test
        void inner_commit() {
            log.info("외부 트랜잭션 시작");
            TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
            log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // true
    
            log.info("내부 트랜잭션 시작");
            TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
            log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // false
            log.info("내부 트랜잭션 커밋");
            txManager.commit(inner);
    
            log.info("외부 트랜잭션 커밋");
            txManager.commit(outer);
        }
    • txManager.getTransaction(new DefaultTransactionAttribute()): 트랜잭션 매니저를 통해 트랜잭션을 시작(획득)합니다.
    • 외부 트랜잭션은 처음 수행된 트랜잭션으로 신규 트랜잭션(isNewTransaction=true)입니다.
    • 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여하여 신규 트랜잭션(isNewTransaction=false)이 아닙니다.

     

      스프링이 어떻게 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 물리 트랜잭션으로 동작하는지는 실행 결과를 보면 알 수 있습니다.

    더보기

    외부 트랜잭션 시작
    Creating new transaction with name [null]:
    PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    Acquired Connection [HikariProxyConnection@1943867171 wrapping conn0] for JDBC transaction
    Switching JDBC Connection [HikariProxyConnection@1943867171 wrapping conn0] to manual commit
    outer.isNewTransaction()=true

    내부 트랜잭션 시작
    Participating in existing transaction
    inner.isNewTransaction()=false
    내부 트랜잭션 커밋

    외부 트랜잭션 커밋
    Initiating transaction commit
    Committing JDBC transaction on Connection [HikariProxyConnection@1943867171 wrapping conn0]
    Releasing JDBC Connection [HikariProxyConnection@1943867171 wrapping conn0] after transaction

    • 외부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통한 물리 트랜잭션을 시작(manual commit)하고, DB 커넥션을 통해 커밋 하는 것을 확인할 수 있습니다.
    • 내부 트랜잭션을 시작할 때 Participating in existing transaction이라는 메시지를 확인할 수 있습니다. 내부 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 뜻입니다.
    • 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작합니다. 내부 트랜잭션의 경우, 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않습니다. 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나기 때문에 실제 커밋을 호출하면 안됩니다.
    • 스프링은 어려 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 합니다.

     

    @Transactional과 REQUIRED

      트랜잭션 전파의 기본 값은 REQUIRED 입니다. 그러므로 다음 둘은 같습니다.

    @Transactional(propagation = Propagation.REQUIRED)
    @Transactional

     

    롤백(rollback)

      내부 트랜잭션이 커밋되고, 외부 트랜잭션이 롤백되는 상황은 외부 트랜잭션이 물리 트랜잭션을 관리하기에 전체 롤백된다고 쉽게 이해할 수 있습니다. 다음은 내부 트랜잭션이 롤백되고, 외부 트랜잭션이 커밋되는 예제 코드입니다.

        @Test
        void inner_rollback() {
            log.info("외부 트랜잭션 시작");
            TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
    
            log.info("내부 트랜잭션 시작");
            TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
            log.info("내부 트랜잭션 롤백");
            txManager.rollback(inner); //rollback-only 표시
    
            log.info("외부 트랜잭션 커밋");
            assertThatThrownBy(() -> txManager.commit(outer))
                    .isInstanceOf(UnexpectedRollbackException.class);
        }
    더보기

    외부 트랜잭션 시작
    Creating new transaction with name [null]:
    PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    Acquired Connection [HikariProxyConnection@220038608 wrapping conn0] for JDBC transaction
    Switching JDBC Connection [HikariProxyConnection@220038608 wrapping conn0] to manual commit

     

    내부 트랜잭션 시작
    Participating in existing transaction
    내부 트랜잭션 롤백
    Participating transaction failed - marking existing transaction as rollback-only
    Setting JDBC transaction [HikariProxyConnection@220038608 wrapping conn0] rollback-only

     

    외부 트랜잭션 커밋
    Global transaction is marked as rollback-only but transactional code requested commit
    Initiating transaction rollback
    Rolling back JDBC transaction on Connection [HikariProxyConnection@220038608 wrapping conn0]
    Releasing JDBC Connection [HikariProxyConnection@220038608 wrapping conn0] after transaction

    • Participating transaction failed - marking existing transaction as rollback-only: 내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지 않습니다. 대신 기존 트랜잭션을 롤백 전용으로 표시합니다.
    • 외부 트랜잭션은 커밋을 호출했지만, 전체 트랜잭션이 롤백 전용으로 표시되어 있어 물리 트린잭션을 롤백합니다.
    • 스프링은 이 경우 UnexpectedRollbackException 런타임 예외를 던집니다.

     

    스프링 트랜잭션 전파- REQUIRES_NEW

      외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법입니다. 내부 트랜잭션을 시작할 때 REQUIRES_NEW 옵션을 사용하면 됩니다. 별도의 물리 트랜잭션을 가진다는 뜻은 DB 커넥션을 따로 사용한다는 뜻으로, REQUIRES_NEW 옵션을 사용하면 데이터베이스 커넥션이 동시에 2개 사용된다는 점을 주의해야 합니다.

     

      다음은 예제 코드입니다.

        @Test
        void inner_rollback_requires_new() {
            log.info("외부 트랜잭션 시작");
            TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
            log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); //true
    
            log.info("내부 트랜잭션 시작");
            DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
            definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
            TransactionStatus inner = txManager.getTransaction(definition);
            log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); //true
    
            log.info("내부 트랜잭션 롤백");
            txManager.rollback(inner); //롤백
    
            log.info("외부 트랜잭션 커밋");
            txManager.commit(outer); //커밋
        }
    • 외부 트랜잭션은 신규 트랜잭션입니다(outer.isNewTransaction()=true).
    • 내부 트랜잭션은 PROPAGATION_REQUIRES_NEW 옵션을 사용했기 때문에 완전히 새로운 신규 트랜잭션으로 생성됩니다(inner.isNewTransaction()=true).
    • 내부 트랜잭션은 실제 물리 트랜잭션을 롤백하고, 외부 트랜잭션은 실제 물리 트랜잭션을 커밋합니다.

     

    @Transactional과 REQUIRES_NEW

    @Transactional(propagation = Propagation.REQUIRES_NEW)

     

     

    [참고 정보]

    스프링 DB 2편 - 데이터 접근 핵심 원리

    전체 소스코드

    반응형

    'BackEnd > Spring DB' 카테고리의 다른 글

    트랜잭션(Transaction) 옵션  (0) 2023.02.11
    트랜잭션(Transaction) 심화  (0) 2023.02.11
    데이터 접근 기술- 활용  (0) 2023.02.09
    데이터 접근 기술- QueryDSL  (0) 2023.02.04
    데이터 접근 기술- Spring Data JPA  (0) 2023.02.04

    댓글

Designed by Tistory.