스프링에선 진행중인 트랜잭션에서 다른 트랜잭션이 참여할 때의 합류조건을 설정할 수 있는 Propagation 옵션이 존재한다. DB에서 자체적으로 제공해주는 트랜잭션 격리레벨과 다르게 전파레벨은 스프링에서 개발자들의 편의를 위해 제공해주는 기능이다.
특히, 트랜잭션 격리 레벨과 마찬가지로 전파레벨은 cs 면접에서 자주 물어보는 주제이기도 하므로 잘 학습해두는 것이 좋을 듯하다. 이번 포스팅에선 트랜잭션 전파 레벨에 대해 작성해보도록 하겠다.
트랜잭션 전파 레벨은 총 7가지
트랜잭션 전파레벨에는 총 7가지 종류가 있다. 스프링 문서에 적혀있는 내용을 바탕으로 한 번 살펴보자.
- REQUIRED: Support a current transaction, create a new one if none exists.
- REQUIRES_NEW: Create a new transaction, and suspend the current transaction if one exists.
- SUPPORTS: Support a current transaction, execute non-transactionally if none exists.
- NOT_SUPPORTED: Execute non-transactionally, suspend the current transaction if one exists.
- MANDATORY: Support a current transaction, throw an exception if none exists.
- NESTED: Execute within a nested transaction if a current transaction exists, behave like REQUIRED otherwise.
- NEVER: Execute non-transactionally, throw an exception if a transaction exists.
보통은 REQUIRED, REQUIRES_NEW를 실무에서 많이 쓰고, 그 외에는 사용 빈도가 낮다.
스프링에서 기본(default)으로 제공하는 전파레벨은 REQUIRED이다. 우리가 @Transactional 어노테이션을 사용하면 이 트랜잭션의 전파레벨은 REQUIRED인 것이다.
한 번 학습테스트를 통해 알아보도록 하자.
학습테스트는 아래 우아한테크코스 레포를 이용했다.
https://github.com/woowacourse/jwp-hands-on/tree/solution-6-transaction
REQUIRED
REQUIRED 전파 레벨은 이미 진행중인 트랜잭션(이하 부모 트랜잭션)이 존재할 때 자식 트랜잭션은 거기에 합류하고, 부모 트랜잭션이 존재하지 않으면 자식 트랜잭션은 새 트랜잭션을 생성하는 것이다.
아래처럼 진행되는 트랜잭션 합류 코드를 보자.
REQUIRED 전파레벨 옵션의 부모 트랜잭션에
REQUIRED 전파레벨 옵션의 자식 트랜잭션이 참여하는 경우
위의 결과처럼 First Transaction (부모 트랜잭션)으로 진행된다.
부모 트랜잭션이 없을 때,
REQUIRED 전파레벨 옵션의 자식 트랜잭션이 참여하는 경우
위의 결과처럼 Second Transaction (자식 트랜잭션)으로 진행된다.
REQUIRED의 경우는 부모 트랜잭션에게 합류해서 참여하는 것으로 기억해두면 좋을 듯하다.
REQUIRED로 진행하는 사례는 매우 많지만, 간단하게 언급해보도록 하겠다.
kth990303이 자전거를 사려한다. kth990303은 자전거 샵 사장님께 돈을 드리고 자전거를 산다. 따라서 아래와 같은 연산이 한 트랜잭션 내에서 진행된다.
돈을 자전거 샵 사장님께 드리면 (A) -> kth990303은 자전거를 받는다 (B)
A, B 모두 REQUIRED 전파레벨을 가진다.
kth990303이 자전거를 받는 트랜잭션에서 예외가 발생해 해당 트랜잭션이 롤백된다고 해보자. 이 경우, kth990303은 자전거를 받지 못했기 때문에 kth990303의 돈 또한 자전거 샵 사장님께 드리는 연산은 롤백돼야 한다.
다행히 B의 트랜잭션 전파레벨이 REQUIRED였기 때문에 부모 트랜잭션에 합류된 상태로 진행됐다. 따라서 B 연산이 롤백되면 결국 해당 트랜잭션 자체가 롤백돼야 하므로 A 연산도 롤백된다.
REQUIRES_NEW
REQUIRES_NEW 전파 레벨은 이미 진행중인 트랜잭션(이하 부모 트랜잭션)이 존재할 때 부모 트랜잭션을 중단하고 별도로 자식 트랜잭션을 진행 하고, 부모 트랜잭션이 존재하지 않으면 자식 트랜잭션은 새 트랜잭션을 생성하는 것이다.
위와 달리 이번에는 REQUIRED 부모 트랜잭션에 REQUIRES_NEW 자식 트랜잭션이 합류하는 경우를 살펴보자.
REQUIRED 전파레벨 옵션의 부모 트랜잭션에
REQUIRES_NEW 전파레벨 옵션의 자식 트랜잭션이 참여하는 경우
위의 결과처럼 Second Transaction, First Transaction (자식 트랜잭션, 부모 트랜잭션) 순으로 진행된다.
부모 트랜잭션이 없을 때,
REQUIRES_NEW 전파레벨 옵션의 자식 트랜잭션이 참여하는 경우
위의 결과처럼 Second Transaction (자식 트랜잭션)으로 진행된다.
REQUIRES_NEW의 경우는 부모 트랜잭션으로부터 독립해서 선 참여하는 것으로 기억해두면 좋을 듯하다.
(단, 동일 스레드 내에서 별도의 커넥션을 잡는 것임에 유의하자.)
REQUIRES_NEW로 진행하는 사례는 아래와 같다.
A라는 댓글 관련 트랜잭션이 있고, B라는 알림 기능 트랜잭션이 있다.
댓글을 작성하면(A) -> 사용자에게 알림을 보내주는(B) 기능을 한 트랜잭션에서 진행할 것이다.
만약 A가 REQUIRED, B도 REQUIRED라면 알림 기능(B)에서 예외가 발생하여 트랜잭션이 롤백될 때, 작성한 댓글(A)도 롤백돼버리는 의도치 않은 버그가 발생할 것이다. REQUIRED의 경우 부모 트랜잭션에 합류하기 때문이다.
A가 REQUIRED, B가 REQUIRED_NEW라면 알림 기능(B)에서 예외가 발생하여 트랜잭션이 롤백된다 하더라도, 작성한 댓글(A)은 롤백되지 않고 잘 작성된 상태로 남게 될 것이다.
(다만, try-catch, 또는 @TransactionalEventListener 어노테이션 없이 코드를 작성할 경우 부모 트랜잭션도 롤백되는 걸 볼 수 있다. 이에 대한 이유는 곧 포스팅할 예정이다.)
NESTED
여기서부턴 (내가 알기론) 사용 빈도가 낮다.
따라서 위 REQUIRED, REQUIRES_NEW 보다는 간단히 작성해보도록 하겠다.
정의를 다시 한 번 보자.
- NESTED: Execute within a nested transaction if a current transaction exists, behave like REQUIRED otherwise.
부모 트랜잭션이 진행될 때, 자식 트랜잭션이 부모 트랜잭션과 중첩하게 만들어진다.
...?
이 무슨 REQUIRED, REQUIRES_NEW 사이의 어중간한 이해되지 않는 말일까?
조금 더 쉽게 풀어쓰면 아래와 같다.
부모 트랜잭션의 커밋, 롤백에 자식은 영향을 받지만, 자식 트랜잭션의 커밋, 롤백에 부모는 영향을 받지 않는다.
즉, 부모 트랜잭션에 완전히 합류하는 것은 아니지만, JPA cascade 옵션처럼 부모의 커밋, 롤백에 완전히 같은 영향을 받게 되는 것이다. 만약 자식이 롤백된다 해도, 지정해놓은 체크포인트까지만 롤백이 된다.
그 외의 경우는 REQUIRED처럼 행동한다.
그런데 만약 Hibernate를 사용하고 있다면 이 NESTED 전파레벨 옵션은 쓸 수가 없다.
Hibernate에선 SavepointManager의 구현체가 존재하지 않기 때문이다.
https://stackoverflow.com/questions/37927208/nested-transaction-in-spring-app-with-jpa-postgres
따라서 NESTED 전파레벨 옵션을 사용하는 코드들은 대부분 JdbcTemplate인 것을 확인할 수 있다.
SUPPORTS, MANDATORY
사실 여기서부턴 (내가 알기론) 사용되는 빈도가 매우 매우 낮다.
그렇기 때문에 REQUIRED, REQUIRES_NEW보다 더욱 더 간단히 작성해보도록 하겠다.
이 둘의 정의를 다시 한 번 보자.
- SUPPORTS: Support a current transaction, execute non-transactionally if none exists.
- MANDATORY: Support a current transaction, throw an exception if none exists.
둘 다 부모 트랜잭션이 존재하면 REQUIRED와 마찬가지로 합류한다.
하지만, 부모 트랜잭션이 없을 경우 SUPPORTS는 트랜잭션 없이 진행, MANDATORY는 예외를 터뜨린다는 차이가 있다.
그 외의 전파레벨 옵션에 대해선 위에 적힌 영어 정의를 참고하자.
참고
- https://github.com/woowacourse/jwp-hands-on/tree/solution-6-transaction
- https://stackoverflow.com/questions/37927208/nested-transaction-in-spring-app-with-jpa-postgres
- https://www.baeldung.com/spring-transactional-propagation-isolation
- https://developyo.tistory.com/m/250
- https://oingdaddy.tistory.com/m/28
'JAVA > JAVA | Spring 학습기록' 카테고리의 다른 글
[Spring] REQUIRED, REQUIRES_NEW 옵션과 Try-Catch (4) | 2022.10.25 |
---|---|
[Spring] REQUIRES_NEW 옵션만으론 자식이 롤백될 때 부모도 롤백된다 (2) | 2022.10.20 |
[Spring] @SpringBootTest의 webEnvironment와 @Transactional (5) | 2022.08.23 |
[Spring] @MockBean VS @SpyBean (1) | 2022.08.22 |
[Spring] log4j2를 활용한 로깅 전략을 다룬 yml 파일을 생성하자 (2) | 2022.08.21 |