@Transactional이 있는 메소드에 내가 사용자 지정 예외가 터졌는데, 예상했던 것과 달리 롤백이 되지 않아서 당황했던 적이 있다. 오늘은 트랜잭션을 지정한 메소드에 에러가 발생했는데 롤백이 되지않은 이유에 대해 알아보겠다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
@Service
@Log4j2
@RequiredArgsConstructor
public class MemberService{
private final MemberRepository memberRepository;
@Transactional
public void exceptionMethod() {
memberRepository.save(new Member());
try {
throw new Exception();
}catch (Exception e){
log.info("에러 발생");
}
}
}
위 코드를 실행했을 때 롤백이 발생할까? 위에서 알려줬듯 정답은 '발생하지 않는다'이다.
@Service
@Log4j2
@RequiredArgsConstructor
public class MemberService{
private final MemberRepository memberRepository;
@Transactional
public void exceptionMethod() {
memberRepository.save(new Member());
try {
throw new RuntimeException();
}catch (RuntimeException e){
log.info("에러 발생");
}
}
}
그렇다면 위 코드는 어떨까? RuntimeException은 UncheckedException에 속하니 롤백이 발생해야할 것 같지만, 막상 실행 해보면 롤백되지 않는다. 이유는 try-catch문안에 있어서이다. RuntimeException이 발생했지만 try-catch문으로 예외를 확인하고 처리를 했기 때문이다.
@Service
@RequiredArgsConstructor
public class MemberService{
private final MemberRepository memberRepository;
@Transactional
public void exceptionMethod() {
memberRepository.save(new Member());
throw new RuntimeException();
}
}
try-catch문이 없어지고 나서야 이제야 우리가 예상했던 트랜잭션 롤백이 실행된다. 좀 더 자세히 말하자면 예외가 발생했을 때 rollback-only 마킹을 하고, 최종 커밋을 할때 rollback-only가 마킹되어 있는것을 확인하고 롤백을 한다.
CheckedException도 롤백하려면 여러가지 방법이 있다.
@Transactional(rollbackOn = {Exception.class})
방법 1은 롤백하고 싶은 CheckedException을 명시해주는 것이다. @Transactional의 옵션 rollbackOn의 default는 Runtime과 Error만 되어있다. 이 값을 원하는 예외로 바꿔주기만 하면된다.
...
try{
...
} catch(Exception e){
throw new ExampleException(e.getMessage());
}
방법 2는 사용자 정의 에러를 발생시키는 것이다. 당연히 사용자 정의 에러는 RuntimeException을 상속 받아야 한다.
아마 스프링은 UncheckedException은 말 그대로 개발자가 예상치 못한 에러라 판단해서 롤백하게 하는것 같다. 반면 CheckedException은 예상한 에러이니 굳이 롤백할 이유가 없다고 판단해서이고...
트랜잭션에서 RuntimeException에 대해 자세하게 쓰인 우아한기술블로그를 보는 것도 추천한다.