[JPA] Service Layer 에서 DataIntegrityViolationException 처리
유니크 컬럼에 중복값을 넣으려고 할 때, DataIntegrityViolationException이 발생해 db호출이 일어나는 메소드에서 Exception을 잡아 IllegalArgumentException으로 감싸 넘기려고 했는데 캐치 되지 않고 해당 에러가 리턴되었다.
@Transactional
public void addMemberPhone(MemberPhoneRequest memberPhone) {
MemberPhone memberPhone = new MemberPhone();
...
try{
memberPhoneRepo.save(memberPhone);
}catch (DataIntegrityViolationException e){
throw new IllegalArgumentException("이미 존재하는 전화번호 입니다.");
}
}
error
Exception이 캐치되지 않는 이유는 JPA의 영속성 컨텍스트와 관련이 있습니다.
- @Transactional이 붙은 메소드에서 Repository.save() or saveAll()이 호출될 때 쿼리를 실행하지 않고 쓰기 지연 상태가 되어 1차 캐시에만 쿼리를 보관 하고 있습니다.
- 메소드가 정상적으로 종료되었다면 실제 쿼리를 디비에 실행해 DB에 반영하게 됩니다.
- try-catch 블럭 내부에서 쿼리가 실행되지 않아 DataIntegrityViolationException이 발생되지 않습니다.
- 메소드가 종료된 후에 쿼리가 실행되면서 DataIntegrityViolationException이 발생합니다.
쓰기 지연 (transactional write-behind)
- 영속성 컨텍스트에 변경이 발생했을 때, 바로 DB로 쿼리를 보내지 않고 쿼리를 1차 캐시에 보관하고 있다가, 영속성 컨텍스트가 flush 하는 시점에 쿼리를 DB로 보냄.
해결방법
- saveAndFlush() 메소드 사용
- @ExceptionHandler 예외 캐치
- Controller 에 작성
- 프로젝트 전역 설정 필요한 경우 @ControllerAdvice
첫번째 방법
saveAndFlush() 메소드를 호출해 1차캐시에 쿼리를 보관하지 않고 DB에 바로 실행하도록 해 해당 메소드 내에서 try-catch 처리 가능
두,세번째 방법
작성 내용은 동일하되, 에러 캐치 범위에 대한 차이가 있습니다.
- Controller.class에 작성 하는 경우 클래스 내에서 발생한 DataIntegrityViolationException 만 캐치
- 프로젝트 전역에서 DataIntegrityViolationException이 발생한 경우 캐치
@ExceptionHandler(value = {DataIntegrityViolationException.class})
public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error(e.getMessage(), e);
return ResponseEntity.badRequest().body(new IllegalArgumentException("중복된 데이터가 존재합니다."));
}
Service layer 단위 테스트 해당 이슈를 확인할 수 없음, Controller layer 에서 확인 필요함.
+)
@Transactional를 사용하지 않는 경우, 쿼리 실행 메소드마다 flush - commit 동작됨 트랜잭션기능을 사용할 수 없음.
사용하면 안되는 방법임.
참고 :
https://www.baeldung.com/spring-data-jpa-save-saveandflush
https://www.baeldung.com/spring-dataIntegrityviolationexception