얼마전 회사에서 JPA를 테스트하던 중 아래와 같은 에러가 발생했다.
jpa transaction silently rolled back because it has been marked as rollback-only
기존 소스는 try 에서 INSERT 에러가 나더라도 catch 에서 UPDATE 를 처리함으로 정상 처리를 요하는 로직이었다. JPA를 이렇게 코딩하면 안된다는 것은 알지만 기존 소스가 이렇게 되어있어서 건드릴수 없는 부분이었다.
대략적인 소스는 아래와 같다. 샘플로 작성된 소스이므로 이해를 부탁드립니다(...)
Car car = new Car();
car.setName(1); //PK
car.setColor("Red");
Output output = new Output();
try {
entityManager.persist(car); //flush 실행 --- (1)
output.setResult("Insert");
} catch(Exception e) {
entityManager.merge(car); //flush 실행 --- (2)
output.setResult("Update");
}
return output;
작성되어있던 코드는 try 안에서 persist를 실행하고 영속성 컨텍스트(Persistence Context)를 명시적 flush를 해줌으로서 DB에 insert문을 보낸다.
여기서 Duplication 에러가 발생한다면 catch 문에서 merge를 통해 update를 처리하겠다는게 이 소스의 로직이었다.
여기서의 문제점은 2가지였다.
@ExceptionHandler(value = {Exception.class})
public ErrorResponse exception(Exception ex, HttpServletRequest request) {
log.error(ex);
}
로그를 확인해보니 2건의 Oracle Duplication 에러와 jpa transaction silently rolled back because it has been marked as rollback-only 에러가 발생했다.
먼저 INSERT 문이 2건이 실행되었다는 점을 찾아봤다.
내 짧은 JPA 지식으로는 (1) 에서 INSERT 문이 실행되고 (2) 에서 UPDATE 문이 실행되어야 한다고 생각했다.
그러나 결과는 (1) 에서 INSERT, (2) 에서 INSERT 총 2번의 INSERT문이 실행되었다.
결과적으로 동작했던 방식은 아래와 같다.
SQL 에러가 발생해도 해당 SQL 문이 쓰기 지연 저장소에 clear 되지 않고 계속 쌓여있다가 다시 flush를 호출하던 순간 SQL이 재전송된다는 사실을 간과했던 것이다. 애초에 UPDATE 문은 실행되지도 않았다.
또한 try~catch 로 SQL Exception을 정상 처리하더라도 JPA는 Exception이 발생하는 순간
Transaction에서 Rollback Mark를 하게 되고 비지니스 로직이 정상 처리 되더라도
최종 Transaction의 Commit 시점에 jpa transaction silently rolled back because it has been marked as rollback-only 에러가 발생하는 것이다.
JPA는 UnCheckException을 예상치 못한 예외라 롤백시키는게 Default 전략이다.
명시적으로 Exception을 잡는 순간이 아니면 Exception이 발생한 순간 비지니스 로직 전체가 Rollback이 되어야 하므로 Transaction의 전파 속성을 REQUIRED_NEW로 설정하진 못했지만,
만약 에러가 발생하더라도 정상 Commit을 하고 싶다면 새로운 Transaction을 생성하는 전략으로 실행해야 할 것 같다.
결과는 try~catch로 잡는 로직을 기존의 JPA 원칙대로 findById로 객체의 존재 여부를 확인한 후 Insert or Update를 실행하는 것이 맞다고 판단하여 변경하기로 했다.