JPA Exception #1

또리·2022년 7월 21일
0

JPA

목록 보기
1/2

에러

얼마전 회사에서 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가지였다.

  1. try 에서 INSERT 에러가 발생해 catch 로 빠졌는데 catch에서 INSERT를 재실행하였다.
  2. Exception을 try~catch를 잡아 정상 return 해줬음에도 비지니스 로직이 끝난 후 @ExceptionHandler에 의해 다시 한번 exception 처리가 된다는 것이었다.
    @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문이 실행되었다.

동작 방식

결과적으로 동작했던 방식은 아래와 같다.

  1. (1) 에서 persist를 통해 신규로 만든 Entity 객체를 영속성 컨텍스트에 저장한다.
  2. flush를 통해 쓰기 지연 저장소에 쌓여있던 SQL문을 DB에 반영하는 순간 Duplication 에러가 발생한다.
  3. catch 문으로 들어와 merge를 통해 PK로 조회해온 영속성 객체를 영속성 컨텍스트에 저장한 후 변경 사항을 Copy한다.
  4. flush를 실행 하는 순간 2번 과정에서 flush 중 실패한 INSERT 문이 clear 되지 않고 재실행되었다.
  5. 비지니스 로직이 종료된 후 Transaction Rollback Mark로 인해 한번 더 Exception이 발생해 @ExceptionHandler로 빠지게 되었다.
  6. 결국 정상 Output이 아닌 에러 Output을 내보내게 되었다.

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를 실행하는 것이 맞다고 판단하여 변경하기로 했다.

profile
공인중개사를 공부하는 금융 개발자

0개의 댓글