본문 바로가기

트러블 슈팅

[JPA] UnexpectedRollbackException


@Transactional
을 사용한 메서드에서 예외가 발생하게 되면 Rollback 처리가 된다.

@Transational
public void saveUser() {
    userTermsRespository.saveAll(userTerms);
    User user = userRepository.save(user); // 에러 발생
}

* user를 저장하는 과정에서 UnChecked Exception이 발생하면 userTerms는 Rollback 된다. 

@Transational
public void saveUser() {
    userTermsRespository.saveAll(userTerms);
    
    try {
        User user = userRepository.save(user); // 에러 발생
    } catch (Exception e) {
        log.error("userSave Error", e);
    }
}

만일, try-catch를 사용해서 UnChecked Exception을 처리하면 Rollback 되지 않는다.


이러한 내용을 바탕으로, try-catch로 예외를 처리하여 에러가 발생해도 기존의 로직은 Rollback 되지 않도록 코드를 작성하였는데, 다음과 같은 에러가 발생하였다.

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only

 

다음은 문제의 코드이다.

public class UserService {
    @Transational
    public void saveUser(final User user) {
        		***

        userTermsRespository.saveAll(userTerms);
        User user = userRepository.save(user); // 에러 발생

       			***
    }
}

public class LogService {
    @Transational
    public void saveLog() {
        		***

        try {
            userService.saveUser(user) // 에러 발생
        } catch (Exception e) {
            log.error("userSave Error", e);
        }

        LogRespositroy.save(log);

        		***
    }
}

위와 같이, User를 저장하는 로직이 실패해도 Log는 저장되도록 코드를 작성하였으나, 실제로는 모든 로직이 Rollback 처리가 되었다.


원인

saveUser() 메서드에 @Transactional이 적용되어 있고, @Transactiuonal의 default Propagation 전략은 Propagation.REQUIRED로 부모 Transaction이 없으면 새로 생성하고 존재하면 부모 Transaction에 참여하는 전략이다. 해당 전략은 자식 Transaction에서 UnChecked Exception이 발생하여 Rollback 마크를 남기게 되면, 부모 Transaction도 함께 Rollback된다.

 

saveUser() 메서드를 자세히 살펴보면 다음과 같다.

private final Connection connection = dataSource.getConnection();

public void saveUser(final User user) {
    try {
        connection.setAutoCommit(false);

            ***

        userTermsRespository.saveAll(userTerms);
        User user = userRepository.save(user); // 에러 발생

            ***
        connection.commit();
    } catch (Exception e) {
        connection.rollback();
    }
}

userRepository.save(user)에서 에러가 발생하는 순간 Rollback 마크를 Transaction에 남기기 때문에, 부모 Transaction도 Commit 되지 않고 Rollback이 진행된다.


해결

1. @Transactional의 Propagation 전략을 PROPAGATION.NESTED로 설정하여 부모 Transaction이 자식 Transaction의 영향을 받지 않도록 한다. 

-> 해당 방법은 JDBC 3.0 스펙의 저장포인트(savepoint)를 지원하는 드라이버와 DataSourceTransactionManager를 이용할 경우에 적용 가능하다. 이외의 환경에서 사용하게 되면 다음과 같은 에러가 발생한다.

org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities

2. @Transactional의 Propagation 전략을 PROPAGATION.REQUIRES_NEW로 설정하여 새로운 Transaction에서 처리한다.

->  DataBase Connection을 하나 더 사용하기 때문에 하나의 로직에 2개의 Connection을 사용하게 된다.

3. Checked Exception으로 예외를 던진다.

-> Checked Exception이 발생해도 Transaction은 Rollback 되지 않는다. 

4. 자식 Transaction에서 try-catch를 사용한다.

-> 가장 단순하면서 쉬운 방법이다.