Spring - Transaction propagation(트랜잭션 전파)

salgu·2022년 11월 13일
0

Spring

목록 보기
2/22

Transaction propagation 이란

한가지의 서비스에 트랜잭션이 둘 이상 있을때 어떻게 동작할 것이고 어떻게 처리할것인가 에 대한 개념입니다.

  • 해당 사진에서 로직1은 외부 트랜잭션이고 로직2는 내부에서 다시 트랜잭션이 걸린 로직이라고 가정하겠습니다.
  • 물리 트랜잭션은 실제 커넥션을 통해 커밋, 롤백하는 일반적인 트랜잭션 단위입니다.
  • 논리 트랜잭션은 트랜잭션 매니저를 통해 사용하는 트랜잭션 단위입니다.

기본 옵션 : REQUIRED

REQUIRE의 기본 원칙은

  • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됩니다.
  • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됩니다.
    @Test
    void innerCommit() {
        log.info("outer tx start");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction() => {}", outer.isNewTransaction());

        inner();

        log.info("outer tx commit");
        txManager.commit(outer);
    }

    private void inner() {
        log.info("inner tx start");
        TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("inner.isNewTransaction() => {}", inner.isNewTransaction());

        log.info("inner tx commit");
        txManager.commit(inner);
    }
  • txManager.getTransaction(new DefaultTransactionAttribute()) : 트랜잭션 시작
  • isNewTransaction() : 처음 수행된 트랜잭션 확인 메소드(true, false)

해당 코드를 실행 하시게 되면 아래와 같은 로그가 뜹니다.

outer는 새로운 트랜잭션을 시작했기 때문에 true가 반환되었고
inner는 새로운 트랜잭션을 시작하지 않고 외부에 있는 outer 트랜잭션을 받아

Participating in existing transaction 

로그가 나왔습니다
해당 로그가 의미 하는것은 새로운 트랜잭션을 생성하지 않고 outer에서 시작했던 트랜잭션에 참여하겠다는 뜻입니다.

그 후 inner tx commit로그가 나왔지만 실제로 commit되었던 로그는 나오지 않고 outer tx commit이 호출 되었을때 트랜잭션 커밋로그가 나왔습니다.
왜냐하면 inner 트랜잭션이 커밋을 해버리면 outer 로직이 끝나기 전에 커밋되어버리면 안되기 때문에 내부 트랜잭션은 커밋을 할 수 없고 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 하고 이를 통해 트랜잭션 중복 커밋 문제를 해결합니다.

여기서 inner tx가 롤백되고 outer tx가 커밋이 된다면 어떻게 동작하는지 알아보겠습니다.

    @Test
    void innerRollback() {
        log.info("outer tx start");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction() => {}", outer.isNewTransaction());

        log.info("inner tx start");
        TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("inner.isNewTransaction() => {}", inner.isNewTransaction());

        log.info("inner tx rollback");
        txManager.rollback(inner);    // rollback-only mark

        log.info("outer tx commit");
        Assertions.assertThatThrownBy(() -> txManager.commit(outer))
                .isInstanceOf(UnexpectedRollbackException.class);
    }

해당 테스트를 돌려보시면

inner tx는

Participating in existing transaction

로그와 함께 기존에 있던 트랜잭션에 정상적으로 참여를 하고 롤백을 했고
외부 트랜잭션이 커밋을 한다면 UnexpectedRollbackException이 뜨면서 롤백되게 됩니다.

왜냐하면 inner tx가 물리 트랜잭션에 롤백을 호출하지 않고

Participating transaction failed - marking existing transaction as rollback-only 

로그와 같이 트랜잭션 동기화 매니저에 rollbackOnly를 true로 설정하여 물리 트랜잭션은 무조건 롤백이 될수밖에 없도록 해주기 때문입니다.

REQUIRE 동작과정


옵션 : REQUIRE_NEW

REQUIRE_NEW 옵션은 트랜잭션이 진행되던중 해당 옵션을 가진 트랜잭션이 inner 에서 시작될때
새로운 커넥션을 가져와 독립적으로 트랜잭션이 이루어 집니다.
기본옵션인 REQUIRE옵션에서는 inner tx가 롤백이 된다면 outer tx도 롤백이 되었지만
REQUIRE_NEW옵션은 inner tx의 커넥션이 새로 생성되기 때문에 inner tx가 롤백이 되어도
outer tx는 롤백이 되지않고 따로 관리됩니다.

	@Test
    void innerRollbackRequireNew() {
        log.info("outer tx start");
        TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
        log.info("outer.isNewTransaction() => {}", outer.isNewTransaction());

        log.info("inner tx start");
        DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus inner = txManager.getTransaction(definition);
        log.info("inner.isNewTransaction() => {}", inner.isNewTransaction());

        log.info("inner tx rollback");
        txManager.rollback(inner);

        log.info("outer tx commit");
        txManager.commit(outer);
    }

REQUIRE_NEW 옵션을 가진 inner tx가 롤백이되고 outer tx는 커밋이 되는 테스트 코드입니다.

돌려보시면 정상적으로 실행이 되었고 outer tx와 inner tx의 isNewTransaction() 값이 둘다 true인 것을 확인할 수 있습니다.
그리고 로그를 확인해보시면 inner tx가 실행될때

Suspending current transaction, creating new transaction with name [null]

outer tx가 Suspending되는 것을 확인할 수 있고 inner tx가 종료되자

Resuming suspended transaction after completion of inner transaction

다시 outer tx로 전환이 되는것을 확인할 수 있습니다.

REQUIRE_NEW 동작과정

알아야될 사항

트랜잭션 옵션 isolation, timeout, readOnly와 같은 옵션들은 트랜잭션이 처음 시작될 때만 적용됩니다.
따라서 트랜잭션에 참여하는 경우에는 적용되지 않고
REQUIRED 를 통한 트랜잭션 시작, REQUIRES_NEW 를 통한 트랜잭션 시작 시점에만 적용됩니다.

선택

트랜잭션을 분리해야될 상황에 트랜잭션의 REQUIRE_NEW 라는 옵션을 사용해서 분리해도 되지만
구조를 변경할 수 있다면 Facade 클래스를 생성해 분리하는것도 하나의 방법이 됩니다.





reference:
김영한 - 스프링 DB 2편 - 데이터 접근 활용 기술

profile
https://github.com/leeeesanggyu, leeeesanggyu@gmail.com

0개의 댓글