개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Spring › 데이터 접근과 트랜잭션

트랜잭션 전파

예외가 나면 어디까지 롤백되나

주문을 저장하는 메서드가 로그를 저장하는 다른 빈의 메서드를 부릅니다. 둘 다 @Transactional입니다. 전파 속성, 예외가 나는 위치와 종류, 예외를 잡는지에 따라 주문과 로그 중 무엇이 DB에 남는지 달라집니다.

실행으로 대조 · 스프링 7.0.9와 H2에서 72가지 조합을 실행해 대조

이 실습에 나오는 개념

@Transactional
메서드나 클래스에 붙여 그 범위를 하나의 트랜잭션으로 실행하게 하는 애너테이션이다. 프록시가 메서드 실행 전에 트랜잭션을 시작하고, 정상적으로 끝나면 커밋, 예외가 발생하면 규칙에 따라 롤백한다. 트랜잭션을 시작하고 끝내는 코드를 직접 쓰지 않아도 되어 선언적 트랜잭션이라고 한다.
트랜잭션 전파(Propagation)
트랜잭션이 이미 진행 중일 때 @Transactional 메서드가 호출되면 어떻게 할지 정하는 설정이다. 기본값 REQUIRED는 진행 중인 트랜잭션에 참여하고 없으면 새로 시작한다. REQUIRES_NEW는 기존 트랜잭션을 잠시 멈추고 항상 별도의 새 트랜잭션을 시작한다. NESTED는 같은 트랜잭션 안에서 세이브포인트를 두어 부분 롤백을 가능하게 한다.
롤백 규칙과 rollbackFor
@Transactional은 기본적으로 RuntimeException과 Error가 메서드 밖으로 던져질 때만 롤백하고, Checked 예외가 던져지면 커밋한다. Checked 예외에서도 롤백하려면 rollbackFor 속성에 그 예외를 지정한다. 또한 예외를 메서드 안에서 잡아 처리해 버리면 프록시가 예외를 알 수 없어 롤백되지 않는다. 단, 그 예외가 같은 트랜잭션에 참여한 다른 @Transactional 메서드에서 던져진 것이라면 이미 롤백 전용으로 표시되어 있어, 잡아서 처리해도 커밋 시점에 UnexpectedRollbackException이 발생한다.
세이브포인트
트랜잭션 진행 중에 표시해 두는 중간 지점이다. 이후 문제가 생기면 트랜잭션 전체가 아니라 그 지점까지만 되돌릴 수 있다. 스프링의 NESTED 전파 방식이 이 기능을 이용한다.
Checked 예외와 Unchecked 예외
Unchecked 예외는 RuntimeException과 그 하위 클래스로(명세상으로는 Error 계열도 unchecked에 속한다), 컴파일러가 처리 여부를 검사하지 않는다(NullPointerException, IllegalArgumentException 등). Checked 예외는 그 밖의 Exception으로, 반드시 catch로 처리하거나 throws로 선언해야 컴파일된다(IOException, SQLException 등). 스프링의 @Transactional은 기본적으로 RuntimeException과 Error에서만 롤백하고 Checked 예외에서는 커밋한다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. placeOrder() 호출. 메서드를 실행하기 전에 프록시가 트랜잭션 A를 시작합니다(커넥션 1개 사용).
  2. 주문을 INSERT했습니다. 아직 커밋 전이라 트랜잭션 A 안에서만 보입니다.
  3. saveLog() 호출. 전파 속성이 REQUIRED이고 이미 트랜잭션 A가 진행 중이므로, 새로 만들지 않고 A에 참여합니다.
  4. 로그를 INSERT했습니다. 트랜잭션 A에 속합니다.
  5. saveLog() 안에서 IllegalStateException이 발생해 메서드 밖으로 나갑니다.
  6. saveLog()의 프록시: 언체크 예외는 롤백 대상입니다. 하지만 이 메서드는 A에 참여만 했기 때문에 직접 롤백하지 못합니다. 대신 A에 "롤백만 가능(rollback-only)" 표시를 남기고 예외를 그대로 던집니다.
  7. placeOrder()가 예외를 잡지 않아 IllegalStateException이 그대로 밖으로 나갑니다. placeOrder()의 프록시: 언체크 예외는 롤백 대상입니다. 프록시가 트랜잭션 A를 롤백합니다. A 안에서 한 작업이 모두 취소됩니다.

실행 결과

코드

@Service class OrderService {
    @Transactional
    public void placeOrder() {
        orderDao.insert(order);            // 주문 저장
        logService.saveLog();
    }
}

@Service class LogService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void saveLog() {
        logDao.insert(log);                // 로그 저장
        throw new IllegalStateException();
    }
}

이 모형의 전제

saveLog()는 다른 빈의 메서드라서 프록시를 거쳐 호출됩니다(같은 클래스 안에서 부르면 안쪽 @Transactional이 적용되지 않습니다. 이 내용은 "프록시와 자기 호출" 실습에서 다룹니다). 규칙은 스프링 프레임워크의 기본 동작을 따랐습니다. 언체크 예외와 Error에만 롤백하고, 참여한 트랜잭션에서 롤백 대상 예외가 나가면 전체에 rollback-only 표시를 남깁니다. NESTED는 JDBC 세이브포인트로 구현되며 JdbcTemplate이나 MyBatis가 쓰는 DataSourceTransactionManager에서 동작합니다. JPA(JpaTransactionManager + Hibernate)에서는 NESTED를 쓰면 NestedTransactionNotSupportedException이 납니다. 세이브포인트가 JDBC 커넥션에만 적용되고 영속성 컨텍스트에는 적용되지 않기 때문입니다. 72가지 조합 모두를 실제 스프링(스프링 프레임워크 7.0.9, DataSourceTransactionManager, H2 데이터베이스)에서 실행해, 주문과 로그가 DB에 남는지, 호출한 쪽이 받는 것, 동시에 쓴 커넥션 수가 화면과 일치하는 것을 확인했습니다.

앞 실습: 트랜잭션 기본 · 다음 실습: JdbcTemplate과 예외 변환

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요