트랜잭션 전파(Transaction Propagation) 정의

트랜잭션이 하나 시작한 상태에서 또 다른 트랜잭션을 시작하면 새 트랜잭션을 생성할지, 기존 트랜잭션에 참여할지 등등을 결정하는 방식

Spring에서는 기본적으로 기존 트랜잭션에 참여하는 방식이 기본값이고, 원할 시 다른 방식으로 변경이 가능합니다.


트랜잭션 전파 상황 예시

outer라는 트랜잭션이 하나 시작되고, 커밋이나 롤백으로 트랜잭션이 끝나기 전에 inner라는 트랜잭션이 시작되도록 하였습니다.

@Slf4j  
@SpringBootTest  
public class BasicTxTest {  
	
    @Autowired  
    PlatformTransactionManager txManager;  
	
    @TestConfiguration  
    static class Config {  
	
        @Bean  
        public PlatformTransactionManager transactionManager(DataSource dataSource) {  
            return new DataSourceTransactionManager(dataSource);  
        }  
    }
	
	@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);  
	}
}

[실행 결과]

외부 트랜잭션 시작
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

정리

  1. 내부 트랜잭션이 시작할 때는 트랜잭션이 새로 시작하지 않는다.
  • 먼저 시작한 outer 트랜잭션의 경우 Connection도 할당되고 isNewTransaction()의 값이 true가 나옴
  • 이후에 시작한 inner 트랜잭션의 경우 Participating in existing transaction라는 로그가 나오며 이전의 트랜잭션에 참여가 되고, isNewTransaction()의 값도 false가 나옴
  1. 내부 트랜잭션이 커밋할 때에는 별다른 작업을 하지 않는다.
  • 내부 트랜잭션이 커밋할 때 실제로 해당 커넥션이 커밋되면 트랜잭션이 끝나버리기 때문에 별다른 작업을 하지 않는 것을 로그에서 확인 가능
  • 외부 트랜잭션이 커밋이 되었을 때 실제 커넥션에서 커밋 작업이 이루어지며 트랜잭션이 종료됨

물리/논리 트랜잭션 이해

위 상황처럼 트랜잭션이 시작할 때, 이미 시작되어있는 트랜잭션이 있으면 참여를 하며 하나의 트랜잭션처럼 진행이 됩니다.

위에서 확인했듯이 실제로는 커넥션은 한번만 할당되고 커밋/롤백도 한번만 진행되기에 이 전체를 아우르는 실제 트랜잭션을 물리 트랜잭션이라고 부릅니다.

그리고 트랜잭션 매니저를 통해 하나의 로직을 묶은 트랜잭션 단위를 논리 트랜잭션이라고 부릅니다. 위 경우의 innerouter트랜잭션을 논리 트랜잭션이라고 부를 수 있습니다.


REQUIRED

이 옵션은 기본 옵션으로, PlatformTransactionManager으로 별다른 설정 없이 트랜잭션을 생성하면 해당 옵션이 적용됩니다.

위의 예시가 REQUIRED 옵션을 했을 때의 작업 결과입니다.

해당 옵션은 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 전체 롤백 되는 성질을 갖고 있습니다.

[모든 논리 트랜잭션이 커밋인 경우]

[한 논리 트랜잭션이라도 롤백인 경우]

1. 트랜잭션 생성 시 옵션 지정 방법

transactionManager.getTransaction(new DefaultTransactionAttribute());

2. 요청 흐름

3. 상황별 응답

3-1. 모든 논리 트랜잭션 커밋

내부 트랜잭션에서 commit을 한다고 해도 실제 커넥션에서 커밋을 진행하진 않습니다.

외부 트랜잭션까지 commit이 완료가 다 되어야 트랜잭션 매니저는 커넥션에서 물리적으로 커밋을 진행합니다.

3-2. 외부 트랜잭션이 롤백

내부 트랜잭션에서는 어짜피 commit을 하더라도 실제 물리적인 커밋을 하진 않기 때문에 외부 트랜잭션에서 롤백이 발생하더라도 별다른 작업을 할 필요가 없습니다.

외부 트랜잭션에서 롤백이 발생하면서 커넥션에 물리적인 롤백 작업을 진행하여 물리 트랜잭션에서 롤백을 진행합니다.

3-3. 내부 트랜잭션이 롤백

내부 트랜잭션에서 롤백이 발생하더라도 물리 트랜잭션이 종료된 것은 아니므로 즉시 물리적인 롤백을 수행하면 안 됩니다. 그러나 아무런 조치를 취하지 않으면 외부 트랜잭션은 내부 트랜잭션의 성공 여부를 알 수 없고, 결국 외부 트랜잭션이 커밋될 경우 내부 트랜잭션의 롤백 여부와 관계없이 최종적으로 커밋이 진행될 위험이 있습니다.

이를 방지하기 위해 내부 트랜잭션에서 롤백이 발생하면 rollbackOnly 플래그를 true로 설정합니다. 이후 외부 트랜잭션이 커밋을 요청하더라도, 해당 플래그가 한 번이라도 true로 설정되었다면 트랜잭션 매니저는 물리적인 롤백을 수행합니다.

또한, 클라이언트에게 UnexpectedRollbackException을 던져 커밋을 시도했지만 예상치 못한 롤백이 발생했음을 알립니다.


REQUIRES_NEW

REQUIRED와 달리 각각의 트랜잭션을 완전히 분리하여 별도의 물리 트랜잭션으로 사용하는 방법입니다.
하나라도 롤백이 이루어지면 전체가 롤백이 되는 REQUIRED와 다르게 각각의 트랜잭션의 커밋과 롤백이 별도로 이루어지게 됩니다.

[REQUIRES_NEW 옵션에서 내부 트랜잭션이 롤백일 때]

1. 트랜잭션 생성 시 옵션 지정 방법

getTransaction()에 지정할 TransactionAttribute에 setPropagationBehaviorTransactionDefinition.PROPAGATION_REQUIRES_NEW를 넣어주면 됩니다.

DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
 
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
 
TransactionStatus inner = txManager.getTransaction(definition);

2. 요청 흐름

각 트랜잭션별로 각각의 커넥션이 할당됩니다.

3. 내부 트랜잭션 롤백 시 응답

REQUIRED에서 내부 트랜잭션 롤백 상황과 다르게 각 트랜잭션에게 할당된 커넥션에게 트랜잭션별로 커밋 혹은 롤백을 각각 수행합니다. 때문에, rollbackOnly와 같은 플래그로 외부 트랜잭션에 롤백 여부를 넘기는 작업이 필요없습니다.


기타 다양한 전파 옵션

1. SUPPORT

트랜잭션을 지원하는 상황에 사용

  • 기존 트랜잭션이 없을 때 : 트랜잭션 없이 진행
  • 기존 트랜잭션이 있을 때 : 기존 트랜잭션 참여

2. NOT_SUPPORT

트랜잭션을 지원하지 않는 상황에 사용

  • 기존 트랜잭션이 없을 때 : 트랜잭션 없이 진행
  • 기존 트랜잭션이 있을 때 : 트랜잭션 없이 진행 (기존 트랜잭션은 보류)

3. MANDATORY

트랜잭션이 의무적으로 있어야 하는 상황에 사용

  • 기존 트랜잭션이 없을 때 : IllegalTransactionStateException 예외 발생
  • 기존 트랜잭션이 있을 때 : 기존 트랜잭션 참여

4. NEVER

트랜잭션이 무조건 없어야 하는 상황에 사용

  • 기존 트랜잭션이 없을 때 : 트랜잭션 없이 진행
  • 기존 트랜잭션이 있을 때 : IllegalTransactionStateException 예외 발생

5. NESTED

  • 기존 트랜잭션이 없을 때 : 새 트랜잭션 생성
  • 기존 트랜잭션이 있을 때 : 중첩 트랜잭션 생성

중첩 트랜잭션이란?

중첩 트랜잭션은 롤백되어도 외부 트랜잭션이 커밋할 수 있습니다. 하지만 외부 트랜잭션이 롤백되면 중첩 트랜잭션도 롤백되어야 합니다.