beadss / jpa-study

jpa슽터디입니다
1 stars 2 forks source link

15장 정리 중....... #42

Open joont92 opened 5 years ago

joont92 commented 5 years ago

JPA 예외

JPA의 표준 예외들은 javax.persistence.PersistenceException의 자식 클래스이다.
그리고 이 클래스는 RuntimeException의 자식이다. 즉, JPA의 예외는 모두 언체크 예외이다.

JPA 표준 예외

JPA 표준 예외는 크게 아래의 2가지로 나뉜다.
난 실행단계에서 아래 2개를 어떻게 구분해야할지 잘 모르겠다

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

심각한 예외이므로 복구해선 안되는 예외들이다.
이 예외가 발생하면 트랜잭션을 강제로 커밋해도 커밋되지 안혹 javax.persistence.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.QueryTimeException 쿼리 실행 시간 초과시 발생

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

서비스 계층에서 데이터 접근 기술에 직접 의존하는 것은 좋은 설계가 아니다.
이것은 예외도 마찬가지다. 서비스 계층에서 위 JPA 예외에 직접 의존하면 결국 JPA에 의존하게 되는것이다.
스프링 프레임워크는 이런 문제를 해결하기 위해 JPA 예외를 추상화해서 제공한다.

blah blah..

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

위처럼 JPA예외를 스프링 예외로 변경해서 받으려면 PersistenceExceptionTranslationPostProcessor를 스프링 빈으로 등록하면 된다.
이것은 @Repository 어노테이션을 사용한곳에 예외 변환 AOP를 적용해서 JPA 예외를 스프링 추상화 예외로 변환해준다.

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

아래 예외는 변환되어서 던져질 것이다.

@Repository
public class NoResultExceptionTestRepository{
    @PersistenceContext
    private EntityManager em;

    public Member findMember(){
        // 조회결과 없음
        return em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
            .setParameter("id", 999)
            .getSingleResult();
    }
}

원래라면 NoResultException이 발생해야 하지만, 등록한 AOP 인터셉터가 동작해서 EmptyResultDataAccessException으로 변환해서 반환한다.

만약 예외를 변환하지 않고 그대로 반환하고 싶다면 반환할 JPA 예외나 JPA 예외의 부모 클래스를 직접 명시하면 된다.

@Repository
public class NoResultExceptionTestRepository{
    public Member findMember() throws javax.persistence.NoResultException{
        return em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
            .setParameter("id", 999)
            .getSingleResult();
    }
}

예외의 최상위 클래스인 Exception을 throws 하면 예외를 아예 변환하지 않을 것이다.

트랜잭션 롤백 시 주의사항

트랜잭션을 롤백하는 것은 데이터베이스의 반영사항만 롤백하는 것이지, 수정한 자바 객체까지 원상태로 복구해주지는 않는다.
이 말인 즉 트랜잭션이 롤백되었다고 영속성 컨텍스트도 롤백되는 것은 아니라는 것을 의미하는데,
기본 전략인 트랜잭션당 영속성 컨텍스트


성능 최적화

N+1 문제

성능상 가장 주의해야 하는것이 이 N+1 문제이다.
아래와 같은 엔티티가 있다고 가정한다.

@Entity
class Member{
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders = new ArrayList<>();
}

@Entity
class Order{
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Member member;
}

(참고로 Order에서 Member를 EAGER로 설정해도 동일하게 N+1은 발생한다)

즉시 로딩

List<Member> members = 
    em.createQuery("SELECT m FROM Member", Member.class)
    .getResultList();

예전에도 언급했지만, JPA는 fetchType을 전혀 신경쓰지 않고 충실하게 JPQL에 맞춰 SQL을 생성한다.

따라서 Member를 전체 조회하는 쿼리가 먼저 실행되고,
Member의 개수만큼 Order를 조회하게 될 것이다(...)

SELECT * FROM Member; -- if result is 5
SELECT * FROM Order_ WHERE MEMBER_ID = 1;
SELECT * FROM Order_ WHERE MEMBER_ID = 2;
SELECT * FROM Order_ WHERE MEMBER_ID = 3;
SELECT * FROM Order_ WHERE MEMBER_ID = 4;
SELECT * FROM Order_ WHERE MEMBER_ID = 5;

이처럼 처음 실행한 SQL의 결과 수만큼 추가로 SQL을 실행하는 것을 N+1 문제라고 한다.

지연 로딩

즉시로딩을 지연로딩으로 바꿔도 N+1에서 자유로울수는 없다.
즉시로딩이 아니라서 Member 조회와 동시에 Member 건수만큼 Order를 조회해오진 않겠지만,
Member에서 Order를 사용하는 시점에는 똑같이 불러오게 된다.

for(Member member : membres){
    System.out.println(member.getOrders().size());
}

members 개수만큼 order 조회 SQL이 실행될 것이다.
즉, 이것도 결국 N+1 문제다.

해결법

페치 조인

가장 일반적인 방법이다. SQL 조인을 이용해서 연관된 엔티티를 함께 조회하므로 N+1 문제가 발생하지 않는다.
JPQL은 아래와 같다.

SELECT m FROM Member m JOIN FETCH m.orders

JOIN으로 같이 조회해서 Member 엔티티의 orders 속성에 초기화 하였기 때문에 더이상 N+1이 발생하지 않는다.
참고로 위 예제는 일대다 조인이므로 결과가 늘어날 수 있다. DISTINCT를 써줘야한다.

하이버네이트 @BatchSize

하이버네이트가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 이용하면 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.

@Entity
class Member{
    @Id @GeneratedValue
    private Long id;

    @org.hibernate.annotations.BatchSize(size = 5)
    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders = new ArrayList<>();
}

즉시로딩이므로 Member를 조회하는 시점에 Order를 같이 조회한다.
@BatchSize가 있으므로 Member의 건수만큼 추가 SQL을 날리지 않고, 조회한 Member 의 id들을 모아서 SQL IN 절을 날린다.

SELECT * FROM
ORDER_ WHERE MEMBER_ID IN(
    ?, ?, ?, ?, ?
)

size는 IN절에 올수있는 최대 인자 개수를 말한다. 만약 Member의 개수가 10개라면 위의 IN절이 2번 실행될것이다.

그리고 만약 지연로딩이라면 지연로딩된 엔티티 최초 사용시점에 5건을 미리 로딩해두고, 6번째 엔티티 사용 시점에 다음 SQL을 추가로 실행한다.

hibernate.default_batch_fetch_size 속성을 사용하면 애플리케이션 전체에 기본으로 @BatchSize를 적용할 수 있다.

<property name="hibernate.default_batch_fetch_size" value="5" />

하이버네이트 @Fetch(FetchMode.SUBSELECT)

연관된 데이터를 조회할 때 서브쿼리를 사용해서 N+1 문제를 해결한다

@Entity
class Member{
    @Id @GeneratedValue
    private Long id;

    @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Order> orders = new ArrayList<>();
}

아래와 같이 실행된다.

SELECT * FROM Member;
SELECT * FROM Order_
    WHERE MEMBER_ID IN(
        SELECT ID
        FROM Member
    )

즉시로딩으로 설정하면 조회시점에, 지연로딩으로 설정하면 지연로딩된 엔티티를 사용하는 시점에 위의 쿼리가 실행된다.

모두 지연로딩으로 설정하고 성능 최적화가 필요한 곳에는 JPQL 페치 조인을 사용하는 것이 추천되는 전략이다.


읽기 전용 쿼리의 성능 최적화

JPA의 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하는 특징이 있다.
하지만 단순 조회 화면에서는 조회한 엔티티를 다시 조회할 필요도 없고, 수정할 필요도 없어서 이때는 스냅샷 인스턴스를 위한 메모리가 낭비된다.
이럴 경우 아래의 방법으로 메모리 사용량을 최적화할 수 있다.

스칼라 타입으로 조회

엔티티가 아닌 스칼라 타입으로 모든 필드를 조회하는 것이다.
알다시피 스칼라 타입은 영속성 컨텍스트가 관리하지 않는다.

SELECT m.id, m.name, m.age FROM Member m

읽기 전용 쿼리 힌트 사용

하이버네이트 전용 힌트인 org.hibernate.readOnly를 사용하면 엔티티를 읽기 전용으로 조회할 수 있다.
읽기 전용이므로 영속성 컨텍스트가 스냅샷을 저장하지 않으므로 메모리 사용량을 최적화 할 수 있다.

Member member = em.createQuery("SELECT m FROM Member m", Member.class)
    .setHint("org.hibernate.readOnly", true)
    .getSingleResult();

스냅샷이 없으므로 member의 값을 수정해도 update 쿼리가 발생하지 않는다.

스냅샷만 저장하지 않는 것이지, 1차 캐시에는 그대로 저장한다
똑같은 식별자로 2번 조회했을 경우 반환되는 엔티티의 주소가 같다

읽기 전용 트랜잭션 사용

스프링 프레임워크를 사용하면 트랜잭션을 읽기 전용 모드로 설정할 수 있다.

@Transactional(readOnly = true)

트랜잭션을 읽기 전용으로 설정하면 스프링 프레임워크가 하이버네이트 세션의 플러시 모드를 MANUAL로 설정한다.
이렇게하면 강제로 플러시 호출을 하지 않는 한 플러시가 일어나지 않는다.

엔티티의 플러시 모드는 AUTO, COMMIT 모드만 있다.
MANUAL 모드는 하이버네이트 세션에 있는 플러시모드이다. 이는 강제로 플러시를 호출하지 않으면 절대 플러시가 일어나지 않는 특징을 가지고 있다.
하이버네이트 세션은 JPA 엔티티 매니저를 하이버네이트로 구현한 구현체이다.

플러시를 수행하지 않으므로 플러시할 떄 일어나는 스냅샷 비교와 같은 무거운 로직들이 실행되지 않으므로 성능이 향상된다.
(그래도 스냅샷은 그대로 저장하는 듯. 단지 플러시만 일어나지 않는 것 같다.)
물론 트랜잭션을 시작했으므로 트랜잭션 시작, 수행, 커밋의 과정은 이루어진다.

트랜잭션 밖에서 읽기

트랜잭션 없이 엔티티를 조회하는 것을 의미한다.
JPA에서 엔티티를 변경하려면 트랜잭션을 필수이므로, 조회가 목적일 때만 사용해야 한다.

JPA는 기본적으로 아래의 2가지 특성이 있다

스프링을 사용하지 않을 경우에는 그냥 간단히 트랜잭션을 시작해주지만 않고, 엔티티매니저만 가져와서 사용하면 된다.
스프링을 사용할 경우 아래와 같이 트랜잭션을 사용하지 않겠다고 해주면 되는데,

@Transactional(propagation = Propagation.NOT_SUPPORTED)

스프링은 기본적으로 트랜잭션이 시작할 때 영속성 컨텍스트를 실행하고, 트랜잭션이 스프링은 트랜잭션과 함께 영속성 컨텍스트를 실행시키는데 저렇게 해도 되는지?