정리 배경
@Transactional은 처음에는 단순해 보이지만, 실제로 사용해보면 예상과 다른 동작을 겪게 됩니다. 전파 옵션(REQUIRED, REQUIRES_NEW), rollback-only, 프록시, ThreadLocal 같은 개념들이 얽혀 있기 때문입니다.
이번 정리의 목표는 다음 세 가지입니다.
- 트랜잭션 전파 방식 이해 (REQUIRED vs REQUIRES_NEW)
- rollback-only가 언제 설정되고, 어떻게 동작하는지
- 트랜잭션 매니저가 싱글톤인데 상태 충돌이 발생하지 않는 이유
1. 트랜잭션 전파 (Propagation)
REQUIRED (기본값)
@Transactional
public void outer() {
inner();
}
- outer()에서 트랜잭션이 시작되면, inner()는 동일한 트랜잭션을 공유합니다.
- inner()에서 예외가 발생하면 전체 트랜잭션이 롤백됩니다.
- 동일 트랜잭션을 공유하므로, rollback-only가 설정되면 상위 메서드도 롤백됩니다.
REQUIRES_NEW
@Transactional
public void outer() {
try {
inner(); // 별도 트랜잭션
} catch (Exception e) {
// outer 트랜잭션은 커밋 가능
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() {
throw new RuntimeException();
}
- inner() 실행 시 기존 트랜잭션은 잠시 중단되고, 새로운 트랜잭션이 시작됩니다.
- inner()에서 예외가 발생해도 outer() 트랜잭션에는 영향을 주지 않습니다.
- 하위 트랜잭션이 실패하더라도 상위 트랜잭션은 커밋 가능합니다.
2. rollback-only 플래그
예외 발생 시 rollback-only 마킹
@Transactional
public void method() {
throw new RuntimeException();
}
- RuntimeException이 발생하면 현재 트랜잭션은 rollback-only 상태로 전환됩니다.
- 이후 커밋을 시도해도 무조건 롤백됩니다.
rollback-only 상태에서 예외를 catch해도 무의미
@Transactional
public void facade() {
try {
child();
} catch (Exception e) {
// 예외는 잡았지만 rollback-only는 이미 설정됨
}
}
@Transactional
public void child() {
throw new RuntimeException();
}
- REQUIRED 전파이므로 facade()와 child()는 같은 트랜잭션을 사용합니다.
- child()에서 예외가 발생하면 rollback-only가 설정됩니다.
- 예외를 facade()에서 잡더라도, 커밋 시점에 롤백되며 UnexpectedRollbackException이 발생할 수 있습니다.
3. 싱글톤 트랜잭션 매니저와 ThreadLocal
트랜잭션 매니저는 싱글톤, 상태는 ThreadLocal로 관리
- PlatformTransactionManager는 스프링 빈으로 싱글톤으로 관리됩니다.
- 트랜잭션의 상태(rollback-only, 커넥션 등)는 ThreadLocal을 사용해 스레드마다 분리 저장됩니다.
- 덕분에 여러 요청이 동시에 들어와도 트랜잭션 간 상태 충돌이 발생하지 않습니다.
관련 클래스 요약
클래스역할
| TransactionStatus | 트랜잭션 상태 보관 (e.g. isRollbackOnly) |
| TransactionSynchronizationManager | 현재 스레드의 트랜잭션 리소스를 저장 |
| AbstractPlatformTransactionManager | 트랜잭션 시작/커밋/롤백 로직 처리 |
핵심 요약
- REQUIRED는 트랜잭션을 공유하므로, 하위 메서드에서 예외가 발생하면 rollback-only가 설정되어 전체 트랜잭션이 롤백됩니다.
- 예외를 catch하더라도 rollback-only 상태에서는 트랜잭션 커밋이 불가능합니다.
- REQUIRES_NEW는 별도의 트랜잭션을 생성하므로, 하위 트랜잭션 실패가 상위 트랜잭션에 영향을 주지 않습니다.
- 트랜잭션 매니저는 싱글톤이지만 ThreadLocal 덕분에 스레드별로 상태가 안전하게 분리됩니다.
정리하면서 느낀 점
- rollback-only는 한 번 설정되면 되돌릴 수 없습니다. 예외를 잡았는지보다 트랜잭션이 커밋 가능한 상태인지가 더 중요합니다.
- ThreadLocal은 단순히 동시성 문제 해결을 넘어서, 트랜잭션 격리에 핵심적인 역할을 합니다.
- REQUIRES_NEW는 실패할 수 있는 작업(예: 감사 로그)을 분리 처리할 때 유용합니다.
'Spring' 카테고리의 다른 글
| ApplicationEvent로 비즈니스 경계 나누기: 고민과 선택 (3) | 2025.08.28 |
|---|---|
| 결제 시스템에 Circuit Breaker를 도입하며 – 계층별 책임 분리와 안정성 확보 전략 (1) | 2025.08.22 |
| DB 인덱스 설계와 Redis 캐싱(Port-Adapter) 적용기 (2) | 2025.08.15 |
| 도메인 검증 책임은 어디에 둘까요? – Service vs Facade, 저는 이렇게 선택했습니다 (2) | 2025.08.01 |
| @Transactional 내부 호출 문제와 해결 방법 (0) | 2025.03.12 |