Spring

[인프런/스프링DB 2편] 6. 스프링 트랜잭션 전파 (1)

주니어주니 2023. 7. 25. 22:54

 

1. 트랜잭션 기본

 

 

* 트랜잭션 관련 로그 

logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
#JPA log
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.resource.transaction=DEBUG
#JPA SQL
logging.level.org.hibernate.SQL=DEBUG

 

 

1) 기본 커밋, 롤백

package hello.springtx.propagation;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;

import javax.sql.DataSource;

@Slf4j
@SpringBootTest
public class BasicTxTest {

    @Autowired
    PlatformTransactionManager txManager;

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

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

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

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

        log.info("트랜잭션 롤백 시작");
        txManager.rollback(status);
        log.info("트랜잭션 커밋 완료");
    }
  • 원래 기본으로 트랜잭션 매니저 등록해주지만, 내가 작성하면 작성한 트랜잭션 매니저 적용
  • DataSourceTransactionManager를 스프링 빈으로 등록

 

커밋
롤백

 

 

2) 트랜잭션 각각 두번 사용

@Test
void double_commit() {
    log.info("트랜잭션1 시작");
    TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());
    log.info("트랜잭션1 커밋");
    txManager.commit(tx1);
    log.info("트랜잭션 커밋 완료");

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

 

  • 트랜잭션 1
    • 트랜잭션 1 시작 -> 히카리 커넥션 풀에서 conn0 커넥션 획득
    • 트랜잭션 1 커밋 -> 히카리 커넥션 풀에 conn0 커넥션 반납

  • 트랜잭션 2 
    • 트랜잭션 2 시작 -> 히카리 커넥션 풀에서 conn0 커넥션 획득
    • 트랜잭션 2 커밋 -> 히카리 커넥션 풀에 conn0 커넥션 반납

  • 트랜잭션 각각 수행 -> 각각 다른 커넥션 사용 

 

 

2. 스프링 트랜잭션 전파 (Propagation)

 

2-1. 전파 예제

 

 

  • 외부 트랜잭션 (처음 트랜잭션)이 수행중인데, 내부 트랜잭션이 수행되는 경우
    • 외부와 내부를 묶어서 하나의 트랜잭션으로 만듦 
    • 내부 트랜잭션이 외부 트랜잭션에 "참여"

 

  • 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶임
  • 물리 트랜잭션 : 실제 데이터베이스에 적용되는 트랜잭션 (커넥션을 통해 커밋, 롤백하는 단위)
  • 논리 트랜잭션 : 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위
    (트랜잭션이 진행되는 중에 추가로 트랜잭션을 사용하는 경우에만 쓰이는 개념)

 

📌 원칙 

* 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋
* 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백

 

 

1) 모두 커밋되는 경우

 

 

@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);
}

 

 

  • 외부 트랜잭션이 수행중인데, 내부 트랜잭션 추가 -> 내부 트랜잭션이 외부 트랜잭션에 참여
    • 내부 트랜잭션이 외부 트랜잭션을 그대로 이어받아서 따른다는 뜻 ! 
    • -> 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶임

  • isNewTransaction() 
    • 외부 트랜잭션처음 수행된 트랜잭션 -> isNewTransaction = true
    • 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여 -> isNewTransaction = false

  • 커밋을 2번 호출
    • 외부 트랜잭션물리 트랜잭션을 시작, 커밋 가능
    • 내부 트랜잭션이 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버림 (커밋/롤백 -> 트랜잭션 종료)
    • 내부 트랜잭션은 사실상 아무것도 하지 않음

 

 

* 흐름 순서

요청흐름
응답흐름

 

 

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

  7. txManager.getTransaction() : 내부 트랜잭션 시작
  8. 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해 기존 트랜잭션이 존재하는지 확인
  9. 기존 트랜잭션 존재 -> 기존 트랜잭션에 참여 -> 물리 트랜잭션을 건들지 않음
  10. 트랜잭션 매니저는 트랜잭션 생성 결과를 TransactionStatus에 담아서 반환하는데, 여기에서 신규 트랜잭션 여부 확인 (isNewTransaction = false)
  11. 로직2 사용 ( + 커넥션 필요한 경우, 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션 획득해서 사용 )

  12. 로직2 끝난 뒤 트랜잭션 매니저를 통해 내부 트랜잭션 커밋
  13. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
    신규 트랜잭션 X -> 실제 커밋 호출 X (논리 커밋)

  14. 로직1 끝난 뒤 트랜잭션 매니저를 통해 외부 트랜잭션 커밋
  15. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
    신규 트랜잭션 O -> 실제 커밋 호출 O (물리 커밋)

 

📌 정리 
* 트랜잭션 매니저에 커밋 호출 -> 항상 실제 커넥션에 물리 커밋이 발생하는 것 X 
* 신규 트랜잭션인 경우에만 실제 커넥션 사용 -> 물리 커밋, 롤백 수행

 

 

 

2) 외부 롤백 -> 전체 롤백되는 경우

 

 

@Test
void outer_rollback() {
    log.info("외부 트랜잭션 시작");
    TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());

    log.info("내부 트랜잭션 시작");
    TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());

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

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

 

  • 내부 트랜잭션새로운 트랜잭션 X -> 실제 물리 트랜잭션에 커밋 X
  • 외부 트랜잭션 롤백 -> 물리 롤백

 

 

3) 내부 롤백 -> 전체 롤백

 

  • 내부 트랜잭션새로운 트랜잭션 X -> 물리 트랜잭션에 영향 X -> 근데 왜 롤백되냐?

 

@Test
void inner_rollback() {
    log.info("외부 트랜잭션 시작");
    TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());

    log.info("내부 트랜잭션 시작");
    TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());

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

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

 

 

 

* 응답 흐름

  1. 로직2에 문제 발생 -> 내부 트랜잭션 롤백
  2. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
    신규 트랜잭션 X -> 실제 롤백 호출 X 
  3. 대신, 트랜잭션 동기화 매니저에 롤백 전용 마크 (rollbackOnly = true) 표시 !

  4. 로직1 수행 -> 외부 트랜잭션 커밋
  5. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
    신규 트랜잭션 O -> rollbackOnly 표시 확인 -> rollbackOnly = true 표시 有 -> 롤백 요청
  6. 물리 롤백
  7. 커밋을 호출했지만 롤백됨 -> UnexpectedRollbackException 런타임 예외를 던져서 명확하게 알려줌

 

📌 정리 

* 논리 트랜잭션이 하나라도 롤백 -> 물리 트랜잭션 롤백
* 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크 표시 
* 외부 트랜잭션을 커밋할 때 롤백 전용 마크 확인 -> 있으면 물리 트랜잭션 롤백 -> UnexpectedRollbackException 예외 던짐

 

 

 

4) 외부 트랜잭션과 내부 트랜잭션 분리 (REQUIRES_NEW)

 

  • 외부 트랜잭션과 내부 트랜잭션이 별도의 물리 트랜잭션 -> 별도의 커넥션 사용 

 

@Test
void inner_rollback_requires_new() {
    log.info("외부 트랜잭션 시작");
    TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
    log.info("outer.isNewTransaction()={}", outer.isNewTransaction());

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

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

    log.info("외부 트랜잭션 커밋");
    txManager.commit(outer);
}
  • setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW)
    • 새로운 물리 트랜잭션 시작

 

 

 

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

  7. txManager.getTransaction() + REQUIRES_NEW 옵션 : 새로운 내부 트랜잭션 시작
  8. 트랜잭션 매니저는 데이터소스를 통해 커넥션 생성
  9. 생성한 커넥션을 수동 커밋 모드(setAutoCommit(false))로 설정 -> 물리 트랜잭션 시작
  10. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션 보관
    • 이때 con1은 잠시 보류, con2 사용
  11. 트랜잭션 매니저는 신규 트랜잭션 생성 결과 반환 (isNewTransaction = true)
  12. 로직 2 사용 ( + 커넥션 필요한 경우, 트랜잭션 동기화 매니저에 있는 con2 커넥션 획득해서 사용 )

 

 

  1. 로직 2 문제 발생 -> 롤백 요청
  2. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
    신규 트랜잭션 O -> 실제 롤백 호출 O 
  3. con2 물리 트랜잭션 롤백 (트랜잭션 종료 -> con2 종료되거나 커넥션 풀에 반납)

  4. 로직 1 수행 -> 커밋 요청 
  5. 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작
    신규 트랜잭션 O -> 실제 커밋 호출 O 
  6. 이때 rollbackOnly 설정 확인 -> 없음 -> 커밋 
  7. con1 물리 트랜잭션 커밋 (트랜잭션 종료 -> con1 종료되거나 커넥션 풀에 반납)

 

 


 

2-2. 전파 옵션

 

(1) REQUIRED (기본) - 트랜잭션 필수

  • 기존 트랜잭션 無 : 새로운 트랜잭션 생성
  • 기존 트랜잭션 有 : 기존 트랜잭션에 참여

 

(2) REQUIRES_NEW - 항상 새로운 트랜잭션 생성

  • 기존 트랜잭션 無 : 새로운 트랜잭션 생성
  • 기존 트랜잭션 有 : 새로운 트랜잭션 생성

 

(3) SUPPORT - 트랜잭션 지원 

  • 기존 트랜잭션 無 : 트랜잭션 없이 진행
  • 기존 트랜잭션 有 : 기존 트랜잭션에 참여

 

(4) NOT_SUPPORT - 트랜잭션 지원 X

  • 기존 트랜잭션 無 : 트랜잭션 없이 진행
  • 기존 트랜잭션 有 : 트랜잭션 없이 진행 (기존 트랜잭션은 보류)

 

(5) MANDATORY - 트랜잭션 의무

  • 기존 트랜잭션 無 : IllegalTransactionStateException 예외 발생
  • 기존 트랜잭션 有 : 기존 트랜잭션에 참여

 

(6) NEVER - 트랜잭션 사용 X

  • 기존 트랜잭션 無 : 트랜잭션 없이 진행
  • 기존 트랜잭션 有 : IllegalTransactionStateException 예외 발생

 

(7) NESTED- 트랜잭션 중첩 (JPA에서 사용 X)

  • 기존 트랜잭션 無 : 새로운 트랜잭션 생성
  • 기존 트랜잭션 有 : 중첩 트랜잭션 생성

 

isolation, timeout, readOnly : 트랜잭션이 처음 시작될 때만 적용 (참여하는 경우 X)