http://www.yes24.com/Product/Goods/19040233?Acode=101

 

자바 ORM 표준 JPA 프로그래밍 - YES24

자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA 기초 이론과 핵심 원리, 그리고

www.yes24.com

이 글은 "자바 ORM 표준 JPA 프로그래밍" 책을 읽고 정리한 내용임을 밝힙니다.

자세한 내용을 알고 싶다면 책을 읽는 것을 강력하게 추천 드립니다.

단, 여기 적는 내용은 제가 공부한 내용과, 실험한 내용을 모두 포함 합니다.

 

 

JPA 표준 예외 정리

Exception 특징

jpa 표준 예외는 모두 javax.persistence.PersistenceException 의 자식 클래스

그리고 PersistenceException 은 RuntimeException 의 자식 클래스

 

따라서 JPA 는 모두 Unchecked 예외

JPA Exception 종류

크게 2가지로 나눔

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

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

이 예외는 매우 심각한 예외. 따라서 예외가 발생했을 때, 트랜젝션의 내용을 절대 복구해선 안된다.

예외 설명
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() 호출 했는데 결과가 2 이상일 때 반환
javax.persistence.LockTimeoutException 비관적 락에서 시간 초과 시 발생
javax.persistence.QueryTimeoutException 쿼리 실행 시간 초과 시 발생

 

 

JPA 표준 예외, 스프링 예외로 변환

서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존하는 것은 좋은 설계라고 할 수 없다.

예외도 마찬가지.

예를 들어서 서비스 계층에서 JPA 예외를 직접 사용하면 (처리하면) JPA 에 의존하게 됨

스프링 프레임워크는 이런 문제를 해결하기 위해서 데이터 접근 계층에 대한 예외를 추상화해서 개발자에게 제공

JPA 에서 발생할 수 있는 Java 기본 예외도 변환해줌

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

JPA 예외를 스프링 프레임쿼트 예외로 변환하려면 PersistenceExceptionTranslationPostProcessor 을 빈으로 등록하면 된다.

스프링 xml 설정

<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

JavaConfig 설정

@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
}

이렇게 하면 @Repository 어노테이션을 사용한 곳에 예외 변환 AOP 기술을 적용해서 JPA 예외를 Spring 예외로 변환해준다.

변환 예제 코드

@Repository // 중요
public class ExceptionRepository {

    @PersistenceContext
    private EntityManager em;

    public MemberEntity findMember() {
        return em.createQuery("select m from MemberEntity m", MemberEntity.class).getSingleResult();
    }
}
@SpringBootTest
class ExceptionRepositoryTest {

    @Autowired
    private ExceptionRepository exceptionRepository;

    @Test
    @DisplayName("예외 변환기 테스트")
    public void exceptionTest() {
        exceptionRepository.findMember();
    }
}

예외가 변환되어 나오는 모습을 볼 수 있다

.

이는 findMember 예외를 빠져나갈 때, 인터셉터가 동작해서 이 예외를 EmptyResultDataAccessException 으로 변환하기 때문이다.

현재

스프링 부트 2.5.x 버전을 사용하고 있는데 자동으로 PersistenceExceptionTranslationPostProcessor 빈이 등록 되어 있는 것 같다.

만약 의도적으로 PersistenceExceptionTranslationPostProcessor 를 상속 받아서 내용물인 메소드를 변경하면 예외가 변환되지 않는다.

PersistenceExceptionTranslationPostProcessor 상속

public class CustomPersistenceExceptionTranslationPostProcessor extends PersistenceExceptionTranslationPostProcessor {
    public CustomPersistenceExceptionTranslationPostProcessor() {
    }

    @Override
    public void setRepositoryAnnotationType(Class<? extends Annotation> repositoryAnnotationType) {
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
    }
}

메소드 내용물을 비웠다. ⇒ 고의적으로 PersistenceExceptionTranslationPostProcessor 역할을 못하게 만듬

빈으로 등록

@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
    return new CustomPersistenceExceptionTranslationPostProcessor();
}

테스트 결과

예외가 변환되지 않고 그대로 노출 되고 있다.

 

만약 예외를 그대로 받고 싶다면

위에서 작성한 ExceptionRepository 의 findMember 메소드에서 예외를 변환하지 않고 그대로 반환받고 싶다면 thows 절에 그대로 반환할 JPA 예외 or JPA 예외의 부모 클래스를 직접 명시하면 됨

import javax.persistence.NoResultException;

@Repository
public class ExceptionRepository {

    @PersistenceContext
    private EntityManager em;

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

 

트랜젝션 롤백 시 주의 사항

트랜젝션을 롤백하는 것은 DB 반영사항만 롤백하는 것이고 수정한 자바 객체까지 원상태로 복구해주지는 않는다.

다음 코드를 보자.

 

@SpringBootTest
class ExceptionRepositoryTest {
    @PersistenceUnit
    private EntityManagerFactory emf;

    @Test
    @DisplayName("롤백 테스트")
    public void rollbackTest() {

        // EntityManagerFactory 에서 EntityManager 를 얻어 온다.
        EntityManager em = emf.createEntityManager();

        // 트랜젝션을 하나 열었음
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        // 멤버 생성
        MemberEntity member = new MemberEntity();
        member.setName("a");

        // persist 후 변경 => update member_entity set name='b' where id = ?
        em.persist(member);
        member.setName("b");

				// flush 를 호출해서 강제로 SQL 을 날립니다.
        em.flush();

        System.out.println("updated member name is '" + member.getName() + "'");
				// rollback!!
        transaction.rollback();

        System.out.println("member name after rollback is '" + member.getName() + "'");
    }
}
2021... : insert into member_entity (name, id) values (?, ?)
2021... : update member_entity set name=? where id=?
updated member name is 'b'
member name after rollback is 'b'

insert 와 update 쿼리가 나갔지만 rollback이 일어났다. 하지만 member 객체의 이름은 여전히 b 이다.

따라서 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하다.

새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear 를 호출해서 영속성 컨텍스트를 초기화 한 다음에 사용해야한다.

 

스프링 프레임워크는 이런 문제를 예방하기 위해서 영속성 컨텍스트의 범위에 따라 다른 예방책을 사용하고 있다.

먼저, 기본전략인 "트랜젝션당 영속성 컨텍스트" 전략은 문제가 발생하면 트랜젝션 AOP 종료 시점에 트랜젝션을 롤백하면서 영속성 컨텍스트도 함께 종료하기 때문에 문제가 되지 않는다.

 

OSIV 처럼 영속성 컨텍스트의 범위가 트랜잭션 범위보다 넓게 살려두는 경우 문제가 될 수 있는데 이 설정을 사용할 때 rollback 이 일어나면 스프링은 영속성 컨텍스트의 EntityManager.clear() 를 초출해서 이 문제를 예방한다.

org.springframework.orm.jpa.JpaTransactionManager 의 doRollback 메소드를 보면 다음과 같은 코드가 있다.

 

finally {
	if (!txObject.isNewEntityManagerHolder()) {
		// Clear all pending inserts/updates/deletes in the EntityManager.
		// Necessary for pre-bound EntityManagers, to avoid inconsistent state.
		txObject.getEntityManagerHolder().getEntityManager().clear();
	}
}

롤백을 할 때 영속성 컨텍스트를 clear 하기 때문에 문제 되지 않는다.