관리 메뉴

나구리의 개발공부기록

스프링 트랜잭션 전파 - 기본, 커밋/롤백, 트랜잭션 두 번 사용, 전파 기본, 전파 예제, 외부 롤백, 내부 롤백, REQUIRES_NEW, 다양한 전파 옵션 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 DB 2편 - 데이터 접근 활용

스프링 트랜잭션 전파 - 기본, 커밋/롤백, 트랜잭션 두 번 사용, 전파 기본, 전파 예제, 외부 롤백, 내부 롤백, REQUIRES_NEW, 다양한 전파 옵션

소소한나구리 2024. 9. 25. 16:40

  출처 : 인프런 - 스프링 DB 2편 데이터 접근 핵심 원리 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2


1. 스프링 트랜잭션 전파1 - 커밋, 롤백

1) BasicTxTest

  • 기본적으로 트랜잭션을 실행하는 예제 코드
  • new DataSourceTransactionManager를 스프링 빈으로 등록하고 PlatformTransactionManager를 주입받으면 DataSourceTransactionManager가 주입됨

(1) commit(), rollback() 메서드

  • txManager.getTransaction(new DefaultTransactionAttribute())로 트랜잭션을 시작
  • 기존에 배웠던 개념대로 각 메서드를 실행하면 트랜잭션이 커밋되고, 커밋이 완료되는 로그를 볼 수 있음
package hello.springtx.propagation;

@Slf4j
@SpringBootTest
public class BasicTxTest {

    @Autowired
    PlatformTransactionManager txManager;

    @TestConfiguration
    static class BasicTxTestConfig {
        // 원래는 스프링 부트가 알아서 트랜잭션 매니저를 선택하지만, 직접 Bean으로 생성하면 직접 생성한 TransactionManager가 등록됨
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }

    @Test
    void commit() {
        log.info("트랜잭션 시작");
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());

        log.info("트랜잭션 커밋 시작");
        txManager.commit(status);
        log.info("트랜잭션 커밋 완료");
    }

    @Test
    void rollback() {
        log.info("트랜잭션 시작");
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());

        log.info("트랜잭션 롤백 시작");
        txManager.rollback(status);
        log.info("트랜잭션 롤백 완료");
    }
}

 

(2) 실행 로그

  • 실행 로그를 보기 위해서는 application.properties에 log 확인을 위한 설정을 추가하고 테스트를 실행해야함
logging.level.org.springframework.transaction.interceptor=TRACE

# 트랜잭션이 커밋되었는지 롤백 되었는지 로그로 확인
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG

# JPA를 사용하므로 JpaTransactionManager의 로그를 확인
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG

# JPA SQL
logging.level.org.hibernate.SQL=DEBUG

좌) 트랜잭션 커밋이 된 로그 내역 / 우) 트랜잭션 롤백이 된 로그 내역


2. 스프링 트랜잭션 전파2 - 트랜잭션 두 번 사용

1) double_commit() 메서드 추가

  • 트랜잭션이1이 커밋으로 완전히 끝난 후 트랜잭션2가 시작되고 커밋 후 종료
  • 우리가 아는대로 트랜잭션이 2번 실행 됨
@Test
void double_commit() {
    log.info("트랜잭션1 시작");
    TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("트랜잭션1 커밋");
    txManager.commit(tx1);

    log.info("트랜잭션2 시작");
    TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("트랜잭션2 커밋");
    txManager.commit(tx2);
}

 

(1) 실행 로그 확인

  • 트랜잭션 1이 시작하고 히카리커넥션 풀에서 conn0 커넥션을 획득 -> 커밋하고 커넥션풀에 conn0 커넥션 반납
  • 트랜잭션 2가 시작하고 히카리커넥션 풀에서 conn0 커넥션을 획득 -> 커밋하고 커넥션 풀에 conn0 커넥션 반납
  • 같은 conn0 커넥션을 썻기 때문에 동일한 트랜잭션이라고 생각할 수 있지만, 커넥션 풀에서 커넥션이 관리되고 해당 커넥션을 재사용 한것이지 각 커넥션이 같은 트랜잭션의 범위에 있는 것이 아니라 각각의 트랜잭션임

** 주의!

  • 여기서 conn0 커넥션은 커넥션 풀에 있는 커넥션을 재사용 한 것이지만, 트랜잭션1이 conn0 커넥션을 사용 후 완전히 반납하였음
  • 그 이후에 트랜잭션2이 커넥션 풀에 있는 conn0 커넥션을 재사용 한 것이기 때문에 이 둘은 같은 트랜잭션의 커넥션이 아니라 완전히 다른 커넥션으로 인지 해야함
  • 우리가 직접 데이터소스를 만들지 않았기 때문에 스프링이 데이터소스를 만들어서 자동으로 주입해주는데, 이때 히카리 커넥션 풀을 사용했기 때문에 이런 로그가 보인것이지, 만약 직접 데이터 소스를 직접 만들어서 빈에 등록했다면 conn0, conn1로 실제 커넥션이 다른 것을 확인할 수 있음 - 과거 수업에 내용이 있음
  • https://nagul2.tistory.com/306
  • 히카리 커넥션 풀에서 커넥션을 획득하면 실제 커넥션을 그대로 반환하는 것이 아니라 내부 관리를 위해 히카리 프록시 라는 객체를 생성해서 반환(내부에 실제 커넥션이 포함되어있음)하는데, 이 객체의 참조 주소를 확인하면 커넥션 풀에서 획득한 커넥션이 같은지 다른지를 구분할 수 있음
    • 해당 로그를 보면 히카리프록시커넥션의 참조 주소가 서로 다름
    • 트랜잭션1의 히카리프록시커넥션: HikariProxyConnection@1023469953
    • 트랜잭션2의 히카리프록시커넥션: HikariProxyConnection@1673618401
  • 결과적으로 conn0 커넥션은 커넥션 풀을 사용했기 때문에 재사용 된것일 뿐이고, 각각의 커넥션은 완전히 다른 커넥션임

 

  • 트랜잭션이 각각 수행되면서 사용되는 DB커넥션은 각각 다르다고 이해해야함
  • 이 경우 트랜잭션을 각자 관리하기 때문에 전체 트랜잭션을 묶을 수 없음

2) double_commit_rollback() 메서드 추가

  • 트랜잭션1은 커밋, 트랜잭션2는 롤백하는 메서드
  • 트랜잭션을 2번 호출하면 각 커넥션이 트랜잭션을 각자 관리하기 때문에 트랜잭션1이 커밋하고 트랜잭션2가 롤백하면, 트랜잭션1에서 저장한 데이터는 커밋되고, 트랜잭션2에서 저장한 데이터는 롤백됨
@Test
void double_commit_rollback() {
    log.info("트랜잭션1 시작");
    TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("트랜잭션1 커밋");
    txManager.commit(tx1);

    log.info("트랜잭션2 시작");
    TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("트랜잭션2 롤백");
    txManager.rollback(tx2);
}

 

(2) 실행 결과

  • 로그를 보면 마찬가지로 커넥션 풀에서 conn0 커넥션을 재사용했으나 히카리프록시커넥션의 참조 주소를보면 트랜잭션1의 커넥션과, 트랜잭션2의 커넥션은 서로 완전히 다름
  • 로그에서도 트랜잭션1은 커넥션 획득하고 커밋 후 커넥션 풀에 반납하고, 트랜잭션2가 커넥션을 다시 획득하고 롤백 후 반납하는 것을 볼수 있음

 

  • 각 커넥션이 트랜잭션을 각각 관리하기 때문에 트랜잭션1은 커밋되고 트랜잭션2는 롤백됨


3. 스프링 트랜잭션 전파3 - 전파 기본

  • 트랜잭션을 각각 사용하는 것이 아니라 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행하는 경우에는 기존 트랜잭션과 별도의 트랜잭션을 진행해야하는지, 기존 트랜잭션을 그대로 이어 받아서 트랜잭션을 수행해야하는지 결정을 해야함
  • 이런 경우에 어떻게 동작할지 결정하는 것을 트랜잭션 전파(propagation)라 하며, 스프링은 다양한 전파 옵션을 제공함

** 참고

  • 지금 설명하는 내용은 트랜잭션 전파의 기본 옵션인 REQUIRED를 기준으로 설명되며 옵션 종류는 뒤에서 설명함

1) 외부 트랜잭션 수행 중 내부 트랜잭션이 추가로 수행

  • 외부 트랜잭션이 수행중이고 아직 끝나지 않았는데 내부 트랜잭션이 수행되면 스프링에서는 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션으로 만들어 줌
  • 내부 트랜잭션이 외부 트랜잭션에 참여하도록 하는 것이 스프링의 기본 동작이며 옵션을 통해 다른 동작 방식도 선택할 수 있음
  • 외부 트랜잭션 : 두 개의 트랜잭션 중 상대적으로 밖에 있기 때문에 외부 트랜잭션이라고 하며 처음 시작한 트랜잭션이라고 보면 됨
  • 내부 트랜잭션 : 외부의 트랜잭션 수행 중에 호출되기 때문에 마치 내부에 있는 것처럼 보이기 때문에 내부 트랜잭션이라 함

좌) 외부 트랜잭션 수행중 내부 트랜잭션이 수행 / 우) 외부, 내부 트랜잭션을 묶어서 하나의 트랜잭션으로 표현

2) 물리 트랜잭션, 논리 트랜잭션

  • 트랜잭션이 사용중일 때 또다른 트랜잭션이 내부에 사용되면 여러가지 복잡한 상황이 발생하는데, 이해를 돕기 위해서 스프링은 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눔
  • 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶이며, 물리 트랜잭션은 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션을 뜻함 -> 실제 커넥션을 통해서 트랜잭션 시작, 커밋, 롤백 하는 단위
  • 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위
  • 논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에만 나타나며, 단순히 트랜잭션이 하나인 경우에는 이렇게 둘로 구분하지는 않음(더 정확하게는 REQUIRED 전파 옵션을 사용하는 경우에 나타남)

3) 논리 트랜잭션, 물리 트랜잭션 기본 원칙

  • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됨
  • 하나의 논리 트랜잭션이라도 롤백 되면 물리 트랜잭션은 롤백 됨
  • 모든 트랜잭션 매니저를 커밋해야 물리 트랜잭션이 커밋되고, 하나의 트랜잭션 매니저라도 롤백하면 물리 트랜잭션은 롤백 되어버림

좌) 둘다 커밋 -> 물리 트랜잭션 커밋 / 중) 로직2 커밋 로직1 롤백 -> 물리트랜잭션 롤백 / 우) 로직1 커밋, 로직2 롤백 -> 물리 트랜잭션 롤백


4. 스프링 트랜잭션 전파4 - 전파 예제

1) Inner_commit() 추가

  • 외부 트랜잭션이 수행을 하고, 새로운 트랜잭션이 내부에서 실행되는 상황을 테스트
@Test
void inner_commit() {
    log.info("외부 트랜잭션 시작");
    TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // outer 트랜잭션이 처음 수행된 트랜잭션인지 확인

    log.info("내부 트랜잭션 시작");
    TransactionStatus inner = txManager.getTransaction(new DefaultTransactionDefinition());
    // inner 트랜잭션이 처음 수행된 트랜잭션인지 확인하면 false -> 스프링의 기본설정
    // 외부에 트랜잭션이 있는데 내부에서 트랜잭션이 새롭게 수행되어도 새로운 트랜잭션이 아니라 하나의 물리 트랜잭션으로 보는 것
    log.info("inner.isNewTransaction()={}", inner.isNewTransaction());

    log.info("내부 트랜잭션 커밋");
    txManager.commit(inner);

    log.info("외부 트랜잭션 커밋");
    txManager.commit(outer);
}

 

(1) 실행 결과

  • 외부 트랜잭션이 수행중에 내부 트랜잭션이 추가로 수행되는 경우, 외부 트랜잭션은 처음 수행된 트랜잭션이므로 신규 트랜잭션이 되어 외부 트랜잭션의 isNewTransaction의 결과는 true가 됨
  • 내부 트랜잭션이 시작되는 시점에서는 이미 외부 트랜잭션이 진행중인 상태이므로 내부 트랜잭션은 외부 트랜잭션에 참여하게 되고 내부 트랜잭션의 isNewTransaction의 결과는 false가 됨
  • 그리고 둘의 트랜잭션이 모두 커밋되었으므로 트랜잭션의 결과는 커밋됨

 

(2) 트랜잭션 참여

  • 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻이며 다른 관점으로 보면 외부에서 시작된 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
  • 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것

(3) 의문점

  • 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶인다고 설명했는데, 코드를 보면 커밋을 두번 호출함
  • 트랜잭션의 하나의 커넥션에 커밋이 호출되면 DB에 반영되어 커넥션이 종료되어야 할 것 같은데 어떻게 2번을 호출할 수 있을까?

2) 스프링의 물리 트랜잭션(외부트랜잭션, 내부 트랜잭션) 동작 분석

(1) 로그내역 분석

  • 외부 트랜잭션을 시작하고 커밋할 때 manual commit이라는 로그가 있는데 이것이 수동커밋 모드 즉, 트랜잭션을 시작한다는 뜻이며 곧 물리 트랜잭션을 시작한다는 뜻임
  • 하지만 내부 트랜잭션을 시작하고 커밋할 때는 이러한 트랜잭션을 시작하는 로그를 전혀 확인할 수 없고 Participating in existing transaction이라는 로그를 볼 수 있음
  • Participating(참여), 즉 내부 트랜잭션은 기존에 존재하는 외부 트랜잭션에 참여 한다는 뜻
  • 외부 트랜잭션만 물리 트랜잭션을 시작하고 커밋하며, 내부 트랜잭션은 외부 트랜잭션에 참여만 할 뿐 실제 트랜잭션의 동작은 전혀 하지 않는 것
  • 내부 트랜잭션이 실제 물리 트랜잭션을 커밋해버리면 트랜잭션이 끝나고 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어나갈 수 없기 때뭍에 내부 트랜잭션은 물리 트랜잭션을 커밋하면 안됨
  • 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리트랜잭션을 관리하도록 하여 트랜잭션 중복 커밋 문제를 해결함
외부 트랜잭션 시작
Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [HikariProxyConnection@1034246552 wrapping conn0 ...] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@1034246552 wrapping conn0: ... ] to manual commit
outer.isNewTransaction()=true

내부 트랜잭션 시작
Participating in existing transaction
inner.isNewTransaction()=false
내부 트랜잭션 커밋

외부 트랜잭션 커밋
Initiating transaction commit
Committing JDBC transaction on Connection [HikariProxyConnection@1034246552 wrapping conn0: ... ]
Releasing JDBC Connection [HikariProxyConnection@1034246552 wrapping conn0: ...] after transaction

3) 트랜잭션 전파의 동작 상세

트랜잭션 전파의 동장 프로세스

(1) 요청 흐름 - 외부 트랜잭션

  • 1. txManager.getTransaction()호출 -> 외부 트랜잭션 시작
  • 2. 트랜잭션 매니저가 데이터소스를 통해 커넥션을 생성
  • 3. 생성한 커넥션을 수동 커밋 모드(setAutoCommit(false))로 설정하여 물리 트랜잭션을 시작
  • 4. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션을 보관
  • 5. 트랜잭션 매니저가 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 반환할 때 신규 트랜잭션의 여부를 담아서 반환, isNewTransaction으로 신규 트랜잭션 여부를 확인할 수 있으며 트랜잭션을 처음 시작했으므로 true가 반환됨
  • 6.로직1의 동작을 수행하며 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용함

(2) 요청 흐름 - 내부 트랜잭션

  • 7. txManager.getTransaction()호출 -> 내부 트랜잭션을 시작
  • 8. 이때 트랜잭션 매니저가 트랜잭션 동기화 매니저를 통해서 기존 트랜잭션이 존재하는지 확인함
  • 9. 기존 트랜잭션이 동작하기때문에 내부 트랜잭션은 기존 트랜잭션에 참여함, 기존 트랜잭션에 참여한다는 뜻은 물리 커넥션에 대한 행위를 아무것도 하지 않는다는 뜻임(참여한다는 로그만 남기고 물리커넥션에 대한 동작을 아무것도 하지 않음)
    • 이미 기존 트랜잭션인 외부 트랜잭션에서 물리 트랜잭션을 시작했고 물리 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 담아두었음
    • 물리 트랜잭션이 진행중이므로 아무것도 하지 않아도 이후에 로직2는 기존에 시작된 물리트랜잭션을 자연스럽게 사용하게 되는 것(트랜잭션에 동기화 매니저에 보관된 기존 커넥션을 사용)
  • 10. 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 반환하고 마찬가지로 신규 트랜잭션 여부를 확인할 수 있으며 여기서는 기존 트랜잭션에 참여했으므로 신규 트랜잭션이 아니여서 isNewTransaction의 결과가 false로 나옴
  • 11. 로직2의 동작을 수행하고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 외부 트랜잭션이 보관한 커넥션을 획득해서 사용함

(3) 응답 흐름 - 내부 트랜잭션

  • 12. 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋
  • 13. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작하며, 내부 트랜잭션은 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않음
    • 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버리기 때문에 아직 트랜잭션이 끝나지 않았으므로 실제 커밋을 호출하지 않고 물리 트랜잭션이 외부 트랜잭션을 종료할 때까지 이어지게 설정해둔 것

(4) 응답 흐름 - 외부 트랜잭션

  • 14. 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋
  • 15. 외부 트랜잭션은 신규 트랜잭션이기 때문에 DB 커넥션에 실제 커밋을 호출함
  • 16. 트랜잭션 매니저에 커밋하는 것이 논리적인 커밋이라면 실제 커넥션에 커밋하는 것을 물리 커밋이라고 할 수 있음(실제 DB에 커밋이 반영되고 물리 커넥션이 종료됨)

4) 핵심 정리

  • 트랜잭션 매니저에 커밋을 호출한다고 해서 실제 커넥션에 물리 커밋이 발생하지 않음
  • 신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행하며 신규 트랜잭션이 아니면 실제 물리 커넥션을 사용하지 않음 즉, 트랜잭션이 내부에서 추가로 사용되면 트랜잭션 매니저에 커밋하는 것이 항상 물리 커밋으로 이어지지 않음
  • 논리 트랜잭션과 물리 트랜잭션(트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다고 보는 관점) 혹은 외부 트랜잭션과 내부 트랜잭션(외부 트랜잭션(처음 시작한 트랜잭션)이 실제 물리 커넥션을 관리한다는 관점)으로 나누어 설명할 수 있음

5. 스프링 트랜잭션 전파5 - 외부 롤백

1) outer_rollback() 추가

  • 내부 트랜잭션은 커밋되는데 외부 트랜잭션이 롤백되는 상황일 때
  • 논리 트랜잭션이 하나라도 롤백되면 전체 물리트랜잭션은 롤백 됨 -> 앞서 학습한 내용과 거의같고 진행과정도 비슷하기 때문에 이해가 쉬움
@Test
void outer_rollback() {
    log.info("외부 트랜잭션 시작");
    TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("outer.isNewTransaction()={}", outer.isNewTransaction());

    log.info("내부 트랜잭션 시작");
    TransactionStatus inner = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("inner.isNewTransaction()={}", inner.isNewTransaction());

    log.info("내부 트랜잭션 커밋");
    txManager.commit(inner);    // 내부 트랜잭션은 커밋

    log.info("외부 트랜잭션 롤백");
    txManager.rollback(outer);  // 외부 트랜잭션은 롤백
}

 

(1) 실행 결과

  • 외부 트랜잭션이 물리 트랜잭션을 시작하고 롤백하며, 내부 트랜잭션은 배운내용대로 물리 트랜잭션에 관여하지 않음
  • 결과적으로 외부 트랜잭션에서 시작한 물리 트랜잭션의 범위가 내부 트랜잭션까지 사용되며 외부 트랜잭션이 롤백되면서 전체 내용은 모두 롤백됨

 

(2) 응답 흐름

  • 요청흐름은 동일하므로 생략
  • 1. 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋
  • 2. 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않음
  • 3. 로직1이 문제가 발생했다고 가정하여 트랜잭션 매니저를 통해 외부 트랜잭션을 롤백을 요청
  • 4. 트랜잭션 매니저가 롤백 시점에 신규 트랜잭션인 것을 확인하고, 신규 커넥션이기 때문에 DB커넥션에 실제 롤백을 호출
  • 5. 트랜잭션 매니저에 롤백하는 것이 논리적인 롤백이라면, 실제 커넥션에 롤백하는 것을 물리 롤백이라 할 수 있으며 실제 데이터베이스에 롤백이 반영되고 물리 트랜잭션이 끝남


6. 스프링 트랜잭션 전파6 - 내부 롤백

1) inner_rollback() 추가

  • 내부 트랜잭션은 롤백, 외부 트랜잭션이 커밋되는 상황
  • 겉으로 보기에는 단순하지만 실제로는 단순하지 않은데 내부 트랜잭션이 롤백을 했지만 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않고 물리 트랜잭션에 영향을 주는 외부 트랜잭션이 커밋을 했고 외부 트랜잭션이 물리 트랜잭션을 관리하기 때문에 실제 트랜잭션은 커밋되어야 할 것 같다고 생각할 수도 있음
  • 하지만 트랜잭션의 대원칙, 전체 논리 트랜잭션이 커밋되어야 커밋되고, 논리 트랜잭션이 하나라도 롤백되면 전부 롤백되기 때문에 롤백이 됨
@Test
void inner_rollback() {
    log.info("외부 트랜잭션 시작");
    TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("outer.isNewTransaction()={}", outer.isNewTransaction());

    log.info("내부 트랜잭션 시작");
    TransactionStatus inner = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("inner.isNewTransaction()={}", inner.isNewTransaction());

    log.info("내부 트랜잭션 롤백");
    txManager.rollback(inner);   // 내부 트랜잭션은 롤백 -> rollback-only 마킹
    
    log.info("외부 트랜잭션 커밋");
    txManager.commit(outer);     // 외부 트랜잭션은 커밋
}

 

(1) 실행 결과

  • 예외가 터지면서 테스트가 실패하고 로그가 출력됨
  • 로그를 보면 외부 트랜잭션이 시작하여 물리 트랜잭션이 시작되고 내부 트랜잭션이 기존 트랜잭션에 참여한다는 로그를 남기며 기존 트랜잭션에 참여함
  • 내부 트랜잭션이 롤백을 하면 Participating transaction failed - marking existing transaction as rollback-only 이라는 로그를 남기는데, 참여 트랜잭션이 실패했고 rollback-only라는 마크를 남김
  • 실제 물리 트랜잭션은 롤백하지 않고 대신 기존 트랜잭션을 롤백 전용으로 표시함
  • 외부 트랜잭션이 커밋을 요청했지만, Global transaction is marked as rollback-only but transactional code requested commit 로그를 남기며 물리 트랜잭션이 롤백 되었는데 Global transaction(전체 트랜잭션)이 롤백으로 rollback-only로 표시되어 있기 때문에 물리 트랜잭션이 롤백 됨

 

(2) 응답 흐름

  • 1. 로직2에 문제가 생길 경우 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백함
  • 2. 내부 트랜잭션은 신규 트랜잭션이 아니므로 실제 롤백은 호출하지 않음
  • 3. 대신 트랜잭션 동기화 매니저에 rollbackOnly=true라는 표시를 해둠
  • 4. 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋함
  • 5. 외부 트랜잭션은 신규 트랜잭션이므로 DB 커넥션에 실제 커밋을 호출하는데 먼저 트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly=true) 표시가 있는지 확인을 하고, 롤백 전용 표시가 있다면 물리 트랜잭션을 롤백함
  • 6. 실제 데이터베이스에 롤백이 반영되고 물리트랜잭션이 끝남
  • 7. 트랜잭션 매니저에 커밋을 호출한 개발자입장에서는 커밋을 기대 했는데 내부 트랜잭션에서 롤백 전용 표시가 됨으로 인해서 롤백이 되었는데 이 경우는 조용히 넘어갈 수 있는 상황이 아니라 명확하게 알려주어야 하기 때문에 UnexpectedRollbackException 런타임 예외를 던짐
    • 시스템 입장에서는 커밋을 호출 했지만 롤백이 되엇다는 것은 분명하게 알려주어야 함
    • 예를 들어 고객은 주문이 성공했다고 생각했는데, 실제로는 롤백이 되어 주문이 생성되지 않았다면 명확하게 알려주어야 하는 것처럼 스프링은 이런경우 커밋을 시도했지만 기대하지 않는 롤백이 발생했다는 것을 명확하게 알려주기 위해 UnexpectedRollbackException 런타임 예외를 알리도록 똑똑하게 설계 되어있음

(3) 정리

  • 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백됨
  • 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시하게되고, 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 먼저 확인한 뒤 롤백전용 마크가 표시되어있으면 물리 트랜잭션을 롤백함
  • 이러한 상황(실제 트랜잭션이 커밋을 날렸지만 롤백된 상황)을 명확하게 알리기위해서 UnexpectedRollbackException 예외를 던짐

 

(4) 예외가 터지는 테스트를 성공하도록 수정

log.info("외부 트랜잭션 커밋");
//  txManager.commit(outer);     // 외부 트랜잭션은 커밋
assertThatThrownBy(() -> txManager.commit(outer))
        .isInstanceOf(UnexpectedRollbackException.class);

 

** 참고

  • 애플리케이션 개발에서 중요한 기본 원칙은 모호함을 제거하여 명확하게 개발해야 한다는 것
  • 이렇게 커밋을 호출했는데, 내부에서 롤백이 발생한 경우 모호하게 두면 아주 심각한 문제가 발생하게 되며 기대한 결과가 다른 경우 예외를 발생시켜서 명확하게 문제를 알려주는 것이 좋은 설계임
  • 내부 트랜잭션이 커밋되고 외부 트랜잭션이 롤백되었을 때 예외를 던지지 않는 이유는, 내부 트랜잭션은 실제 물리 트랜잭션에 아무 영향을 끼치지 않고 외부 트랜잭션이 롤백을 명령하면 명확하게 전체 트랜잭션이 롤백(외부, 내부 포함)되기 때문에 개발자가 헷갈리는 상황이 발생하지 않기 때문

7. 스프링 트랜잭션 전파7 - REQUIRES_NEW

1) REQUIRES_NEW 옵션

  • 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법 -> 커밋과 롤백이 각각 별도로 이루어지게 됨
  • 이 방법은 내부 트랜잭션에 문제가 발생해서 롤백을 해도 외부 트랜잭션에는 영향을 주지 않음
  • 반대로 외부 트랜잭션에 문제가 발생해도 내부 트랜잭션에 영향을 주지 않음
  • 이 방법을 사용하는 구체적인 예시는 이후에 알아볼 예정이며 지금은 작동 원리를 이해하는 것에 초점

  • 내부 트랜잭션을 시작할 때 REQUIRES_NEW옵션을 사용하면 물리 트랜잭션과 내부 트랜잭션을 분리하여 외부 트랜잭션과 내부 트랜잭션이 각각 별도의 물리 트랜잭션을 가지게 됨(DB 커넥션을 따로 사용한다는 뜻)
  • 내부 트랜잭션이 롤백되면서 로직2가 롤백이 되어도 로직1에서 저장한 데이터는 영향을 주지 않고 로직2는 롤백, 로직1은 커밋이 됨

2) inner_rollback_requires_new() 추가

  • 내부 트랜잭션을 시작할 때 전파 옵션인 propagationBehavior에 PROPAGATION_REQUIRES_NEW옵션을 설정
  • 이 전파 옵션을 사용하면 내부 트랜잭션을 시작할 때 기존 트랜잭션에 참여하는 것이 아니라 새로운 물리 트랜잭션을 만들어서 시작함
@Test
void inner_rollback_required_new() {
    log.info("외부 트랜잭션 시작");
    TransactionStatus outer = txManager.getTransaction(new DefaultTransactionDefinition());
    log.info("outer.isNewTransaction()={}", outer.isNewTransaction());

    log.info("내부 트랜잭션 시작");
    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();

    // 트랜잭션 옵션 설정 - PROPAGATION_REQUIRES_NEW: 내부 트랜잭션을 새로운 트랜잭션으로 설정
    // 디폴트 옵션은 PROPAGATION_REQUIRED - 내부 트랜잭션이 기존 트랜잭션에 참여
    definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    TransactionStatus inner = txManager.getTransaction(definition);
    log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // REQUIRES_NEW 옵션설정 - true 반환

    log.info("내부 트랜잭션 롤백");
    txManager.rollback(inner);  // 내부 트랜잭션 롤백

    log.info("외부 트랜잭션 커밋");
    txManager.commit(outer);    // 외부 트랜잭션 커밋
}

 

(1) 실행 결과

  • 기존 디폴트 설정과는 다르게 내부 트랜잭션에 실제 커넥션이 동작하는 로그들이 확인됨
  • 히카리 커넥션 풀에서 새로운 커넥션 conn1을 받아서 jdbc 드라이버를 동작하고, inner.inNewTransaction()=true의 로그에서 확인 되듯 새로운 트랜잭션으로 실행되었음
  • 내부 트랜잭션 시작 다음 로그를 보면 Suspending current transaction 이라는 로그를 볼 수 있는데, 기존 트랜잭션(외부 트랜잭션)을 뒤로 미뤄두고 새로운 커넥션을 확보해서 새로운 트랜잭션을 수행되고, 내부 트랜잭션이 롤백을 요청하여 실제 커넥션(conn1)이 롤백을 반영한뒤 트랜잭션이 종료되어 커넥션을 반납함
  • 내부 트랜잭션이 모두 종료되고 미뤄졌던 외부 트랜잭션이 커밋을 호출하면 실제 커넥션(conn0)이 커밋을 반영하고 트랜잭션이 종료되어 커넥션을 반납함

 

  • 외부 트랜잭션 시작
    • 외부 트랜잭션을 시작하면서 conn0 커넥션을 획득하고 manual commit으로 변경한 뒤 물리 트랜잭션을 시작
    • outer.isNewTransaction() = true, 외부 트랜잭션은 신규 트랜잭션
  • 내부 트랜잭션 시작
    • 내부 트랜잭션을 시작하면서 conn1 커넥션을 획득하고 manual commit으로 변경한 뒤 물리 트랜잭션을 시작
    • PROPAGATION_REQUIRES_NEW 옵션을 사용하여 외부 트랜잭션에 참여하는 것이 아닌 완전히 새로운 신규 트랜잭션으로 생성됨, inner.isNewTransaction()=true로 확인할 수 있음
  • 내부 트랜잭션 롤백
    • 내부 트랜잭션의 로직에서 롤백이 호출되면 신규 트랜잭션이기 때문에 실제 물리 트랜잭션을 롤백함
    • conn1 커넥션의 물리 트랜잭션이 롤백을 처리함
  • 외부 트랜잭션 커밋
    • 외부 트랜잭션은 정상적으로 커밋되면 신규 트랜잭션이기 때문에 실재 물리 트랜잭션을 커밋함
    • conn0 커넥션의 물리 트랜잭션이 커밋을 처리함

3) REQUIRES_NEW 동작 상세

REQIORES_NEW 옵션의 요청, 응답 흐름

(1) 요청 흐름 - 외부 트랜잭션

  • 1. txManager.getTransaction() 호출해서 외부 트랜잭션을 시작
  • 2. 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성함
  • 3. 생성한 커넥션을 수동 커밋 모드로 설정하여 물리 트랜잭션을 시작
  • 4. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션을 보관
  • 5. 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 반환하는데, 여기에 신규 트랜잭션의 여부가 담겨있어 isNewTransaction을 통해 신규 트랜잭션 여부를 확인할 수 있음, 트랜잭션을 처음 시작했으므로 해당 트랜잭션은 신규 트랜잭션임
  • 6. 로직1이 사용되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용함

(2) 요청 흐름 - 내부 트랜잭션

  • 7. REQUIRES_NEW 옵션과 함께 txManager.getTransaction()이 호출되면서 내부 트랜잭션을 시작하고 트랜잭션 매니저가 REQUIRED_NEW옵션을 확인하면 기존 트랜잭션에 참여하는 것이 아니라 새로운 트랜잭션을 시작함
  • 8. 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성함
  • 9. 생성한 커넥션을 수동 커밋 모드로 설정하여 물리 트랜잭션을 시작
  • 10. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션을 보관, 이때 con1 커넥션은 잠시 보류되고 지금부터는 새로 생성된 con2가 내부 트랜잭션을 완료할 때까지 사용됨
  • 11. 트랜잭션 매니저는 신규 트랜잭션의 생성한 결과를 반환, 옵션 설정으로 해당 트랜잭션도 신규 트랜잭션임
  • 12. 로직2가 사용되고 커넥션이 필요한 경우 트랜잭션 동기화 매니저에 있는  con2 커넥션을 획득해서 사용함

(3) 응답 흐름 - 내부 트랜잭션

  • 1. 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 롤백(로직2에 문제가 생겨 롤백한다는 가정)
  • 2. 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션 여부를 체크, 신규 트랜잭션이므로 실제 롤백을 호출
  • 3. 내부 트랜잭션이 con2 물리 트랜잭션을 롤백, 트랜잭션이 종료되고 con2는 종료되거나 커넥션 풀에 반납된 이후에 con1의 보류가 끝나고 다시 con1을 사용함

(4) 응답 흐름 - 외부 트랜잭션

  • 4. 외부 트랜잭션에 커밋을 요청
  • 5. 외부 트랜잭션은 신규 트랜잭션이기 때문에 물리 트랜잭션을 커밋
  • 6. 이때 rollbackOnly 설정 여부를 체크하고 rollbackOnly 설정이 없으므로 커밋을 수행
  • 7. 본인이 만든 con1 커넥션을 통해 물리 트랜잭션이 커밋되고 con1은 종료되거나 커넥션 풀에 반납됨

4) 정리

  • REQUIRES_NEW 옵션을 사용하면 물리 트랜잭션이 명확하게 분리됨
  • 하지만 데이터 커넥션이 동시에 2개를 사용한다는 점을 주의해야하는데, 만약 내부 트랜잭션의 로직이 오래 걸려서 빨리 반환이 되지 않아 고객 요청은 500개가 왔는데 커넥션은 1000개가 호출되면서 데이터베이스에서 정해놓은 커넥션 수가 모두 고갈 되어 장애가 발수 있음
  • 자주 발생되는 현상은 아니지만 트래픽이 많거나 성능이 중요한 곳에서는 조심히 사용할 필요성은 있음

8. 스프링 트랜잭션 전파8 - 다양한 전파 옵션

  • 전파 옵션에 별도의 설정하지 않으면 스프링은 REQUIERD가 기본으로 사용하며 실무에서 대부분 기본 옵션을 사용함
  • 아주 가끔 REQUIRES_NEW을 사용하고 나머지는 거의 사용하지 않아 참고용으로 알아두고 필요할 때 찾아보면 됨

1) REQUIRED

  • 가장 많이 사용하는 기본 설정
  • 기존 트랜잭션이 없으면 생성하고, 있으면 기존 트랜잭션에 참여함
  • 트랜잭션이 필수라는 의미로 이해하면 됨(필수이기 때문에 없으면 만들고, 있으면 참여함)

2) REQUIRES_NEW

  • 항상 새로운 트랜잭션을 생성함
  • 기존 트랜잭션이 없어도 생성하고, 기존 트랜잭션이 있어도 생성함

3) SUPPORT

  • 트랜잭션을 지원한다는 뜻
  • 기존 트랜잭션이 없으면 없는대로 진행하고 있으면 기존 트랜잭션에 참여함

4) NOT_SUPPORT

  • 트랜잭션을 지원하지 않는다는 뜻
  • 기존 트랜잭션이 없으면 트랜잭션 없이 진행하고 트랜잭션이 있어도 트랜잭션 없이 진행함(기존 트랜잭션은 보류가 됨)

5) MANDATORY

  • 강한 의무사항으로 트랜잭션이 반드이 있어야하고 트랜잭션이 없으면 예외가 발생함
  • 기존 트랜잭션이 없으면 IllegalTransactionStateException 예외가 발생하고 기존 트랜잭션이 있으면 참여함

6) NEVER

  • 트랜잭션을 사용하지 않는다는 강한의미로 기존 트랜잭션이 있으면 예외가 발생함, 기존 트랜잭션도 허용하지 않겠다는 강한 부정의 의미
  • 기존 트랜잭션이 없으면 트랜잭션 없이 진행하고 기존 트랜잭션이 있으면 IllegalTransactionStateException 예외가 발생함

7) NESTED

  • 기존 트랜잭션이 없으면 새로운 트랜잭션을 성하고 기존 트랜잭션이 있으면 중첩 트랜잭션을 만듦
  • 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만 중첩 트랜잭션이 외부 트랜잭션에 영향을 주지 않음
  • 예를 들어 중첩 트랜잭션이 롤백되어도 외부 트랜잭션은 커밋할 수 있는데, 외부 트랜잭션이 롤백 되면 중첩 트랜잭션도 함께 롤백됨
  • JDBC savepoint 기능을 사용하며 DB 드라이버에서 해당 기능을 지원하는지 확인이 필요하고 JPA에서는 사용할 수 없음

8) 트랜잭션 전파와 옵션

  • isolation, timeout, readOnly 옵션은 트랜잭션이 처음 시작될 때만 적용되므로 트랜잭션에 참여하는 경우에는 적용되지 않음
  • REQUIRED를 통한 처음 트랜잭션이 시작될때, REQUIRED_NEW를 통해 모든 트랜잭션이 시작 되는 시점에 적용됨