Respository 계층 메소드들을 예외 처리하며 JPA가 던지는 에러를 어떻게 처리해야 할지 고민이었습니다.
JPA가 던지는 에러는 아래와 같이 트랜잭션을 기본적으로 롤백하는 언체크 에러도 있고, 트랜잭션을 롤백하지 않는 체크 에러도 있습니다.
물론 JPA가 던지는 대부분의 에러들은 RunTimeException을 상속하는 언체크 에러로 트랜잭션이 롤백됩니다.
하지만 아래와 같이 일부 경우에는 JPA가 체크 예외를 던지게 됩니다.
- SQLException: JPA의 내부 구현이 JDBC를 사용하므로, SQL 문이 잘못되었거나 데이터베이스에 문제가 발생했을 때 SQLException이 발생할 수 있습니다.
- EntityExistsException: JPA에서 새로운 엔티티를 저장하려 할 때, 동일한 ID를 가진 엔티티가 이미 존재하는 경우 발생합니다.
- EntityNotFoundException: 지정된 엔티티가 존재하지 않을 때 발생합니다. 주로 EntityManager.find() 메소드를 사용할 때 발생합니다.
- TransactionRequiredException: 트랜잭션이 필요하지만 현재 트랜잭션이 없는 경우 발생합니다. 예를 들어, 비트 연산이나 읽기 작업을 트랜잭션 없이 실행할 때 발생합니다.
- LockTimeoutException: JPA에서 데이터에 대한 잠금을 시도할 때, 지정된 시간 내에 잠금을 얻지 못한 경우 발생합니다.
- OptimisticLockException: 엔티티의 최적화 잠금이 실패했을 때 발생합니다. 이는 여러 사용자가 동시에 동일한 엔티티를 수정하려 할 때 발생할 수 있습니다.
그렇다면 이런 체크 예외들까지 트랜잭션 롤백되도록 만들려면 어떻게 할까요?
가장 쉬운 방법은 Service 계층에서 @Transcational(rollBackFor = {exception.Class})를 사용하는 것입니다. 이 방법으로 Repository에서 특정 이유로 SQLException과 같은 체크 예외가 던져지면 이를 받아 롤백하도록 만들 수 있습니다.
하지만 @Transcational(rollBackFor = {exception.Class})을 사용하는 방법보다 더 개선된 방법이 있습니다.
그것은 바로 PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 설정해 JPA가 던지는 예외를 추상화된 스프링 예외로 변환하는 것입니다. PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 등록하면 @Repository 애노테이션을 사용한 곳에 예외 변환 AOP를 적용해서 JPA 예외를 스프링 프레임워크가 추상화한 예외로 변환해 줍니다.
PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 등록한 JpaExceptionTranslationConfig 클래스 코드를 살펴봅시다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
@Configuration
public class JpaExceptionTranslationConfig {
@Bean
public PersistenceExceptionTranslationPostProcessor persistenceExceptionTranslationPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
}
}
이렇게 설정하면 JPA가 던지는 예외가 스프링 프레임워크 예외로 변환됩니다. 이때 변환되는 스프링 프레임워크 예외들은 모두
DataAccessException 예외 클래스를 상속받고 있습니다. DataAccessException는 언체크 에러로 RuntimeException을 상속받습니다. 상속 구조는 아래와 같습니다.
java.lang.Throwable
└── java.lang.Exception
└── java.lang.RuntimeException
└── org.springframework.dao.DataAccessException
이렇게 JPA가 던지는 예외를 스프링 프레임워크 예외로 변환하면 Service 계층에서 Repository의 구현 기술에 직접 의존하지 않게 됩니다. Service 계층에서 Repository 계층의 구현 기술에 직접 의존하는 것은 좋은 설계라고 할 수 없습니다.
기존에는 Service 계층에서 JPA 예외에 대응해야 하며 JPA에 의존했지만, JPA 예외를 스프링 프레임워크 예외로 추상화하며 불필요한 의존관계를 끊게 되었습니다.
JPA의 여러 예외들은 각각 변환되는 스프링 예외가 다르지만, 이 스프링 예외는 모두 DataAccessException을 상속하고 있습니다.
따라서 변환된 여러 스프링 예외를 아래 코드와 같이 DataAccessException으로 묶어서 처리할 수 있게 되었습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
// jpa repository error
@ExceptionHandler(DataAccessException.class)
private ResponseEntity<ErrorResponse> handleEntityPersistException(
Exception exception,
HttpServletRequest request
) {
return exceptionResponseEntity(exception.getMessage(), request.getRequestURI());
}
}
물론 상황에 따라서 이렇게 상위 타입 예외로 묶어 처리하는 방식이 자세한 예외를 표현하지 못할 수 있지만, 일단 할 수 있다는 사실을 보였습니다.
이제 PersistenceExceptionTranslationPostProcessor을 스프링 빈으로 등록하여 Repository에서 발생하는 JPA 예외를 모두 스프링 예외로 변환했습니다. 기존의 JPA에서 던지던 체크 예외 또한 스프링 예외로 변환되며 DataAccessException을 상속받게 됩니다.
따라서 맨 처음 살펴보았던 JPA의 체크 예외들도 언체크 예외로 변환되며, JPA가 던지는 모든 예외가 트랜잭션 롤백의 trigger가 됩니다.
아래 글에 제 궁금증이 이어서 나옵니다.
https://praaay.tistory.com/32
그럼 모든 JPA 에러는 롤백 처리해야 할까? (롤백 여부)
https://praaay.tistory.com/31 JPA가 던지는 check-error를 uncheck-error로 변환하면, 트랜잭션이 rollback 될까?Respository 계층 메소드들을 예외 처리하며 JPA가 던지는 에러를 어떻게 처리해야 할지 고민이었습니
praaay.tistory.com
'실시간 채팅 솔루션 개발 > 문제 해결 사례' 카테고리의 다른 글
Entity 삭제 시 발생하는 Referential integrity constraint violation 에러 (0) | 2024.10.25 |
---|---|
그럼 모든 JPA 에러는 롤백 처리해야 할까? (트랜잭션 롤백 여부) (0) | 2024.10.23 |
프록시의 PK만 조회하면, 1+N 쿼리 문제가 터지지 않는다? (1) | 2024.10.21 |
유연함을 위해 Custom Result 타입을 사용하자 (0) | 2024.09.28 |