한가지의 서비스에 트랜잭션이 둘 이상 있을때 어떻게 동작할 것이고 어떻게 처리할것인가 에 대한 개념입니다.
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);
}
해당 코드를 실행 하시게 되면 아래와 같은 로그가 뜹니다.
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_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로 전환이 되는것을 확인할 수 있습니다.
트랜잭션 옵션 isolation, timeout, readOnly와 같은 옵션들은 트랜잭션이 처음 시작될 때만 적용됩니다.
따라서 트랜잭션에 참여하는 경우에는 적용되지 않고
REQUIRED 를 통한 트랜잭션 시작, REQUIRES_NEW 를 통한 트랜잭션 시작 시점에만 적용됩니다.
트랜잭션을 분리해야될 상황에 트랜잭션의 REQUIRE_NEW 라는 옵션을 사용해서 분리해도 되지만
구조를 변경할 수 있다면 Facade 클래스를 생성해 분리하는것도 하나의 방법이 됩니다.
reference:
김영한 - 스프링 DB 2편 - 데이터 접근 활용 기술