JPA 예외처리와 엔티티 비교

JPA 표준 예외

JPA 표준 예외들은 javax.persistence.PersistenceException의 자식 클래스다. 이 예외 클래스들은 RuntimeException의 자식이므로 모두 비검사 예외다.

트랜잭션 롤백을 표시하는 예외

심각한 예외이므로 복구해서는 안 된다. 강제로 커밋해도 트랜잭션 커밋되지 않고 RollbackException 예외가 발생한다.

예외 설명
javax.persistence.EntityExistsException EntityManager.persist() 호출 시 이미 같은 엔티티가 있으면 발생
javax.persistence.EntityNotFoundException EntityManager.getReference() 호출 시 실제 엔티티가 존재하지 않으면 발생, refresh(), lock()에서도 발생
javax.persistence.OptimisticLockException 낙관적 락 충돌 시 발생
javax.persistence.PessimisticLockException 비관적 락 충돌 시 발생
javax.persistence.RollbackException EntityTransaction.commit() 실패 시 발생, 롤백이 표시되어 있는 트랜잭션 커밋 시에도 발생
javax.persistence.TransactionRequiredException 트랜잭션이 필요할 때 트랜잭션이 없으면 발생, 트랜잭션 없이 엔티티를 변경할 때 주로 발생

트랜잭션 롤백을 표시하지 않는 예외

심각한 예외가 아니므로 개발자가 트랜잭션을 커밋할지 롤백할지 판단한다.

예외 설명
javax.persistence.NoResultException Query.getSingleResult() 호출 시 결과가 하나도 없을 때 발생
javax.persistence.NonUniqueResultException Query.getSingleResult() 호출 시 결과가 둘 이상일 때 발생
javax.persistence.LockTimeoutException 비관적 락에서 시간 초과 시 발생
javax.persistence.QueryTimeoutException 쿼리 실행 시간 초과 시 발생

스프링 프레임워크의 JPA 예외 반환

서비스 계층에서 JPA 예외를 직접 사용하면 JPA에 의존하게 되는데 스프링 프레임워크는 이런 문제를 해결하고자 데이터 접근 계층에 대한 예외를 추상화해서 개발자에게 제공한다.

JPA 예외 스프링 변환 예외
javax.persistence.PersistenceException org.springframework.orm.jpa.JpaSystemException
javax.persistence.NoResultException org.springframework.dao.EmptyResultDataAccessException
javax.persistence.NonUniqueResultException org.springframework.dao.DataAccessException
javax.persistence.LockTimeoutException org.springframework.dao.CannotAcquireLockException
javax.persistence.QueryTimeoutException org.springframework.dao.QueryTimeoutException
javax.persistence.EntityExistsException org.springframework.dao.DataIntegrityViolationException
javax.persistence.EntityNotFoundException org.springframework.orm.jpa.JpaObjectRetrievalFailureException
javax.persistence.OptimisticLockException org.springframework.orm.jpa.JpaOptimisticLockingFailureException
javax.persistence.PessimisticLockException org.springframework.dao.PessimisticLockingFailureException
javax.persistence.TransactionRequiredException org.springframework.dao.InvalidDataAccessApiUsageException
javax.persistence.RollbackException org.springframework.transaction.TransactionSystemException
java.lang.IllegalStatieException org.springframework.dao.InvalidDataAccessApiUsageException
java.lang.IllegalArgumentException org.springframework.dao.InvalidDataAccessApiUsageException

스프링 프레임워크에 JPA 예외 변환기 적용

JPA 예외를 스프링 프레임워크가 제공하는 추상화된 예외로 변경하려면 PersistenceExceptionTranslationPostProcessor 를 스프링 빈으로 등록해야 한다. 그러면 @Repository 애너테이션을 사용하는 곳에 예외 변환 AOP를 적용해서 JPA 예외를 스프링 프레임워크가 추상화한 예외로 변환해준다.

@Bean
public PersistenceExceptionTransactionPostProcessor exceptionTranslation() {
		return new PersistenceExceptionTranslationPostProcessor();
}
@Repository
public class NoResultExceptionTestRepository {
    @PersistenceContext EntityManager em;

    public Member findMember() {
        return em.createQuery("select m from Member m", Member.class)
                  .getSingleResult();
    }
}

findMember()의 결과가 없을 때 javax.persistence.NoResultException 이 발생한다. 예외가 findMember() 메서드를 빠져 나갈 때 등록한 AOP가 동작하여 org.springframework.dao.EmptyResultDataAccessException 예외로 변환해서 반환한다. 예외를 변환하지 않고 그대로 반환하고 싶은 경우 throws 절에 반환할 JPA 예외를 직접 명시하면 된다.

트랜잭션 롤백 시 주의사항

트랜잭션을 롤백하는 것은 DB의 반영사항만 롤백하는 것이지 객체까지 복구하지는 못 한다. 예를 들어 엔티티를 조회해서 수정하는 중에 문제가 있어서 트랜잭션을 롤백하면 DB의 데이터는 복구되지만 객체는 수정된 상태로 영속성 컨텍스트에 남아 있다. 따라서 새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear() 를 호출해서 영속성 컨텍스트를 초기화한 다음에 사용해야 한다. 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하다.

기본 전략인 트랜잭션 당 영속성 컨텍스트 전략은 문제가 발생하면 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하므로 문제가 발생하지 않는다. 문제는 OSIV 처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 발생한다. 이 때 스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정하면 트랜잭션 롤백 시 영속성 컨텍스트를 초기화해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.

엔티티 비교

  • 영속성 컨텍스트 내부에는 인스턴스 보관소인 1차 캐시가 존재한다. 1차 캐시는 영속성 컨텍스트와 생명주기가 같다.
  • 1차 캐시의 가장 큰 장점은 애플리케이션 수준의 반복 가능한 읽기가 가능하다는 점이다. 같은 영속성 컨텍스트에서 엔티티를 조회하면 항상 같은 엔티티 인스턴스를 반환한다.

영속성 컨텍스트가 같을 때

같은 트랜잭션 범위에 있으면 같은 영속성 컨텍스트를 사용한다. 영속성 컨텍스트가 같으면 엔티티를 비교할 때 3가지 조건을 모두 만족해야 한다.

  • 동일성 : == 비교가 같다.
  • 동등성 : equals() 비교가 같다.
  • 데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다.

영속성 컨텍스트가 다를 때

트랜잭션이 달라 서로 다른 영속성 컨텍스트 간의 엔티티를 비교할 때가 있을 수 있다. 이 때는 equals() 를 이용하여 엔티티의 비즈니스 키를 활용한 동등성 비교를 사용하는 것이 권장된다. 예를 들어, 주민번호가 있다면 좋은 비즈니스 키 대상이 될 수 있다. 또는 회원 엔티티에 이름과 연락처가 같은 회원이 없다면, 이를 조합하여 비즈니스 키로 활용할 수 있다.

Categories:

Updated:

Comments