Spring

[Spring] 트랜잭션 전파 정리

꾸준함. 2023. 4. 30. 03:39

개요

트랜잭션이 이미 진행 중인데 추가로 트랜잭션을 수행할 때 추가 트랜잭션이 어떻게 동작할지 결정하는 것을 트랜잭션 전파 혹은 Transaction Propagation이라고 합니다.

이번 게시글에서는 스프링에서 제공하는 트랜잭션 전파에 대해 간단히 정리해 보겠습니다.

이해를 돕기 위해 아래 게시글을 먼저 보고 오는 것을 추천드립니다.

https://jaimemin.tistory.com/2271

 

@Transactional 동작 원리

용어 정리 트랜잭션 관리에는 크게 두 가지 방법이 있습니다. 선언 방식의 트랜잭션 관리 프로그래밍 방식의 트랜잭션 관리 선언적 트랜잭션 관리 @Transactional 애노테이션 하나만 선언해서 편리

jaimemin.tistory.com

 

트랜잭션 전파 옵션

스프링 프레임워크는 다양한 트랜잭션 전파 옵션을 아래와 같이 제공합니다.


 

  • REQUIRED
    • 트랜잭션의 default 설정이며 가장 많이 사용하는 설정
    • 기존 트랜잭션이 없으면 생성하고, 있으면 기존 트랜잭션에 참여
  • REQUIRES_NEW
    • REQUIRED와 달리 항상 새로운 트랜잭션 생성
  • SUPPORT
    • 기존 트랜잭션이 없으면 없는 대로 진행하고 있으면 참여
  • NOT_SUPPORT
    • 기존 트랜잭션 유무와 상관없이 트랜잭션 없이 진행
    • 기존 트랜잭션이 있을 경우 해당 트랜잭션은 보류
  • MANDATORY
    • 반드시 트랜잭션이 있어야 하는 옵션이며 기존 트랜잭션이 없으면 exception 발생
  • NEVER
    • MANDATORY와 상반되는 옵션이며 기존 트랜잭션이 있으면 예외 발생
  • NESTED
    • 기존 트랜잭션이 없을 경우 새로운 트랜잭션 생성
    • 기존 트랜잭션이 있으면 중첩 트랜잭션 생성
    • 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만 중첩 트랜잭션은 외부 트랜잭션에 영향을 주지 않음
    • 즉, 중첩 트랜잭션이 롤백이 되어도 외부 트랜잭션은 커밋할 수 있음
    • 하지만 외부 트랜잭션이 롤백될 경우 중첩 트랜잭션도 같이 롤백
    • 해당 옵션은 DB 벤더마다 지원 유무가 다르며 JPA에서 사용할 수 없음

 

실무에서는 주로 REQUIRED와 REQUIRES_NEW 옵션을 사용하므로 이 두 옵션 위주로 내용을 정리하겠습니다.

 

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

스프링 트랜잭션을 이해하기 위해서는 물리 트랜잭션과 논리 트랜잭션의 개념을 알아야 합니다.

이해를 돕기 위해 트랜잭션 옵션이 디폴트 옵션인 REQUIRED라고 가정하고 이 두 개념을 정의를 하겠습니다.

  • 물리 트랜잭션: 실제 DB에 적용되는 트랜잭션
  • 논리 트랜잭션: TransactionManager를 통해 트랜잭션을 사용하는 단위이며 물리 트랜잭션 내 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋이 됨, 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백

 

위 내용을 그림으로 그리면 아래와 같습니다.

 

 

트랜잭션이 이미 진행 중인데 추가로 트랜잭션을 수행할 경우 물리 트랜잭션 내 모든 트랜잭션이 성공적으로 커밋이 되어야 물리 트랜잭션이 커밋이 되고 하나라도 실패하여 롤백할 경우 물리 트랜잭션은 롤백을 진행합니다.

다시 강조하지만 위 내용은 @Transactional 전파 옵션이 디폴트 옵션인 REQUIRED일 경우 적용되는 내용입니다.

 

위 내용을 검증하기 위해 실제 테스트 코드를 돌려보면 내부 트랜잭션이 성공하여도 바로 커밋하는 것이 아니라 외부 트랜잭션까지 성공해야 커밋을 수행하는 것을 확인할 수 있습니다.

이는 내부 트랜잭션이 실제 물리 트랜잭션을 커밋할 경우 트랜잭션이 끝나버리기 때문에 트랜잭션을 처음 시작한 외부 트랜잭션에서 이어갈 수 없기 때문입니다.

스프링에서는 이처럼 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 설계를 했습니다. (REQUIRED 옵션)


논리 트랜잭션이 모두 성공하여 실제 커밋이 된 경우

 

테스트 코드를 돌려보면 여러 로그가 찍히는데 여기서 주목해야 할 점은 내부 트랜잭션을 시작할 때 이미 존재하는 트랜잭션에 참여한다는 로그와 외부 트랜잭션까지 성공해야 비로소 실제 물리 트랜잭션 커밋을 진행한다는 점입니다.

 

그렇다면 이번에는 외부 트랜잭션 혹은 내부 트랜잭션이 실패하여 롤백할 경우 실제 물리 트랜잭션도 롤백이 되는지 확인해 보겠습니다.


내부 트랜잭션이 실패하여 롤백하는 경우

 

위 테스트 코드를 통해 외부 트랜잭션 혹은 내부 트랜잭션이 실패하여 롤백할 경우 실제 물리 트랜잭션도 롤백이 되는 것을 확인할 수 있습니다.

위 로그에서는 Participating transaction failed - marking existing transaction as rollback-only 이 부분을 주목해야 합니다.

내부 트랜잭션이 롤백될 경우 실제 롤백을 진행하는 것이 아니라 rollback-only라고 마킹을 합니다.

그러면 트랜잭션 매니저가 마킹된 내용을 보고 실제 물리 트랜잭션 롤백을 진행합니다.

바로 롤백을 진행하지 않는 이유는 내부 트랜잭션에서 롤백을 진행해 버릴 경우 DB 커넥션이 끊기고 트랜잭션이 끝나버리기 때문입니다.

참고로 rollback-only 마킹이 있을 경우 스프링은 UnexpectedRollbackException 예외를 던집니다.

 

REQUIRES-NEW

여태까지는 디폴트 옵션인 REQUIRED 옵션에 대한 내용이었고 지금부터는 실무에서 종종 쓰이는 REQUIRES-NEW 옵션에 대해 간략하게 정리해 보겠습니다.

앞선 내용을 복습하자면 REQUIRES-NEW 옵션을 propagation 옵션으로 부여할 경우 기존 트랜잭션이 있더라도 새로운 트랜잭션을 생성합니다.

즉, 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션으로 묶인 것이 아니라 별도 물리 트랜잭션을 가진다는 뜻이며 DB 커넥션이 동시에 2개 사용됩니다. (경우에 따라 성능에 영향을 줄 수 있음)

위 케이스의 경우 REQUIRES-NEW 옵션이 부여된 내부 트랜잭션이 실패하여 롤백이 되더라도 외부 트랜잭션이 성공할 경우 외부 트랜잭션의 물리 트랜잭션은 커밋이 됩니다.

테스트 코드를 확인하면 아래와 같습니다.


REQUIRES-NEW

 

테스트 코드를 돌린 후 로그를 확인하면 내부 트랜잭션이 실패하여 롤백이 되었지만 외부 트랜잭션과는 별개의 물리 트랜잭션이기 때문에 외부 트랜잭션은 커밋이 되었음을 확인할 수 있습니다.

 

위 옵션이 실무에서 사용되는 경우는 보통 비즈니스 로직을 수행하는 도중 로그를 DB에 저장할 때 사용됩니다.

로그의 경우 지금 당장 실패하더라도 보통 백업이 되어있기 때문에 추후 예약 배치를 돌려 다시 저장하면 됩니다.

이런 경우 로그 저장하는 메서드에 @Transactional을 걸되 전파 옵션을 REQUIRES-NEW를 부여하여 로그 저장에 실패하더라도 비즈니스 로직이 성공했다면 핵심 로직은 커밋이 되도록 적용을 합니다.

 

앞서 설명했듯이 REQUIRES-NEW 옵션의 경우 DB 커넥션을 두 개 사용하기 때문에 성능에 영향을 끼칠 수가 있습니다.

성능이 중요할 경우 로그 저장하는 메서드도 REQUIRED로 전파 옵션을 부여하고 Facade 디자인 패턴을 적용하여 비즈니스 로직을 수행하는 서비스를 먼저 호출한 뒤 로그를 저장하는 서비스를 호출하여 HTTP 요청에 동시에 2개의 DB 커넥션을 사용하는 것을 방지할 수 있습니다.

 

참고

스프링 DB 2편 - 데이터 접근 활용 기술 (김영한 강사님)

 

반응형