1. 트랜잭션 전파란?
- 트랜잭션 전파(Transaction Propagation)는 진행 중인 트랜잭션의 범위에서 새로운 트랜잭션이 시작될 때, 이 두 트랜잭션을 어떻게 처리할지 결정하는 정책입니다.
- 스프링에 서는
@Transactional
애노테이션의propagation
속성을 통해 트랜잭션 전파를 설정할 수 있습니다. - 트랜잭션 전파(Transaction Propagation)는 스프링 프레임워크에서 제공하는 기능으로, 데이터베이스 자체에 있는 기능이 아닙니다.
- 스프링은 여러 데이터베이스 트랜잭션 관리 방식을 추상화하여 일관된 API로 제공합니다.
- 데이터베이스는 단순히 트랜잭션의 시작(BEGIN), 커밋(COMMIT), 롤백(ROLLBACK)만 알고 있습니다.
- 중첩 트랜잭션이나 트랜잭션 간 관계는 데이터베이스가 아닌 스프링이 관리합니다.
- 스프링은 TransactionManager를 사용하여 트랜잭션 상태를 추적하고 관리합니다.
- 트랜잭션 동기화 매니저(Transaction Synchronization Manager)를 통해 현재 쓰레드의 트랜잭션 정보를 유지합니다.
1.1 주요 트랜잭션 개념
1.1.1 물리적 트랜잭션 vs 논리적 트랜잭션
물리적 트랜잭션(Physical Transaction)
- 실제 데이터베이스와의 연결에서 시작되고 커밋 또는 롤백되는 실제 트랜잭션입니다.
- 데이터베이스 연결, 커밋, 롤백과 같은 실제 리소스 작업을 수행합니다.
- 실제 커넥션을 통해서 트랜잭션을 시작(
setAutoCommit(false))
하고, 실제 커넥션을 통해서 커밋, 롤백하는 단위입니다. - 예: 하나의 데이터베이스 연결에서 실행되는 실제 트랜잭션.
논리적 트랜잭션(Logical Transaction)
- 스프링이 트랜잭션 범위를 관리하기 위해 사용하는 개념적인 트랜잭션 단위입니다
@Transactional
이 적용된 각 메서드는 하나의 논리적 트랜잭션 범위를 가집니다- 여러 논리적 트랜잭션이 하나의 물리적 트랜잭션을 공유할 수 있습니다.
- 즉 하나의 물리적 트랜잭션 안에 여러 개의 논리적 트랜잭션이 존재할 수 있습니다.
- 다시 말하면 물리적 트랜잭션이 여러개의 논리적 트랜잭션을 묶는 컨테이너와 같은 역할을 한다고 볼 수 있습니다.
- 논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위입니다.
원칙
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됩니다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됩니다.
1.1.2 외부 트랜잭션 vs 내부 트랜잭션
외부 트랜잭션(Outer Transaction)
- 먼저 시작된 트랜잭션을 의미합니다
- 다른 트랜잭션을 포함하는 더 큰 범위의 트랜잭션입니다.
- 처음 트랜잭션을 시작한 외부 트랜잭션이 물리 트랜잭션을 관리하도록 합니다.
내부 트랜잭션(Inner Transaction)
- 이미 진행 중인 트랜잭션(외부 트랜잭션) 안에서 시작되는 새로운 트랜잭션입니다.
예시
@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);
}
- TransactionStatus은 트랜잭션의 상태를 나타내는 인터페이스입니다.
outer.isNewTransaction()
은 true를 반환합니다. 외부 트랜잭션이 새로 시작되었음을 나타냅니다.inner.isNewTransaction()
은 false를 반환합니다. 내부 트랜잭션이 외부 트랜잭션에 참여하고 있음을 나타냅니다.
1.1.3 중복 커밋 문제
- 위 코드에서 재미있는 부분은 commit을 2번 하는 것입니다.
- 내부 트랜잭션을 시작할 때
Participating in existing transaction
이라는 메시지를 확인할 수 있다. - 이 메시지는 내부 트랜잭션이 기존에 존재하는 외부 트랜잭션에 참여한다는 뜻이다.
- 실행 결과를 보면 외부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통한 물리 트랜잭션을 시작(
manual commit
)하고, DB 커넥션을 통해 커밋 하는 것을 확인할 수 있다. - 그러나 내부 트랜잭션을 시작하거나 커밋할 때는 DB 커넥션을 통해 커밋하는 로그를 전혀 확인할 수 없습니다.
- 만약 내부 트랜잭션이 실제 물리 트랜잭션을 커밋하면 트랜잭션이 끝나버립니다. 때문에, 트랜잭션을 처음 시작한 외부 트랜잭션까지 이어갈 수 없습니다.
- 따라서 내부 트랜잭션은 DB 커넥션을 통한 물리 트랜잭션을 커밋하면 안됩니다.
- 스프링은 이렇게 여러 트랜잭션이 함께 사용되는 경우, 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 합니다.
- 이를 통해 트랜잭션 중복 커밋 문제를 해결한다.
동작 과정
txManager.getTransaction()
를 호출해서 외부 트랜잭션을 시작합니다.- 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성합니다.
- 생성한 커넥션을 수동 커밋 모드(
setAutoCommit(false)
)로 설정합니다.- 물리 트랜잭션을 시작합니다.
- 트랜잭션 매니저는 트랜잭션 동기화 매니저에 커넥션을 보관합니다.
- 트랜잭션 매니저는 트랜잭션을 생성한 결과를
TransactionStatus
에 담아서 반환합니다.- 여기에 신규 트랜잭션의 여부가 담겨 있습니다.
isNewTransaction
를 통해 신규 트 랜잭션 여부를 확인할 수 있습니다.- 트랜잭션을 처음 시작했으므로 신규 트랜잭션입니다.(
true
)
- 로직1이 사용되고, 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용합니다.
txManager.getTransaction()
를 호출해서 내부 트랜잭션을 시작합니다.- 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해서 기존 트랜잭션이 존재하는지 확인합니다.
- 기존 트랜잭션이 존재하므로 기존 트랜잭션에 참여합니다. 기존 트랜잭션에 참여한다는 뜻은 사실 아무것도 하지 않는다는 뜻입니다.
- 이미 기존 트랜잭션인 외부 트랜잭션에서 물리 트랜잭션을 시작했습니다. 그리고 물리 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 담아두었습니다.
- 따라서 이미 물리 트랜잭션이 진행 중이므로 그냥 두면 이후 로직이 기존에 시작된 트랜잭션을 자연스럽게 사용하게 되는 것입니다.
- 이후 로직은 자연스럽게 트랜잭션 동기화 매니저에 보관된 기존 커넥션을 사용하게 됩니다.
- 트랜잭션 매니저는 트랜잭션을 생성한 결과를
TransactionStatus
에 담아서 반환하는데, 여기에서isNewTransaction
를 통해 신규 트랜잭션 여부를 확인할 수 있습니다.- 여기서는 기존 트랜잭션에 참여했기 때문에 신규 트랜잭션이 아닙니다. (
false
)
- 여기서는 기존 트랜잭션에 참여했기 때문에 신규 트랜잭션이 아닙니다. (
- 로직2가 사용되고, 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 외부 트랜잭션이 보관한 커넥션을 획득해서 사용합니다.
- 로직2가 끝나고 트랜잭션 매니저를 통해 내부 트랜잭션을 커밋합니다.
- 트랜잭션 매니저는 커밋 시점에 신규 트랜 잭션 여부에 따라 다르게 동작합니다.
- 이 경우 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않습니다.
- 이 부분이 중요한데, 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버립니다.
- 아직 트랜잭션이 끝난 것이 아니기 때문에 실제 커밋을 호출하면 안됩니다.
- 로직1이 끝나고 트랜잭션 매니저를 통해 외부 트랜잭션을 커밋합니다.
- 트랜잭션 매니저는 커밋 시점에 신규 트랜잭션 여부에 따라 다르게 동��합니다.
- 외부 트랜잭션은 신규 트랜잭션입니다. 따라서 DB 커넥션에 실제 커밋을 호출합니다.
- 트랜잭션 매니저에 커밋하는 것이 논리적인 커밋이라면, 실제 커넥션에 커밋하는 것을 물리 커밋이라 할 수 있습니다.
- 실제 데이터베이스에 커밋이 반영되고, 물리 트랜잭션도 끝납니다.
1.1.4 내부 트랜잭션 롤백
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됩니다.
- 따라서 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션도 롤백됩니다.
- 앞서 살펴본 내부 트랜잭션은 커밋 또는 롤백을 하지 않습니다.
- 그 이유는 최상위 외부 트랜잭션이 물리 트랜잭션을 관리하기 때문입니다. (최상위 외부 트랜잭션의 여부는
isNewTransaction()
을 통해 확인할 수 있습니다.) - 따라서 내부 트랜잭션에서 롤백을하면 실제 물리 트랜잭션을 롤백하는 것이 아니라 해당 트랜잭션을 rollback-only로 설정합니다.
- 그 이유는 최상위 외부 트랜잭션이 물리 트랜잭션을 관리하기 때문입니다. (최상위 외부 트랜잭션의 여부는
- 이제 최상위 외부 트랜잭션이 커밋을 시도할 때, 내부 트랜잭션이 rollback-only로 설정되어 있으면 물리 트랜잭션은 롤백하고
UnexpectedRollbackException
예외를 던집니다. UnexpectedRollbackException
예외는 내부 트랜잭션이 rollback-only로 설정되어 있지만 외부 트랜잭션은 이를 인식하지 못한 채 커밋을 시도하는 경우 발생합니다
2. 스프링의 트랜잭션 전파 설정
- 스프링에서는
@Transactional
애노테이션의propagation
속성을 통해 트랜잭션 전파를 설정할 수 있습니다. - 전파 옵션
REQUIRED
: 기본값, 외부 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜잭션을 시작합니다REQUIRES_NEW
: 외부 트랜잭션을 일시 중단하고 새로운 트랜잭션을 시작합니다NESTED
: 중첩 트랜잭션을 생성합니다SUPPORTS
: 외부 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행합니다NOT_SUPPORTED
: 외부 트랜잭션을 일시 중단하고 트랜잭션 없이 실행합니다NEVER
: 외부 트랜잭션이 있으면 예외를 발생시킵니다
2.1 예시 코드
@Transactional(propagation = Propagation.REQUIRED)
public void createPost(Post post) {
// 게시글 저장 로직
}
@Transactional
애노테이션의propagation
속성을 통해 트랜잭션 전파 설정 가능Propagation.REQUIRED
는 기본값으로, 외부 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜잭션을 시작합니다