이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
REQUIRED,REQUIRES_NEW,NESTED는 트랜잭션 참여 방식이 어떻게 다른가?- Isolation level은 어떤 문제를 해결하고 어떤 문제를 해결하지 못하는가?
- checked exception과 runtime exception은 rollback 정책에서 어떻게 다르게 다뤄지는가?
개요
@Transactional을 제대로 이해하려면 세 가지 축을 나누어야 한다. Propagation은 이미 트랜잭션이 있을 때 어떻게 참여할지 정한다. Isolation은 동시에 실행되는 트랜잭션 사이의 읽기 일관성을 정한다. Rollback 정책은 어떤 실패에서 되돌릴지 정한다.
이 세 축을 구분하지 못하면 트랜잭션 설정은 암호처럼 보인다. 실제로는 각각 다른 질문에 답한다. 이미 열린 트랜잭션이 있는가, 동시에 읽고 쓰는 데이터가 어떤 수준으로 보호되어야 하는가, 어떤 예외를 실패로 볼 것인가다.
Propagation
REQUIRED는 기본값이며 기존 트랜잭션이 있으면 참여하고 없으면 새로 만든다. 대부분의 서비스 유스케이스에 맞는다.
같은 물리 트랜잭션에 참여한 내부 범위에서 rollback-only가 표시되면 바깥 범위가 정상 종료되어 commit하려고 해도 실제로는 rollback된다. 이때 바깥 호출자가 commit된 것으로 오해하지 않도록 Spring은 UnexpectedRollbackException을 던질 수 있다.
REQUIRES_NEW는 현재 트랜잭션을 중단하고 새 트랜잭션을 시작한다. 감사 로그, 실패 기록, 알림 발행 기록처럼 본 트랜잭션 rollback과 독립적으로 남겨야 하는 경우에 쓰인다. 다만 바깥 트랜잭션의 리소스가 유지된 상태에서 안쪽 트랜잭션이 새 커넥션을 요구할 수 있으므로, 동시 요청 수보다 여유 있는 커넥션 풀 크기와 deadlock 가능성을 고려해야 한다.
NESTED는 저장점 기반 부분 rollback 모델이다. 전체 트랜잭션 안에서 일부 작업만 rollback하고 바깥 트랜잭션은 계속 진행하는 상황에 맞지만, 데이터베이스와 트랜잭션 매니저 지원 여부에 의존한다.
Isolation
Isolation은 dirty read, non-repeatable read, phantom read 같은 동시성 읽기 현상을 제어한다. 격리 수준을 높이면 더 일관된 읽기를 기대할 수 있지만 lock 비용과 처리량 저하가 생길 수 있다.
중요한 점은 isolation이 모든 비즈니스 경쟁 조건을 해결하지 않는다는 것이다. 예를 들어 쿠폰 수량 100개 제한, 좌석 예약, 재고 차감은 단순 읽기 현상보다 더 넓은 경쟁 조건이다. 이런 경우에는 optimistic locking, pessimistic locking, unique constraint, 재시도 전략을 함께 검토해야 한다.
Rollback
Spring의 선언적 트랜잭션은 기본적으로 unchecked exception과 Error에서 rollback한다. checked exception은 기본적으로 commit될 수 있다. checked exception을 실패로 간주해 rollback해야 한다면 rollbackFor를 명시한다. Spring 6.2부터는 전역 기본 rollback 정책을 모든 예외로 바꾸는 선택지도 있지만, 프로젝트 전체 예외 모델과 함께 결정해야 한다.
@Transactional(rollbackFor = IOException.class)
public void importMembers(Path file) throws IOException {
List<Member> members = readMembers(file);
memberRepository.saveAll(members);
}이 예제에서 파일 처리 실패를 전체 작업 실패로 보고 DB 변경도 되돌리고 싶다면 rollbackFor가 필요하다. 다만 checked exception을 많이 쓰는 설계 자체가 Service 실패 모델에 맞는지도 함께 검토해야 한다.
반대로 예외를 메서드 안에서 잡아 삼키면 기본 rollback 규칙이 작동하지 않을 수 있다. 실패 응답을 반환해야 하더라도 데이터 변경을 되돌려야 한다면 예외를 다시 던지거나 명시적으로 rollback-only를 표시하는 방식까지 설계해야 한다.
코드 예제
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordFailure(String action, Throwable cause) {
auditRepository.save(AuditLog.failure(action, cause.getMessage()));
}
}실패 기록이 본 작업과 독립적으로 남아야 한다면 REQUIRES_NEW가 의미 있다. 반대로 본 작업이 rollback될 때 기록도 함께 없어져야 한다면 기본 REQUIRED가 맞다.
실전 팁
- 전파 설정은 “같이 성공해야 하는가, 독립적으로 남아야 하는가”로 판단한다.
REQUIRES_NEW를 쓰기 전에 커넥션 풀과 호출 빈도를 생각한다.- 내부 트랜잭션 실패를 잡아도 바깥 트랜잭션이
UnexpectedRollbackException을 받을 수 있는 구조인지 확인한다. - Isolation을 높이기 전에 실제로 어떤 동시성 문제가 발생하는지 재현한다.
- rollback 정책은 예외 계층 설계와 함께 본다.
- 트랜잭션 설정이 복잡해지면 유스케이스를 더 작은 책임으로 나누어야 할 수 있다.
위험 신호!
- 문제를 정확히 모른 채 isolation을 최고 수준으로 올린다.
- 모든 보조 작업에
REQUIRES_NEW를 붙인다. - checked exception이 발생했는데 rollback될 것이라고 막연히 기대한다.
- catch 후 로그만 남기고 정상 반환하면서 rollback도 될 것이라고 기대한다.
NESTED를 쓰면서 DB와 TransactionManager 지원 여부를 확인하지 않는다.- rollback을 위해 예외를 삼킨 뒤 boolean만 반환한다.
확인 질문
확인 질문
REQUIRED와REQUIRES_NEW의 차이는 무엇인가?
REQUIRED는 기존 트랜잭션에 참여하고,REQUIRES_NEW는 기존 트랜잭션을 중단한 뒤 새 트랜잭션을 시작한다.- Isolation level이 해결하는 문제는 무엇인가?
- 동시에 실행되는 트랜잭션 사이의 읽기 이상 현상과 일관성 수준을 제어한다.
- checked exception에서 rollback하려면 무엇을 해야 하는가?
rollbackFor를 명시하거나 예외 모델을 runtime exception 중심으로 재설계해야 한다.