본문 바로가기
Spring

트랜잭션 전파(REQUIRES_NEW)

by sangyunpark99 2025. 2. 14.
내부 트랜잭션에 롤백이 발생해도 외부 트랜잭션을 커밋 하는 방법은 없을까?

 

 

이번 글은 트랜잭션 전파 옵션 중 REQUIRES_NEW에 대해 소개하고, 예제 코드를 통해 알아보겠습니다.

아래의 글은 김영한님의 Spring DB V2 강의를 참고해서 작성했습니다.

 

REQUIRES_NEW 옵션은 무엇일까요?

 

REQUIRES_NEW 옵션

REQUIRES_NEW 옵션은 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법입니다. 그래서 커밋과 롤백도 REQUIRES 옵션과 다르게 각각 실행합니다.

 

내부 트랜잭션에 롤백이 발생해도 외부 트랜잭션에 영향을 주지 않게 됩니다. 반대로 외부 트랜잭션에 롤백이 발생해도 내부 트랜잭션에 영향을 주지 않게 됩니다. 별도의 트랜잭션을 사용하기 때문에 서로 다른 DB Connection을 사용하게 됩니다. 

 

어떻게 사용할까요?

 

내부 트랜잭션에 REQUIRES_NEW 옵션을 넣어주면 됩니다. 코드를 통해 확인해보겠습니다.

    @Test
    void inner_transaction_rollback_option_requires_new() {
        log.info("1️⃣outer 트랜잭션 시작");
        TransactionStatus outerStatus = tx.getTransaction(new DefaultTransactionAttribute());
        log.info("1️⃣outer isNewTransaction = {}", outerStatus.isNewTransaction());

        log.info("2️⃣inner 트랜잭션 시작");
        DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus innerStatus = tx.getTransaction(definition);
        log.info("2️⃣inner isNewTransaction = {}", innerStatus.isNewTransaction());
        log.info("2️⃣inner 트랜잭션 롤백");
        tx.rollback(innerStatus);

        log.info("1️⃣outer 트랜잭션 커밋");
        tx.commit(outerStatus);
    }

 

 

아래 코드(setter)를 통해서 트랜잭션 전파 속성을 REQUIRES_NEW 속성으로 변경해줍니다.

DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

 

참고로 propagationBehavior의 기본 값은 PROPAGATION_REQUIRED입니다.

 

public class DefaultTransactionDefinition implements TransactionDefinition, Serializable {

	// ...

    static final Constants constants = new Constants(TransactionDefinition.class);

	private int propagationBehavior = PROPAGATION_REQUIRED;

	private int isolationLevel = ISOLATION_DEFAULT;

	private int timeout = TIMEOUT_DEFAULT;

	private boolean readOnly = false;
    
    // ...

 

출력 결과

 

로그의 흐름은 다음과 같습니다.

  1. 외부 트랜잭션이 시작되고, 커넥션 풀에서 커넥션(conn0)을 획득하고, manual commit으로 변경해서 물리 트랜잭션을 시작합니다.
  2. 내부 트랜잭션이 시작되고, 커넥션 풀에서 커넥션(conn1)을 획득하고, manual commit으로 변경해서 물리 트랜잭션을 시작합니다. 내부 트랜잭션은 외부트랜잭션에 참여하지 않고, 새로운 트랜잭션을 생성한 후 사용합니다. (Suspending 외부 트랜잭션은 잠시 미뤄둡니다. - conn0은 살아있습니다.)
  3. 내부 트랜잭션이 롤백됩니다. 내부 트랜잭션은 새로 생성한 트랜잭션이므로, DB 커넥션(conn1)을 활용하여 실제 롤백을 수행하게 됩니다. 
  4. 외부 트랜잭션이 커밋됩니다. 외부 트랜잭션도 마찬가지로 새로 생성한 트랜잭션이므로, DB 커넥션(conn0)을 활용하여 실제 커밋을 수행하게 됩니다. (Resuming 내부 트랜잭션 생성시 미뤄두었던 외부 트랜잭션을 사용합니다.)

서로 다른 트랜잭션이므로 각각 conn0과 conn1으로 서로 다른 커넥션을 사용하는 것을 확인할 수 있습니다.

 

 

위의 플로우중 요청 플로우를 그림으로 나타내면 다음과 같습니다.

요청 흐름 이미지

요청 플로우과 응답 플로우중 요청 플로우를 먼저 보겠습니다.

먼저, 외부 트랜잭션 요청 플로우는 다음과 같습니다.

 

1. 트랜잭션 매니저를 호출해 외부 트랜잭션을 시작합니다.

2. 트랜잭션 매니저가 데이터 소스를 통해서 커넥션을 생성합니다.

3. 생성된 커넥션을 setAutoCommit(false)를 선언하여 수동 모드로 바꿉니다. (물리 트랜잭션이 시작됩니다.)

4. 트랜잭션 매니저는 트랜잭션 동기화 매니저에게 커넥션을 보관합니다.

 

내부 트랜잭션 요청 플로우는 다음과 같습니다.

 

1. REQUIRES_NEW 옵션으로 트랜잭션 매니저를 호출해 내부 트랜잭션을 시작합니다. (트랜잭션 매니저가 옵션을 확인하고, 새로운 트랜잭션을 시작)

2. 트랜잭션 매니저가 데이터 소스를 통해서 커넥션을 생성합니다.

3. 생성된 커넥션을 setAutoCommit(false)를 선언하여 수동 모드로 바꿉니다. (물리 트랜잭션이 시작됩니다.)

4.  트랜잭션 매니저는 트랜잭션 동기화 매니저에게 커넥션을 보관합니다.

 

응답 플로우를 나타낸 그림은 다음과 같습니다.

응답 플로우 이미지

먼저, 내부 트랜잭션 응답 플로우는 다음과 같습니다.

 

1. 로직2의 오류로 인해 트랜잭션 매니저를 통해서 롤백을 호출합니다.

2. 트랜잭션 매니저는 롤백 시점에 새로 생성한 트랜잭션임을 확인하고 물리 트랜잭션의 실제 롤백을 호출합니다.

3. 내부 트랜잭션이 DB 커넥션(conn1)을 사용해 물리 트랜잭션을 롤백합니다.

4. 트랜잭션이 종료되고, conn1은 커넥션 풀에 반납합니다.

 

외부 트랜잭션 응답 플로우는 다음과 같습니다.

 

1. 로직1이 끝나고, 외부 트랜잭션에서 커밋을 호출합니다.

2. 외부 트랜잭션도 신규 트랜잭션이므로 물리 트랜잭션의 실제 커밋을 호출합니다. ( rollbackOnly 옵션 설정을 체크해서 false이므로 실제 커밋을 진행합니다.)

3. DB 커넥션 (conn0)을 사용해 물리 트랜잭션을 커밋합니다.

4. 트랜잭션이 종료되고, conn0은 커넥션 풀에 반납합니다.

 

 

정리

  • REQUIRES_NEW 옵션을 사용하면 물리 트랜잭션이 분리됩니다.
  • 외부 트랜잭션과 내부 트랜잭션이 각각 서로 다른 DB 커넥션을 사용하여, DB 커넥션이 빨리 고갈될 수 있습니다.

'Spring' 카테고리의 다른 글

트랜잭션 전파(REQUIRED) ROLLBACK 편  (0) 2025.02.13
트랜잭션 전파(REQUIRED) COMMIT편  (0) 2025.02.12
트랜잭션과 Connection  (0) 2025.02.11