이번글에서는 @Transactional propagation 에 대해서 정리해보겠다. 그 전에 트랜잭션 관리 방법에 대해서 간단하게 정리하겠다.
Application 을 개발할 때, Transaction 관리를 두 가지중 선택할 수 있다.
1. Programmatic Transaction Management
2. Declarative Transaction Management
Programmatic Transaction Management 는 말 그대로 Transaction 관리를 코드로 직접 구현한다.
Declarative Transaction Management 는 해석하면 선언적 트랜잭션 관리이다. 흔히 Spring framework 를 사용할 때 설정을 해주며 트랜잭션이 필요한 비즈니스 로직을 구현할 때 @Transactional 어노테이션을 사용한다. 위에서 rollback 과 commit 을 코드로 관리하는 반면, AOP 를 통해서 관리된다. 따라서 보다 비즈니스 로직에 집중할 수 있다.
Programmatic transaction management Vs Declarative transaction management
Programmatic transaction management 는 보통 transaction 의 수가 적을 때 적합한 방식이다. 예를 들어, 자원에 대한 update operation 단지 몇 가지일 경우이다. transaction proxies 설정을 원하지 않을 때 적합하다. 이러한 경우에, TransactionTemplate 을 사용하는 것이 좋다.
반대로, application 이 수많은 transaction operation 이 필요할 경우, Declarative transaction management 가 더 적합하다. 이는 business logic 과 transaction management 을 분리시켜 주며, 설정 또한 그리 어렵지 않다. Spring framework 를 사용할 때, transaction management 설정은 어렵지 않다.
그렇다면 선언적 트랜잭션 관리인 @Transactional 로..
Transaction 관리 설정은 자세하게 다루지 않겠다. Spring 에서 관리되는 TransactionManager 는 PlatformTransactionManager 를 구현한다. 물론, reactive application 의 경우 TransactionManager 는 ReactiveTransactionManager 를 구현한다.

@Transactional 을 까보면 아래와 같은 코드를 확인할 수 있다.
propagation 과 isolation 과 관련된 부분만 발췌했다.
두 가지 이외에도, timeout, read-only, rollback conditions 등을 설정할 수 있다. 또한, transaction manager 를 통해서 구체화할 수 도 있다.
Isolation 위의 주석을 면밀하게 볼 필요가 있다.
Exclusively designed for use with Propagation#REQUIRED or Propagation#REQUIRES_NEW since it only applies to newly started transactions. Consider switching the "validateExistingTransactions" flag to "true" on your transaction manager if you'd like isolation level declarations to get rejected when participating in an existing transaction with a different isolation level.
새롭게 시작된 트랜잭션에만 적용되기에, Propagation#REQUIRED or Propagation#REQUIRES_NEW 와 함께 사용되도록 배타적으로 설계되었다.격리 수준이 다른 기존 트랜잭션이 참여할 때 격리 수준 선언이 거부되도록하려면 트랜잭션 관리자의 "validateExistingTransactions" flag 를 true 로 전환하는 것이 좋다.
왜 REQURIED 와 REQUIRES_NEW 와 함께 사용할 수 있도록 배타적으로 설계 했는지 확인해보자.
아래의 Propagation enum 을 확인해보자.
우선 Propagation 이란 트랜잭션 lifecycle 과 관련된 설정이라고 이해하면 된다.
REQUIRED Propagation
REQUIRED 는 default 이다. Spring 은 활성화된 트랜잭션이 있는지 확인한다. 그리고 존재하지 않는 경우 새로운 트랜잭션을 생성한다. 비즈니스 로직은 활성화된 트랜잭션이 있으면 기존의 트랜잭션에 참여하고, 없는 경우 새롭게 생성된 트랜잭션을 바탕으로 실행된다.
Caller 는 firstTransaction() 을 call 한 경우 활성화된 트랜잭션이 없기 때문에 T1 이 생성된다. 그리고 secondTransaction() 이 call 되는데, 이 때 트랜잭션 매니저는 기존에 활성화된 트랜잭션을 확인하고, T1 을 반환하며 T1 에 secondTransaction() join 되어 트랜잭션이 실행된다.
SUPPORTS Propagation
SUPPORTS 도 활성화된 트랜잭션이 있는지 확인한다. 존재한다면 존재하는 트랜잭션을 사용한다. 만약 활성화 된 트랜잭션이 없다면, 비즈니스 로직은 non-transactional 하게 실행된다.
MANDATORY Propagation
MANDATORY 도 활성화된 트랜잭션이 있는지 확인한다. 존재한다면 존재하는 트랜잭션을 사용한다. 활성화된 트랜잭션이 없다면 예외를 던진다.
NEVER Propagation
NEVER 의 경우 활성화된 트랜잭션이 있다면 Spring 은 예외를 던진다.
NOT_SUPPORTED Propagation
NOT_SUPPORTED 는 기존에 트랜잭션이 존재 한다면, Spring 은 현재 트랜잭션을 suspend 한다. 그리고 비즈니스 로직은 transaction 없이 실행된다.
REQUIRES_NEW Propagation
REQURIRES_NEW 는 기존에 활성화 된 트랜잭션이 있다면, 기존 트랜잭션은 suspend 된다. 그리고 새로운 트랜잭션이 생성된다.
NESTED Propagation
NESTED 는 트랜잭션이 존재하는지 확인하고, 있다면 savepoint 를 표시한다. 만약 비즈니스 로직 실행중에 예외가 발생하면 트랜잭션은 savepoint 로 롤백된다. 반대로 활성화된 트랜잭션이 없다면, REQUIRED 와 같이 처리된다.
지금까지, Propagation 에 대해 정리했다. 그렇다면 다시 돌아가서...
새롭게 시작된 트랜잭션에만 적용되기에, Propagation#REQUIRED or Propagation#REQUIRES_NEW 와 함께 사용되도록 배타적으로 설계되었다. 격리 수준이 다른 기존 트랜잭션이 참여할 때 격리 수준 선언이 거부되도록하려면 트랜잭션 관리자의 "validateExistingTransactions" flag 를 true 로 전환하는 것이 좋다.
해당 내용을 정리해보자.
REQUIRED 는 outer transaction 이 있는 경우, inner transaction 은 outer transaction 에 join 하게 된다. 하지만 만약에 outer t 와 inner t 의 격리레벨이 다르다면?
1. outer t 의 isolation level 이 READ_UNCOMMITED 일 경우.
2. inner t 의 isolation level 이 READ_COMMITED 일 경우.
outer t 에서는 dirty read 가 발생할 수 있고, inner t 에서는 phantom row 가 발생할 수 있다. 극단적인 예이지만, 포인트는 isolation level 을 다르게 설정한 transaction 이 참여할 경우이다. 해당 경우, 예기치 못한 버그를 유발할 수 있고, 이느 꽤나 심각한 이슈가 될 수도 있다. 따라서, 기본 "validateExistingTransactions" 설정 false 를 true 로 변경하여, Propagation REQUIRED 인 두 메서드가 논리적으로 하나의 Transaction 으로 묶일 때 outer t 와 Inner t 의 isolation level 을 검증할 필요가 있다.
REQUIRED 를 사용할 때 한 가지 더 주의할 점이 있다.
각각의 메서드가 Propagation 이 REQUIRED 일 때, outer t 와 inner t 는 물리적으로는 하나의 트랜잭션으로 묶이지만, 논리적으로는 두 개의 트랜잭션 scope 가 생성된다. 각각의 논리적 트랜잭션 scope 는 rollback-only status 를 결정할 수 있다. 그래서 inner t scope 에 새겨진 rollback-only marker 는 outer t 가 commit 할 기회에 영향을 줄 수 있다.
따라서, outer t scope 는 자체적으로 rollback 을 결정할 수 없다. inner scope t 에 의존적이게 된다. 따라서 rollbacK 은 예상치 못하게 발생할 수 있다. UnexpectedRollbackException 이 던져진다. 전체 Transaction Caller 는 commit 이 실제로 수행되지 않았지만 commit 이 되었다고 잘못 인지하면 안되기 위해서 발생시키는 예외이다. 따라서 innert t scope ( 전체 Transaction Caller 는 인식하지 못함 ) 이 rollback-only 로 트랜잭션을 표시하더라도, 전체 Transaction Caller 는 여전히 commit 을 호출한다. 따라서 전체 Transaction caller 는 롤백이 수행되었음을 명확하게 나타 내기 위해 UnexpectedRolbackException 을 수신해야 한다.
혹시 오역이 있을 경우를 위해서, 원문을 아래에 표시합니다.
https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation
When the propagation setting is PROPAGATION_REQUIRED
, a logical transaction scope is created for each method upon which the setting is applied. Each such logical transaction scope can determine rollback-only status individually, with an outer transaction scope being logically independent from the inner transaction scope. In the case of standard PROPAGATION_REQUIRED
behavior, all these scopes are mapped to the same physical transaction. So a rollback-only marker set in the inner transaction scope does affect the outer transaction’s chance to actually commit.
However, in the case where an inner transaction scope sets the rollback-only marker, the outer transaction has not decided on the rollback itself, so the rollback (silently triggered by the inner transaction scope) is unexpected. A corresponding UnexpectedRollbackException
is thrown at that point. This is expected behavior so that the caller of a transaction can never be misled to assume that a commit was performed when it really was not. So, if an inner transaction (of which the outer caller is not aware) silently marks a transaction as rollback-only, the outer caller still calls commit. The outer caller needs to receive an UnexpectedRollbackException
to indicate clearly that a rollback was performed instead.
댓글
댓글 쓰기