Spring/Spring

[Spring] 스프링 트랜잭션 전파와 롤백

2022. 9. 7. 00:37
목차
  1. 트랜잭션 관련 포스팅
  2. 트랜잭션 전파란?
  3. 복수의 트랜잭션 예제
  4. 물리 트랜잭션과 논리 트랜잭션
  5. 트랜잭션 전파 예제
  6. 트랜잭션 동작 흐름
  7. 트랜잭션 롤백
  8. 외부 트랜잭션과 내부 트랜잭션을 분리해서 사용하기 (REQUIRES_NEW)
  9. 참고

트랜잭션 관련 포스팅

  • [Spring] 트랜잭션 추상화 정리
  • [Spring] 트랜잭션 동기화 정리
  • [Spring] 트랜잭션 AOP 동작 흐름
  • [Spring] 트랜잭션 AOP 사용 시 주의점 (ft. Spring AOP self-invocation)
  • [Spring] 스프링 트랜잭션 전파와 롤백
  • [Spring] 트랜잭션 전파 옵션

 

트랜잭션 전파란?

  • 트랜잭션을 각각 사용하는 것이 아니라 트랜잭션이 이미 진행중인데, 여기에 추가로 트랜잭션을 수행하면 어떻게 되는가?
    • 기존 트랜잭션과 별도의 트랜잭션을 따로 진행해야 하는가? 혹은 기존 트랜잭션을 그대로 이어 받아서 트랜잭션을 수행해야 하는가?
  • 이렇게 복수의 트랜잭션을 사용할 경우 동작 방식을 결정하는 것을 트랜잭션 전파(propagation)라고 한다
    • 참고로 스프링은 다양한 트랜잭션 전파 옵션을 제공하며, 본문은 트랜잭션 전파의 기본 옵션인 REQUIRED를 기준으로 설명하고자 한다

 

 

복수의 트랜잭션 예제

외부 트랜잭션이 수행중인데, 내부 트랜잭션이 추가로 수행되는 경우

  • 외부 트랜잭션이 수행중이고 끝나지 않은 상태에서 내부 트랜잭션이 수행된다
  • 외부 트랜잭션
    • 두 트랜잭션 중 상대적으로 밖에 있으므로 외부 트랜잭션이라 명칭한다
    • 외부 트랜잭션 = 처음 시작한 트랜잭션
  • 내부 트랜잭션
    • 외부에 트랜잭션이 수행되고 있는 도중에 호출되기 때문에 마치 내부에 있는 것처럼 보여서 내부 트랜잭션이라 명칭한다
    • 내부 트랜잭션 = 나중에 시작한 트랜잭션
  • 스프링은 이 경우 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다
    • 즉, 내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다
    • 이것이 기본 동작이며 옵션을 통해 다른 동작방식도 선택할 수 있다

 

 

물리 트랜잭션과 논리 트랜잭션

  • 스프링은 이해를 돕기 위해 논리 트랜잭션과 물리 트랜잭션이라는 개념을 나눈다
    • 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이며, 논리 트랜잭션들은 하나의 물리 트랜잭션으로 묶인다
    • 물리 트랜잭션은 우리가 이해하는 실제 데이터베이스에 적용되는 트랜잭션을 뜻하며, 실제 커넥션을 통해서 트랜잭션을 시작하고 실제 커넥션을 통해서 커밋/롤백하는 단위이다
  • 논리 트랜잭션 개념은 트랜잭션이 진행되는 중에 내부에 추가로 트랜잭션을 사용하는 경우에 나타난다(REQUIRED 옵션 기준)
    • 단순히 트랜잭션이 하나인 경우 둘을 구분하지는 않음
  • 트랜잭션이 사용중일 때 또 다른 트랜잭션이 내부에 사용되면 여러가지 복잡한 상황이 발생하게 되는데, 이때 논리 트랜잭션 개념을 도입하면 다음과 같은 단순한 원칙을 만들 수 있다
    • (중요!!) 모든 논리 트랜잭션이 커밋돼야 물리 트랜잭션이 커밋된다
    • (중요!!) 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다

 

 

트랜잭션 전파 예제

외부 트랜잭션이 수행중인데, 내부 트랜잭션을 추가로 수행한 경우

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

    log.info("내부 트랜잭션 시작");
    TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
    log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // inner.isNewTransaction()=false
    log.info("내부 트랜잭션 커밋");
    txManager.commit(inner);

    log.info("외부 트랜잭션 커밋");
    txManager.commit(outer);
}
  • 외부 트랜잭션은 처음 수행된 트랜잭션이므로 신규 트랜잭션이 된다
  • 내부 트랜잭션을 시작하는 시점에는 이미 외부 트랜잭션이 진행중인 상태이다. 이 경우 내부 트랜잭션은 외부 트랜잭션에 참여한다
  • 트랜잭션 참여
    • 내부 트랜잭션이 외부 트랜잭션에 참여한다는 뜻은 내부 트랜잭션이 외부 트랜잭션을 그대로 이어 받아서 따른다는 뜻
      • = 외부 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
      • = 외부에서 시작된 물리적인 트랜잭션의 범위가 내부 트랜잭션까지 넓어진다는 뜻
    • 정리하면 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶이는 것
  • 내부 트랜잭션은 이미 진행중인 외부 트랜잭션에 참여했으므로 신규 트랜잭션이 아니다(isNewTransaction = false)

 

inner_commit() 실행결과

  • 외부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통한 물리 트랜잭션을 시작(mannual commit)하고, DB 커넥션을 통해 커밋하는 것을 확인할 수 있다
  • 반면 내부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통해 커밋하는 로그를 확인할 수 없다

 

정리

  • 외부 트랜잭션만 물리 트랜잭션을 시작/커밋한다
    • 만약 내부 트랜잭션이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버리므로 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없다
    • 따라서 내부 트랜잭션은 DB 커넥션을 통한 물리 트랜잭션을 커밋하면 안된다
  • 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다
    • 이를 통해 트랜잭션 중복 커밋 문제를 해결한다

 

 

트랜잭션 동작 흐름

요청 흐름

요청 흐름

  • 기본적인 동작 흐름은 [Spring] 트랜잭션 AOP 동작 흐름을 참고하자
  • 외부 트랜잭션
    • 실제 물리 트랜잭션을 시작한다
    • 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus(여기에 신규 트랜잭션의 여부가 담겨있음)에 담아서 반환하는데, 여기서는 트랜잭션을 처음 시작했으므로 신규 트랜잭션이다
  • 내부 트랜잭션
    • 기존 트랜잭션이 존재하므로 기존 트랜잭션에 참여한다(= 아무것도 하지 않는다)
    • 트랜잭션 매니저는 트랜잭션을 생성한 결과를 TransactionStatus에 담아서 반환하는데, 여기서는 기존 트랜잭션에 참여했으므로 신규 트랜잭션이 아니다

 

응답 흐름

응답흐름

  • 내부 트랜잭션
    • 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋한다
    • 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작하는데, 이 경우 신규 트랜잭션이 아니므로 실제 커밋을 호출 X
  • 외부 트랜잭션
    • 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋한다
    • 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동작하는데, 이 경우 신규 트랜잭션이므로 DB 커넥션에 실제 커밋 호출
      • 트랜잭션 매니저에 커밋하는 것이 논리적인 커밋이라면 실제 커넥션에 커밋하는 것을 물리 커밋이라 할 수 있다
      • 실제 데이터베이스에 커밋이 반영되고, 물리 트랜잭션도 끝난다

 

정리

  • 트랜잭션 매니저에 커밋을 호출한다고 해서 항상 실제 커넥션에 물리 커밋이 발생하지는 않는다
  • 신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행하며, 신규 트랜잭션이 아니면 실제 물리 커넥션을 사용하지 않는다
  • 트랜잭션이 내부에서 추가로 사용되면 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 트랜잭션이 커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다

 

 

트랜잭션 롤백

1) 외부 롤백

내부 트랜잭션은 커밋되는데 외부 트랜잭션이 롤백되는 경우

@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);
}
  • 외부 롤백은 간단한 상황이므로 이해하기 쉽다
  • 이 경우 내부 트랜잭션을 커밋했더라도 내부 트랜잭션 안에서 저장한 데이터도 모두 함께 롤백된다
    • 논리 트랜잭션이 하나라도 롤백되면 전체 물리 트랜잭션이 롤백되기 때문이다

 

outer_rollback 실행결과

  • 커밋이 롤백으로 바뀌었을 뿐 앞서 나온 내용과 거의 유사하다

 

응답흐름

 

2) 내부 롤백

내부 트랜잭션은 롤백되는데 외부 트랜잭션이 커밋되는 상황

@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); // 외부 트랜잭션 커밋 시에 UnexpectedRollbackException이 발생한다
}
  • 내부 롤백은 외부 롤백보다 좀 더 복잡하다
    • 내부 트랜잭션을 롤백했지만, 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않기 때문이다
  • 이 경우도 외부 롤백과 마찬가지로 전체 트랜잭션이 롤백된다 
    • "하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다"라는 원칙을 기억하자
  • 외부 트랜잭션을 커밋하는 순간 UnexpectedRollbackException 예외가 발생한다

 

inner_rollback() 실행결과

  • 1) 외부 트랜잭션 시작
    • 물리 트랜잭션을 시작한다
  • 2) 내부 트랜잭션 시작
    • Participating in exsiting transaction
    • 기존 트랜잭션에 참여한다
  • 3) 내부 트랜잭션 롤백
    • Participating transaction failed - marking existing transaction as rollback-only
    • 내부 트랜잭션을 롤백하더라도 실제 물리트랜잭션은 롤백 X
    • 대신 기존 트랜잭션을 롤백 전용으로 표시한다
  • 4) 외부 트랜잭션 커밋
    • 외부 트랜잭션을 커밋한다
    • Global transaction is marked as rollback-only
    • 커밋을 호출했지만 전체 트랜잭션이 롤백 전용으로 표시되어있으므로 물리 트랜잭션을 롤백한다

 

응답흐름

  • 내부 트랜잭션은 물리 트랜잭션을 롤백하지 않는 대신에 트랜잭션 동기화 매니저에 롤백 전용(rollbackOnly=true) 표시를 해둔다
  • 외부 트랜잭션을 커밋하는 시점에 트랜잭션 매니저는 트랜잭션 동기화 매니저에 롤백 전용 표시가 있는지 확인하고, 롤백 전용 표시가 있다면 물리 트랜잭션을 롤백한다
  • 마지막으로 클라이언트에 UnexpectedRollbackException 예외를 던진다
    • 트랜잭션 매니저에 커밋을 호출한 개발 입장에서는 분명 커밋을 기대했는데 롤백 전용 표시로 인해 실제로는 롤백이 되어버렸기 때문이다
    • 스프링은 UnexpectedRollbackException 예외를 던짐으로써 "커밋을 시도했지만 기대하지 않은 롤백이 발생했다"라는 사실을 명확하게 알려준다

 

3) 정리

  • 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백된다
  • 내부 논리 트랜잭션이 롤백되면 롤백 전용 마크를 표시한다
  • 외부 트랜잭션을 커밋할 때 롤백 전용 마크를 확인한다
    • 롤백 전용 마크가 표시되어 있으면 물리 트랜잭션을 롤백하고 UnexpectedRollbackException 예외를 던진다

 

 

외부 트랜잭션과 내부 트랜잭션을 분리해서 사용하기 (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); // 커밋
}
  • REQUIRES_NEW 옵션을 주면 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용할 수 있다
    • 즉, 커밋과 롤백도 각각 별도로 이루어진다
  • 내부 트랜잭션을 시작할 때 전파 옵션인 propagationBehavior에 PROPAGATION_REQUIRES_NEW 옵션을 주면 내부 트랜잭션을 시작할 때 기존 트랜잭션에 참여하는 것이 아니라 새로운 물리 트랜잭션을 만들어서 시작하게 된다

 

inner_rollback_requires_new() 실행결과

  • 1) 외부 트랜잭션 시작
    • 외부 트랜잭션을 시작하면서 conn0을 획득하고 manual commit으로 변경해서 물리 트랜잭션을 시작한다
    • 외부 트랜잭션은 신규 트랜잭션이다
  • 2) 내부 트랜잭션 시작
    • 내부 트랜잭션을 시작하면서 conn1을 획득하고 manual commit으로 변경해서 물리 트랜잭션을 시작한다
    • 내부 트랜잭션은 외부 트랜잭션에 참여하지 않고 새로운 신규 트랜잭션으로 생성된다
  • 3) 내부 트랜잭션 롤백
    • 내부 트랜잭션을 롤백한다
    • 내부 트랜잭션은 신규 트랜잭션이므로 실제 물리 트랜잭션을 롤백한다
    • 내부 트랜잭션은 conn1을 사용하므로 conn1에 물리 롤백을 수행한다
  • 4) 외부 트랜잭션 커밋
    • 외부 트랜잭션을 커밋한다
    • 외부 트랜잭션은 신규 트랜잭션이므로 실제 물리 트랜잭션을 커밋한다
    • 외부 트랜잭션은 conn0을 사용하므로 conn0에 물리 커밋을 수행한다

 

요청 흐름

요청 흐름

  • 내부 트랜잭션
    • 트랜잭션 매니저는 REQUIRES_NEW 옵션을 확인하여 기존 트랜잭션에 참여하는 것이 아니라 새로운 트랜잭션을 시작한다
    • 트랜잭션 매니저가 트랜잭션 동기화 매니저에 커넥션을 보관할 때 con1은 잠시 보류되고 con2가 사용된다
      • 내부 트랜잭션이 완료될 때까지는 con2가 사용된다

 

응답 흐름

응답 흐름

  • 내부 트랜잭션
    • 트랜잭션 매니저는 롤백 시점에 신규 트랜잭션에 여부에 따라 다르게 동작하는데, 현재 내부 트랜잭션은 신규 트랜잭션이므로 실제 롤백을 호출한다
    • 내부 트랜잭션이 con2의 물리 트랜잭션을 롤백한다
      • 이후에 con1의 보류가 끝나고 다시 con1을 사용한다
  • 외부 트랜잭션
    • 외부 트랜잭션에 커밋을 요청하는데, 외부 트랜잭션은 신규 트랜잭션이므로 물리 트랜잭션을 커밋한다
      • 이때 rollbackOnly 설정을 체크하는데, rollbackOnly 설정이 없으므로 커밋한다
    • con1을 통해 물리 트랜잭션을 커밋한다

 

정리

  • REQUIRES_NEW 옵션을 사용하면 물리 트랜잭션이 명확하게 분리된다
  • REQUIRES_NEW 옵션을 사용하면 데이터베이스 커넥션이 동시에 2개 사용된다는 점을 주의해야 한다

 

 

참고

스프링 DB 2편

저작자표시 비영리 (새창열림)

'Spring > Spring' 카테고리의 다른 글

[Spring] 빈 중복 시 해결방법(@Autowired, @Qualifier, @Primary)  (0) 2022.12.19
[Spring] 트랜잭션 전파 옵션  (0) 2022.09.07
[Spring] 트랜잭션 AOP 사용 시 주의점 (ft. Spring AOP self-invocation)  (0) 2022.09.01
[Spring] 테스트 코드를 작성할 때 @Transactional을 주의하라  (4) 2022.08.17
[Spring] @Transactional과 메모리 DB를 활용한 데이터 접근 계층 테스트  (0) 2022.08.13
  1. 트랜잭션 관련 포스팅
  2. 트랜잭션 전파란?
  3. 복수의 트랜잭션 예제
  4. 물리 트랜잭션과 논리 트랜잭션
  5. 트랜잭션 전파 예제
  6. 트랜잭션 동작 흐름
  7. 트랜잭션 롤백
  8. 외부 트랜잭션과 내부 트랜잭션을 분리해서 사용하기 (REQUIRES_NEW)
  9. 참고
'Spring/Spring' 카테고리의 다른 글
  • [Spring] 빈 중복 시 해결방법(@Autowired, @Qualifier, @Primary)
  • [Spring] 트랜잭션 전파 옵션
  • [Spring] 트랜잭션 AOP 사용 시 주의점 (ft. Spring AOP self-invocation)
  • [Spring] 테스트 코드를 작성할 때 @Transactional을 주의하라
코택
코택
TaxFree코택 님의 블로그입니다.
코택
TaxFree
코택
전체
오늘
어제
  • 분류 전체보기 (369)
    • Spring (29)
      • Spring (18)
      • 스프링 핵심 원리 - 고급편 (11)
    • Spring Batch (4)
    • JPA (4)
    • CS (89)
      • 자료구조 (2)
      • 네트워크 (5)
      • 운영체제 (1)
      • 데이터베이스 (4)
      • SQL (7)
      • 알고리즘 이론 (4)
      • 알고리즘 문제 풀이 (66)
    • 웹 (28)
      • React.js (4)
      • Next.js (1)
      • Node.js (14)
      • FastAPI (4)
      • Django (5)
    • 프로그래밍 언어 (45)
      • Python (5)
      • Java + Kotlin (29)
      • JavaScript + TypeScript (11)
    • 테스트코드 (26)
      • ATDD, 클린 코드 with Spring (4)
      • 이규원의 현실 세상의 TDD: 안정감을 주는 코드.. (20)
    • 인프라 (6)
      • AWS (2)
      • Kubernetes (4)
    • 트러블슈팅 (25)
    • 책 (89)
      • Effective Java (54)
      • Effective Kotlin (14)
      • 도메인 주도 개발 시작하기: DDD 핵심 개념 정.. (11)
      • 웹 프로그래머를 위한 데이터베이스를 지탱하는 기술 (6)
      • 도메인 주도 설계 첫걸음 (4)
    • Git (10)
    • 회고 (5)
    • etc (8)

블로그 메뉴

  • 홈
  • 방명록
  • 관리
  • GitHub
  • LinkedIn

공지사항

  • 스킨 관련

인기 글

태그

  • 백준
  • 깊이 우선 탐색
  • BOJ
  • 장고
  • 그래프 탐색
  • fastapi
  • Git
  • mysql
  • Shortest Path
  • 브루트포스
  • 그래프
  • dp
  • http
  • atdd
  • 파이썬

최근 댓글

최근 글

hELLO · Designed By 정상우.
코택
[Spring] 스프링 트랜잭션 전파와 롤백
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.