본문 바로가기
Spring

트랜잭션 전파(REQUIRED) COMMIT편

by sangyunpark99 2025. 2. 12.
진행중인 트랜잭션 내부에 또 다른 트랜잭션을 실행하면 어떻게 될까?

 

이번글은 트랜잭션 전파에 대한 개념과 코드 예제를 통해 흐름을 알아보겠습니다.

트랜잭션 전파 옵션은 여러개가 있지만, REQUIRED를 기준으로 작성했습니다.

 

트랜잭션 내부에 또 다른 트랜잭션을 실행하는 경우, 기존 트랜잭션을 사용해야할까요? 아니면 새로운 트랜잭션을 시작해야할까요?

 

트랜잭션 전파

트랜잭션 전파는 현재 실행 중인 트랜잭션이 있을 때, 새로운 트랜잭션을 생성할지 기존 트랜잭션을 그대로 사용할지를 결정하는 방식입니다.

 

트랜잭션 전파는 어떠한 방식으로 사용될까요?

 

외부 트랜잭션이 수행중인 상태에서 내부 트랜잭션이 추가 수행되는 상황을 가정합니다.

 

왜 외부 트랜잭션, 내부 트랜잭션이라 부르나요?

 

상대적으로 바깥쪽에 있는 트랜잭션은 외부 트랜잭션이고, 안쪽에 있는 트랜잭션은 내부 트랜잭션입니다.

쉽게 말해, 위 맥락에서 제일 처음에 실행된 트랜잭션이 외부 트랜잭션이고 그 다음 실행된 트랜잭션이 내부 트랜잭션입니다.

 

 위의 흐름대로라면 트랜잭션은 어떻게 되나요?

 

스프링에서는 외부 트랜잭션과 내부 트랜잭션을 하나의 트랜잭션으로 묶어 관리합니다. 즉, 내부 트랜잭션이 외부의 트랜잭션에 참여하게 되는 것입니다. 

 

스프링은 이러한 상황을 더 이해하기 쉽게 하기 위해 물리 트랜잭션과 논리 트랜잭션으로 나눕니다.

물리 트랜잭션과 논리 트랜잭션은 무엇일까요?

 

물리 트랜잭션 은 실제 DB에 적용되는 트랜잭션을 의미합니다. 커넥션을 사용해서 DB의 트랜잭션을 시작하고 커밋, 롤백을 하는 단위를 나타냅니다.

논리 트랜잭션은 스프링의 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위입니다.

 

단, 논리와 물리 트랜잭션이라는 개념은 트랜잭션이 한개일때는 다룰 필요가 없습니다.

 

왜 물리 트랜잭션과 논리 트랜잭션을 나누게 될까요?

 

논리 트랜잭션과 물리 트랜잭션을 구분하면, 트랜잭션이 진행 중인 상태에서 또 다른 트랜잭션을 실행해야 하는 복잡한 상황에서도 명확한 원칙을 세울 수 있습니다. 이러한 원칙을 정하면 복잡한 트랜잭션 처리 문제를 효과적으로 해결할 수 있습니다. 원칙은 아래와 같습니다.

  • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됩니다.
  • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됩니다.
예시 코드를 통해 트랜잭션 전파를 알아보겠습니다.

 

예시 코드

예시 코드는 트랜잭션 전파의 기본 옵션인 `REQUIRED를 기준으로 설명합니다.

 

REQUIRED 옵션은 다음 2가지를 따릅니다.

  • 기존 트랜잭션이 존재하면 해당 트랜잭션을 그대로 사용합니다.
  • 기존 트랜잭션이 없으면 새로운 트랜잭션을 생성합니다.

 

외부 트랜잭션 수행중 내부 트랜잭션이 만들어지는 경우

 

먼저, 외부 트랜잭션 수행중 내부 트랜잭션이 만들어지는 경우, 내부 트랜잭션이 외부 트랜잭션을 그대로 사용하는지 테스트하겠습니다.

테스트 코드는 아래와 같습니다.

@Slf4j
@SpringBootTest
public class TxPropagationTest {

    @TestConfiguration
    static class Config {
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }

    @Autowired
    private PlatformTransactionManager tx;

    @Test
    void outer_inner_commit() {
        log.info("outer 트랜잭션 시작");
        TransactionStatus outerStatus = tx.getTransaction(new DefaultTransactionAttribute());
        log.info("outer 트랜잭션 is New ? {}", outerStatus.isNewTransaction() ? "✅" : "❌");

        log.info("inner 트랜잭션 시작");
        TransactionStatus innerStatus = tx.getTransaction(new DefaultTransactionAttribute());
        log.info("inner 트랜잭션 is New ? {}", innerStatus.isNewTransaction() ? "✅" : "❌");

    }
}

 

출력 결과

 

외부 트랜잭션은 새로운 트랜잭션이지만, 내부 트랜잭션은 별도의 트랜잭션이 아닙니다. 아직 논리 트랜잭션이 커밋되거나 롤백되지 않았기 때문에, 물리 트랜잭션이 실제로 실행되지 않습니다. 또한, 내부 트랜잭션이 외부 트랜잭션을 이어받아 사용하기 때문에, 트랜잭션은 로그에서 알 수 있듯이 1개의 새로운 트랜잭션만 생성되는 것을 확인할 수 있습니다.

 

다음으로 내부 트랜잭션과 외부 트랜잭션을 커밋해보겠습니다.

(외부 트랜잭션과 내부 트랜잭션을 리펙토링하여 분할하였습니다.)

@Test
void outer_transaction() {
    log.info("outer 트랜잭션 시작");
    TransactionStatus outerStatus = tx.getTransaction(new DefaultTransactionAttribute());
    log.info("outer 트랜잭션 is New ? {}", outerStatus.isNewTransaction() ? "✅" : "❌");
    inner_transaction();
    log.info("outer 트랜잭션 커밋");
    tx.commit(outerStatus);
}

private void inner_transaction() {
     log.info("inner 트랜잭션 시작");
     TransactionStatus innerStatus = tx.getTransaction(new DefaultTransactionAttribute());
     log.info("inner 트랜잭션 is New ? {}", innerStatus.isNewTransaction() ? "✅" : "❌");
     log.info("inner 트랜잭션 커밋");
     tx.commit(innerStatus);
}

 

출력 결과

 

논리적 트랜잭션이 전부 commit을 했기 때문에, 물리 트랜잭션이 커밋이되고, 최종적으로 커넥션을 반납하게 됩니다.

즉, 내부 트랜잭션이 외부 트랜잭션에 참여하게 되어 내부 트랜잭션과 외부 트랜잭션 전체가 하나의 큰 트랜잭션으로 묶이게 됩니다.  

 

트랜잭션을 참여한다는게 무슨 뜻일까요?

 

로그를 보면 Participating in existing transaction라는 로그가 찍힌걸 확인할 수 있습니다.

트랜잭션을 참여한다는 것은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어서 사용한다는 의미입니다.

즉, 외부 트랜잭션과 내부 트랜잭션이 하나의 거대한 물리 트랜잭션으로 묶이는 것을 의미합니다.

 

manual commit은 무엇을 의미할까요?

 

로그를 잘 보면 manual commit이 보입니다. manual commit은 수동 커밋을 한다는 의미입니다.

set autocommit=false로 한 것과 같습니다. 

 

내부 트랜잭션을 커밋하면 물리 트랜잭션이 종료될까요?

 

로그에서 볼 수 있듯이 outer 트랜잭션 커밋을 하는 경우  Initiating transaction commit을 하게 됩니다.

즉, 외부 트랜잭션 커밋시에만 실제 커밋을 한다는 의미입니다. 내부 트랜잭션에서는 커밋을 해도 아무 일도 발생하지 않습니다.

물리 트랜잭션이 종료되기 위해선 실제 커밋이 발생해야 합니다.

 

위 흐름을 그림으로 나타내면 다음과 같습니다.

요청 흐름과 응답 흐름을 순서대로 보겠습니다.

먼저, 외부 트랜잭션 요청 흐름은 다음과 같습니다.

 

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

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

3. 생성된 커넥션을 setAutoCommit(false)를 선언하여 수동 모드로 바꿉니다.

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

 

트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus 에 담아서 반환합니다.

외부 트랜잭션에서 새로운 트랜잭션을 생성했으므로 isNewTransaction의 값이 true가 됩니다.

 

트랜잭션 동기화 매너저가 뭘까요?

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/support/TransactionSynchronizationManager.html

 

TransactionSynchronizationManager (Spring Framework 6.2.2 API)

Return whether there currently is an actual transaction active. This indicates whether the current thread is associated with an actual transaction rather than just with active transaction synchronization. To be called by resource management code that wants

docs.spring.io

 

공식문서에 의하면 "트랜잭션 동기화 매니저는 스레드별로 리소스와 트랜잭션 동기화를 관리하는 중앙 위임자(Central Delegate) 역할을 하고, 트랜잭션 동기화가 활성화된 경우, 트랜잭션 동기화 객체 리스트를 관리할 수 있습니다."라고 설명되어 있습니다.

 

동기화는 무엇일까요?

 

여러 작업이 일정한 순서나 규칙에 따라 일관되게 실행되도록 맞추는 과정을 의미합니다.

 

트랜잭션 동기화는 무엇이고, 왜 필요할까요?

 

정리하면, 트랜잭션 동기화는 트랜잭션의 시작과 종료 시점에 맞춰 트랜잭션과 관련된 리소스(JDBC 커넥션, Hibernate 세션 등)를 자동으로 바인딩하고 정리하는 과정을 나타냅니다.

 

 

트랜잭션 동기화가 필요한 이유는 다음과 같습니다.

 

1. JDBC 커넥션 같은 리소스를 트랜잭션 범위 내에서 재사용하기 위해서 사용합니다.

예를 들어, 하나의 트랜잭션 내에서 같은 JDBC Connection을 여러 번 사용할 때, 매번 새로운 커넥션을 생성하는 것이 아니라 같은 커넥션을 유지해야 합니다.

 

2. 트랜잭션이 끝나면 자동으로 리소스를 해제해야 할때 사용합니다.

예를 들어, 트랜잭션이 커밋되면 커넥션을 닫거나, 롤백되면 트랜잭션과 관련된 정리 작업을 수행해야 합니다.

 

 

 

 

다음으로, 내부 트랜잭션 요청 흐름을 보겠습니다. 내부 트랜잭션 요청 흐름은 다음과 같습니다.

 

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

2. 트랜잭션 매니저가 트랜잭션 동기화 매니저를 통해 기존 트랜잭션이 존재하는지 확인합니다.

3. 외부 트랜잭션에서 생성된 기존 트랜잭션이 존재하기 때문에, 기존 트랜잭션에 참여하게 됩니다.

4. 기존 트랜잭션에 참여하게 된 내부 트랜잭션 로직은 이전 외부 트랜잭션에 의해 생성되고 트랜잭션 동기화 매니저가 보관하고 있는 커넥션을 사용하게 됩니다.

 

외부 트랜잭션과 마찬가지로 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus 에 담아서 반환합니다.

외부 트랜잭션에서 생성한 기존 트랜잭션을 참여했으므로 isNewTransaction의 값이 false가 됩니다.

 

이제 응답 흐름을 보도록 하겠습니다.

외부 트랜잭션 요청 → 내부 트랜잭션 요청 → 내부 트랜잭션 응답 → 외부 트랜잭션 응답

 

내부 트랜잭션 응답이 먼저 진행되니, 내부 트랜잭션 응답 흐름을 보겠습니다. 흐름은 다음과 같습니다.

1. 내부 트랜잭션에서 로직2가 종료되고, 트랜잭션 매니저를 통해서 내부 트랜잭션을 커밋합니다.
2. 현재 내부 트랜잭션은 새로운 트랜잭션이 아닌, 외부 트랜잭션에서 생성된 트랜잭션을 사용중이므로 실제 커밋을 호출하지 않습니다.
(실제 커밋이 호출되면, 외부 트랜잭션에서 생성한 물리 트랜잭션이 종료됩니다.)

 

 

다음은 외부 트랜잭션 응답 흐름을 보겠습니다. 흐름은 다음과 같습니다. 

1. 로직1이 종료되고, 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋합니다.
2. 외부 트랜잭션은 새로운 트랜잭션이므로 커밋 요청시 DB 커넥션에 실제 커밋을 호출합니다.
3. 실제 커밋을 하면, DB에 커밋이 반영이되고, 물리 트랜잭션도 종료됩니다.

 

정리하면, 새로운 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행합니다. 새로운 트랜잭션이 아니면 실제 물리 커넥션을 사용하지 않습니다. 즉, 내부 트랜잭션이 실행되면 트랜잭션 매니저가 논리 트랜잭션을 관리하며, 모든 논리 트랜잭션이 커밋되어야 최종적으로 물리 트랜잭션이 커밋됩니다.

 

정리

  • 트랜잭션 전파는 현재 실행 중인 트랜잭션이 있을 때, 새로운 트랜잭션을 만들지 기존 트랜잭션을 그대로 사용할지를 결정하는 방식입니다.
  • REQUIRED 옵션을 사용하면, 내부 트랜잭션은 외부 트랜잭션과 하나의 트랜잭션으로 묶여 동작합니다.
  • 모든 논리 트랜잭션이 커밋되어야 최종적으로 물리 트랜잭션이 커밋됩니다.
  • 내부 트랜잭션을 커밋해도 실제 커밋이 수행되지 않으며, 외부 트랜잭션이 커밋될 때 최종적으로 DB에 반영됩니다.

'Spring' 카테고리의 다른 글

트랜잭션과 Connection  (0) 2025.02.11