
@Transactional을 사용하면 직접 트랜잭션 시작, 커밋, 롤백을 관리하지 않아도 되니까 매우 편리하다.
하지만 모르고 쓰다보면 원하는대로 동작을 안할 수도 있다.
예를 들어, 예외가 발생하여 롤백이 되어야 하는데 롤백이 안되면 어떻게 될까?
@Transactional이 적용된 메서드가 호출되면, 트랜잭션이 시작된다.
메서드가 정상적으로 종료되면 트랜잭션이 커밋되고, 예외가 발생하면 트랜잭션이 롤백된다.
이때 예외의 종류가 중요한데 기본 설정으로는 RuntimeException 및 그 하위 예외에서만 트랜잭션 롤백이 이루어진다.
Checked Exception의 경우 명시적으로 롤백 설정이 필요하다.
그럼 RuntimeException 등 Unchecked Exception이 발생하기만 하면 항상 롤백이 이루어질까?
Unchecked Exception이 발생해도 try-catch문으로 예외를 확인하고 처리했다면 롤백이 일어나지 않는다.
정말 그러는지 확인하기 위해 테스트를 진행했다.
이전 글에서 다루었던 내부 호출 문제와 함께 다양한 케이스를 확인해보고 싶어 조금 복잡해졌다.
@Transactional 여부예외 종류(Checked vs Unchecked)예외가 발생한 위치(inner vs outer)예외를 잡는 위치(inner vs outer vs etc)모두 @Transactional이 적용내부 메서드에서 Unchecked Exception이 발생예외를 외부 메서드로 전파모든 작업이 롤백되어 아무것도 저장되지 않음@Transactional
public void outerThrowUnchecked() {
Code outerEntity = new Code();
outerEntity.setCode("OUTER_TX_THROW_UNCHECK");
outerEntity.setEtc("OUTER_TX_THROW_UNCHECK");
testRepository.save(outerEntity);
innerTxThrowUnchecked();
}
@Transactional
public void innerTxThrowUnchecked() {
Code innerEntity = new Code();
innerEntity.setCode("INNER_TX_THROW_UNCHECK");
innerEntity.setEtc("INNER_TX_THROW_UNCHECK");
testRepository.save(innerEntity);
throw new RuntimeException("Unchecked Exception in Inner Transactional Method");
}
@Test
void testOuterThrowUncheckedException() {
// given // when
// RuntimeException 이 발생할 것으로 기대하고 메서드 실행
RuntimeException thrownException = assertThrows(RuntimeException.class, () -> testService1.outerThrowUnchecked());
// then
assertThat(thrownException.getMessage()).isEqualTo("Unchecked Exception in Inner Transactional Method");
// outerEntity 가 DB에 저장되지 않았는지 확인 (롤백 확인)
Optional<Code> outerEntity = testRepository.findByCode("OUTER_TX_THROW_UNCHECK");
assertThat(outerEntity).isNotPresent();
// innerEntity 가 DB에 저장되지 않았는지 확인 (롤백 확인)
Optional<Code> innerEntity = testRepository.findByCode("INNER_TX_THROW_UNCHECK");
assertThat(innerEntity).isNotPresent();
// 데이터베이스에 저장된 총 엔티티 수가 0인지 확인 (모두 롤백되었음을 확인)
long count = testRepository.count();
assertThat(count).isZero();
}
모두 @Transactional이 적용내부 메서드에서 Checked Exception이 발생예외를 외부 메서드로 전파Checked Exception은 롤백되지 않음@Transactional
public void outerThrowChecked() throws IOException {
Code outerEntity = new Code();
outerEntity.setCode("OUTER_TX_THROW_CHECK");
outerEntity.setEtc("OUTER_TX_THROW_CHECK");
testRepository.save(outerEntity);
innerTxThrowChecked();
}
@Transactional
public void innerTxThrowChecked() throws IOException {
Code innerEntity = new Code();
innerEntity.setCode("INNER_TX_THROW_CHECK");
innerEntity.setEtc("INNER_TX_THROW_CHECK");
testRepository.save(innerEntity);
throw new IOException("Checked Exception in Inner Transactional Method");
}
@Test
void testOuterThrowCheckedException() {
// given // when
// RuntimeException 이 발생할 것으로 기대하고 메서드 실행
IOException thrownException = assertThrows(IOException.class, () -> testService2.outerThrowChecked());
// then
assertThat(thrownException.getMessage()).isEqualTo("Checked Exception in Inner Transactional Method");
// outerEntity 가 DB에 저장되지 않았는지 확인 (커밋 확인)
Optional<Code> outerEntity = testRepository.findByCode("OUTER_TX_THROW_CHECK");
assertThat(outerEntity).isPresent();
// innerEntity 가 DB에 저장되지 않았는지 확인 (커밋 확인)
Optional<Code> innerEntity = testRepository.findByCode("INNER_TX_THROW_CHECK");
assertThat(innerEntity).isPresent();
// 데이터베이스에 저장된 총 엔티티 수가 2인지 확인 (모두 커밋되었음을 확인)
long count = testRepository.count();
assertThat(count).isEqualTo(2);
}
모두 @Transactional이 적용내부 메서드에서 Unchecked Exception이 발생예외를 외부 메서드에서 캐치예외가 캐치되었기 때문에 롤백되지 않음@Transactional
public void outerCatchUnchecked() {
try {
Code outerEntity = new Code();
outerEntity.setCode("OUTER_NON_TX_CATCH_UNCHECK");
outerEntity.setEtc("OUTER_NON_TX_CATCH_UNCHECK");
testRepository.save(outerEntity);
innerTxThrowUnchecked();
} catch (RuntimeException e) {
// 예외 캐치
}
}
@Transactional
public void innerTxThrowUnchecked() {
Code innerEntity = new Code();
innerEntity.setCode("INNER_TX_THROW_UNCHECK");
innerEntity.setEtc("INNER_TX_THROW_UNCHECK");
testRepository.save(innerEntity);
throw new RuntimeException("Unchecked Exception in Inner Transactional Method");
}
@Test
void testOuterCatchUnchecked() {
// given // when
testService3.outerCatchUnchecked();
// then
// outerEntity 가 DB에 저장되었는지 확인(커밋 확인)
Optional<Code> outerEntity = testRepository.findByCode("OUTER_NON_TX_CATCH_UNCHECK");
assertThat(outerEntity).isPresent();
// innerEntity 가 DB에 저장되지 않았는지 확인(커밋 확인)
Optional<Code> innerEntity = testRepository.findByCode("INNER_TX_THROW_UNCHECK");
assertThat(innerEntity).isPresent();
// 데이터베이스에 저장된 총 엔티티 수가 2인지 확인 (모두 커밋되었음을 확인)
long count = testRepository.count();
assertThat(count).isEqualTo(2);
}
모두 @Transactional이 적용내부 메서드에서 Checked Exception이 발생예외를 외부 메서드에서 캐치예외가 캐치되었기 때문에 롤백되지 않음@Transactional
public void outerCatchChecked() {
try {
Code outerEntity = new Code();
outerEntity.setCode("OUTER_NON_TX_CATCH_CHECK");
outerEntity.setEtc("OUTER_NON_TX_CATCH_CHECK");
testRepository.save(outerEntity);
innerTxThrowChecked();
} catch (IOException e) {
// 예외 캐치
}
}
@Transactional
public void innerTxThrowChecked() throws IOException {
Code innerEntity = new Code();
innerEntity.setCode("INNER_TX_THROW_CHECK");
innerEntity.setEtc("INNER_TX_THROW_CHECK");
testRepository.save(innerEntity);
throw new IOException("Checked Exception in Inner Transactional Method");
}
@Test
void testOuterCatchUnchecked() {
// given // when
testService4.outerCatchChecked();
// then
// outerEntity 가 DB에 저장되었는지 확인(커밋 확인)
Optional<Code> outerEntity = testRepository.findByCode("OUTER_NON_TX_CATCH_CHECK");
assertThat(outerEntity).isPresent();
// innerEntity 가 DB에 저장되지 않았는지 확인(커밋 확인)
Optional<Code> innerEntity = testRepository.findByCode("INNER_TX_THROW_CHECK");
assertThat(innerEntity).isPresent();
// 데이터베이스에 저장된 총 엔티티 수가 2인지 확인 (모두 커밋되었음을 확인)
long count = testRepository.count();
assertThat(count).isEqualTo(2);
}
살펴본 4가지 외에도 약 60여개의 경우의 수가 존재한다.
모두 살펴보면 너무 길어지기도 하고 이 정도로도 충분히 결과를 예상가능할 것이라고 생각한다.
생략된 코드는 transactional-test 에서 확인할 수 있다.
외부 또는 내부 메서드에서 언체크 예외가 발생하고 그것을 처리하지 않았을 때, 트랜잭션이 롤백되어 데이터베이스에 엔티티가 저장되지 않습니다.
그외의 상황에서는 모두 커밋이 이루어집니다.
내부호출은 되도록 사용하지 않아야 하며 필요하다면 메서드를 다른 클래스로 분리해야 한다.
DB 작업 메서드에서 예외가 발생했을 때 롤백이 이루어지기를 의도한다면 메서드 내에서 잡지 않고 밖으로 던져야 한다.
Exception과 같은 Checked Exception은 롤백에 영향을 주지 않기 때문에, 예외는 RuntimeException과 같은 Unchecked Exception을 던져야 하며, 이는 RuntimeException을 상속받은 CustomException을 사용하면 편리할 것이다.
트랜잭션(Transaction)의 예외(Exception)에 따른 롤백 처리
@Transactional 쓸때 try catch를 안쓰면 안되나요?
응? 이게 왜 롤백되는거지?