[Spring]트랜잭션의 전파

스프링에서는 2개의 트랜잭션이 중첩되어 있을 때, 이를 propgation(전파) 옵션으로 제어할 수 있다.

트랜잭션의 중첩은 이미 트랜잭션이 진행중인 상황에서 추가로 트랜잭션을 수행하는 경우를 말한다. 아래와 같은 상황을 말한다.

트랜잭션의 중첩

    @Test
    @DisplayName("중첩된 트랜잭션을 테스트한다.")
    void innerCommit() {
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
        log.info("outer.isNewTransaction() = {}", outer.isNewTransaction());

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

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

/* 로그내용
외부 트랜잭션 시작
Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@1719521616 wrapping conn0: url=jdbc:h2:mem:c66771d4-bd80-4941-9f6f-2f3d9bc59338 user=SA] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@1719521616 wrapping conn0: url=jdbc:h2:mem:c66771d4-bd80-4941-9f6f-2f3d9bc59338 user=SA] to manual commit
outer.isNewTransaction() = true
내부 트랜잭션 시작
Participating in existing transaction
inner.isNewTransaction() = false
내부 트랜잭션 커밋
외부 트랜잭션 커밋
Initiating transaction commit
Committing JDBC transaction on Connection [HikariProxyConnection@1719521616 wrapping conn0: url=jdbc:h2:mem:c66771d4-bd80-4941-9f6f-2f3d9bc59338 user=SA]
Releasing JDBC Connection [HikariProxyConnection@1719521616 wrapping conn0: url=jdbc:h2:mem:c66771d4-bd80-4941-9f6f-2f3d9bc59338 user=SA] after transaction
*/

여기서 외부 트랜잭션은 로그내용 중 Creating new transaction outer.isNewTransaction() = true 라는 것을 통해 신규(물리) 트랜잭션이 생성된 것을 확인할 수 있다. 반면 내부 트랜잭션은 Participating in existing transactioninner.isNewTransaction() = false 를 통해 기존 트랜잭션에 참여한 것을 확인 할 수 있다. 기존 트랜잭션에 참여한다는 의미는 신규(물리) 트랜잭션을 생성하지 않고, 외부 트랜잭션의 커넥션을 사용하겠다는 의미이다.

위에서 내부, 외부 모두 커밋이 발생하여, 최종 물리 트랜잭션은 커밋되고 종료되는 것을 확인 할 수 있다. 그런데 내부 트랜잭션이 커밋을 하였음에도 불구하고, 로그내용을 살펴보면, 실제 물리 트랜잭션에 영향이 없는 것을 알 수 있다. 이는 내부 트랜잭션(isNewTransaction() = false)이 물리 트랜잭션이 아니기 때문이다. 물리트랜잭션이 아닌 경우 커밋을 하여도 아무런 동작을 하지 않는다. 트랜잭션이 진행도중 커밋이나 롤백이 발생하는 경우 해당 트랜잭션은 종료되기 때문에, 실제 DB에 트랜잭션을 적용하는 시점은 물리트랜잭션(isNewTransaction() = true)의 커밋이나 롤백시점이다. 여기서는 외부 트랜잭션이 물리 트랜잭션이다.

외부 트랜잭션의 롤백

    @Test
    @DisplayName("중첩된 트랜잭션 중 외부 트랜잭션에서 롤백하면 물리 트랜잭션도 롤백된다.")
    void outerRollback() {
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());

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

        log.info("외부 트랜잭션 롤백");
        txManager.rollback(outer);
    }

/* 로그내용
외부 트랜잭션 시작
Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@861443773 wrapping conn0: url=jdbc:h2:mem:9d9fd7bd-7d38-48ab-af05-78b0f8ab1d56 user=SA] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@861443773 wrapping conn0: url=jdbc:h2:mem:9d9fd7bd-7d38-48ab-af05-78b0f8ab1d56 user=SA] to manual commit
내부 트랜잭션 시작
Participating in existing transaction
내부 트랜잭션 커밋
외부 트랜잭션 롤백
Initiating transaction rollback
Rolling back JDBC transaction on Connection [HikariProxyConnection@861443773 wrapping conn0: url=jdbc:h2:mem:9d9fd7bd-7d38-48ab-af05-78b0f8ab1d56 user=SA]
Releasing JDBC Connection [HikariProxyConnection@861443773 wrapping conn0: url=jdbc:h2:mem:9d9fd7bd-7d38-48ab-af05-78b0f8ab1d56 user=SA] after transaction
*/

위 코드에서처럼 내부 트랜잭션에서는 커밋, 외부 트랜잭션에서 롤백이 일어난 경우를 살펴보자. 외부 트랜잭션이 롤백되는 경우 내부 트랜잭션이 커밋되었음에도 불구하고, 물리트랜잭션이 롤백되고, 커넥션이 반환되는 것을 알 수 있다. (Rolling back JDBC transaction, Releasing JDBC Connection )

중첩된 트랜잭션에서는 어느 하나라도 논리 트랜잭션이 롤백되면 물리 트랜잭션은 롤백된다. 모든 논리 트랜잭션이 커밋될 경우에만 물리 트랜잭션이 커밋된다.

내부 트랜잭션의 롤백

    @Test
    @DisplayName("중첩된 트랜잭션 중 내부 트랜잭션에서 롤백하면 물리 트랜잭션도 롤백된다.")
    void innerRollback() {
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());

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

        log.info("외부 트랜잭션 커밋");
        Assertions.assertThatThrownBy(() -> txManager.commit(outer))
                .isInstanceOf(UnexpectedRollbackException.class);
    }


/*
로그내용
외부 트랜잭션 시작
Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@2104539672 wrapping conn0: url=jdbc:h2:mem:93841d7d-a69f-4cd7-994f-08e41ed731c9 user=SA] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@2104539672 wrapping conn0: url=jdbc:h2:mem:93841d7d-a69f-4cd7-994f-08e41ed731c9 user=SA] to manual commit
내부 트랜잭션 시작
Participating in existing transaction
내부 트랜잭션 롤백
Participating transaction failed - marking existing transaction as rollback-only
Setting JDBC transaction [HikariProxyConnection@2104539672 wrapping conn0: url=jdbc:h2:mem:93841d7d-a69f-4cd7-994f-08e41ed731c9 user=SA] rollback-only
외부 트랜잭션 커밋
Global transaction is marked as rollback-only but transactional code requested commit
Initiating transaction rollback
Rolling back JDBC transaction on Connection [HikariProxyConnection@2104539672 wrapping conn0: url=jdbc:h2:mem:93841d7d-a69f-4cd7-994f-08e41ed731c9 user=SA]
Releasing JDBC Connection [HikariProxyConnection@2104539672 wrapping conn0: url=jdbc:h2:mem:93841d7d-a69f-4cd7-994f-08e41ed731c9 user=SA] after transaction
*/

위 코드의 경우 내부 트랜잭션은 롤백, 외부 트랜잭션은 커밋된 경우이다.

내부 트랜잭션이 롤백된 후 로그내용에서 Participating transaction failed - marking existing transaction as rollback-only 을 볼 수 있다. 이는 내부 트랜잭션은 참여 트랜잭션(isNewTransaction() = false)으로 실패하였고, 기존 트랜잭션에 rollback-only 라고 표시해 놓겠다는 뜻이다.

비록 외부 트랜잭션에서 커밋으로 정상종료를 하고자 하였지만, 내부 트랜잭션에서 표시해 놓은 rollback-only = true 로 인해 실제 물리트랜잭션에서는 롤백한 것을 확인 할 수 있다.

이 경우 UnexpectedRollbackException.class 이 발생하며, 개발자에게 예외가 발생하였음을 알려주고 있다.

스프링의 트랜잭션 처리 대원칙 “모든 논리 트랜잭션이 commit 되어야 물리 트랜잭션도 commit 된다. 어느 하나라도 논리 트랜잭션이 rollback 되는 경우, 물리 트랜잭션은 rollback 된다”라는 것이 잘 지켜짐을 알 수 있다.

하지만, 경우에 따라서는 중첩 트랜잭션에서도 다른 트랜잭션의 rollback 에 상관없이 commit을 하고 싶은 경우도 있다. 이러한 경우 내부 트랜잭션에서 신규 트랜잭션을 생성(물리 트랜잭션)하여 해결할 수 있다. 이 때 사용하는 옵션이 “REQUIRES_NEW” 이다.

내부 트랜잭션의 신규 트랜잭션 생성

    @Test
    @DisplayName("중첩된 트랜잭션 중 내부 트랜잭션을 신규 생성하고 내부트랜잭션에서 롤백하면, 내부트랜잭션만 롤백된다.")
    void innerRollbackRequireNew() {
        log.info("외부 트랜잭션 시작");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
        log.info("outer.isNewTransaction() = {}", outer.isNewTransaction());

        log.info("내부 트랜잭션 시작");
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        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);
    }

/*
외부 트랜잭션 시작
Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@321255783 wrapping conn0: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@321255783 wrapping conn0: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA] to manual commit
outer.isNewTransaction() = true
내부 트랜잭션 시작
Suspending current transaction, creating new transaction with name [null]
Acquired Connection [HikariProxyConnection@1541107368 wrapping conn1: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@1541107368 wrapping conn1: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA] to manual commit
inner.isNewTransaction() = true
내부 트랜잭션 롤백
Initiating transaction rollback
Rolling back JDBC transaction on Connection [HikariProxyConnection@1541107368 wrapping conn1: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA]
Releasing JDBC Connection [HikariProxyConnection@1541107368 wrapping conn1: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA] after transaction
Resuming suspended transaction after completion of inner transaction
외부 트랜잭션 커밋
Initiating transaction commit
Committing JDBC transaction on Connection [HikariProxyConnection@321255783 wrapping conn0: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA]
Releasing JDBC Connection [HikariProxyConnection@321255783 wrapping conn0: url=jdbc:h2:mem:b4a5617f-590a-4eca-85d8-27a1a7e7a4ad user=SA] after transaction

*/

위 코드에서 내부 트랜잭션을 가져올 때 “REQUIRES_NEW” 옵션을 사용하면, 신규 물리트랜잭션(isNewTransaction() = true)을 생성하는 것을 알 수 있다. 내부 트랜잭션에서 신규 트랜잭션이 생성되면, 외부 트랜잭션 1개, 내부 트랜잭션 1개 총 2개의 물리트랜잭션이 생성된다. 내부 트랜잭션이 사용되는 도중에는 외부 트랜잭션은 대기상태(suspending)가 되고, 내부 트랜잭션이 종료(커밋 또는 롤백)되면 재개(Resuming) 된다. 또한 개별적인 물리트랜잭션으로 구성되어 있어 내부 트랜잭션이 롤백되면, 바로 DB에 트랜잭션이 적용되고 커넥션이 반환되는 것을 알 수 있다.

스프링에서의 트랜잭션 전파의 옵션에 대해서는 다음 링크를 통해서 자세히 알아보자.

[Spring]트랜잭션의 전파옵션

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다