자바 ORM 표준 JPA6
자바 ORM 표준 JPA 프로그래밍6
김영한 지음
JPA 에서 교과서로 유명한 책을 읽고 일부 부분을 메모해봤습니다.
15장 고급 주제와 성능 최적화
예외 처리
JPA 표준 예외들은 javax.persistence.PersistenceException 의 자식 클래스다. 이 예외 클래스는 RuntimeException 의 자식이다. 따라서 JPA 예외는 모두 언체크 예외다.
트랜잭션 롤백을 표시하는 예외와, 트랜잭션 롤백을 표시하지 않는 예외로 나눌 수 있다. 후자의 경우에는 개발자가 트랜잭션 롤백 처리를 하도록 할 수 있다. 그러나 이 때, db 의 반영 사항만 롤백하는 것이지, 수정한 자바 객체까지 복구해주는 것은 아님을 알아야 한다.
따라서 트랜잭션이 롤백된 경우엔, 새로운 영속성 컨텍스트를 사용하거나 EntityManager.clear 를 호출해서 초기화를 진행해야 한다.
엔티티 비교
영속성 컨텍스트에는 엔티티 인스턴스를 보관하는 1차 캐시가 있다. 따라서 영속성 컨텍스트가 같다면 캐시 상의 엔티티도 동일하다. 이 동일하다는 의미는 3 가지 조건을 모두 만족한다는 뜻이다.
- 동일성 비교(==)
- 동등성 비교(equals)
- 데이터베이스 동등성(@id 인 식별자)
테스트 클래스에 @Transactional 을 적용하면 테스트가 끝날 때 트랜잭션을 커밋하지 않고 롤백한다.
테스트에서는 트랜잭션을 사용하지 않고, service 에서는 사용한다면 테스트에서 사용되는 객체와 영속 상태의 객체는 다르다.
1
2
3
4
5
6
7
8
9
10
@Test
public void 회원가입() throws Exception {
Member member = new Member();
Long savedId = memberServbice.join(member);
Member findMember = memberRepository.findOne(saveId);
assertTrue(member == findMember); // 둘의 주소는 다르다.
}
- 테스트 코드에서 회원가입을 시도하면, 서비스 계층의 트랜잭션이 시작되며 영속성 컨테스트가 생성된다.
- member 엔티티가 영속화된다.
- service 계층의 로직이 끝나면서 트랜잭션이 커밋된다. 영속성 컨텍스트가 플러쉬되고 종료된다. member 엔티티는 이제 준영속 상태다.
- 테스트 코드에서 memberRepository 를 통해 호출한 엔티티는 새로운 트랜잭션의 새로운 영속성 컨테스트의 객체이다.
- member 와 findMember 는 각각 다른 영속성 컨텍스트에서 관리되었기에 다른 인스턴스이다. equals 비교는 만족하겠지만 == 비교는 실패하는 것이다.
프록시 심화 주제
프록시는 원본 엔티티를 상속받아서 만들어진다. 영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성을 보장한다. JPA 는 프록시로 조회한 객체도 원본 엔티티와 동일하게 반환한다.
성능 최적화
JPA 에서 주의해야 하는 N + 1 문제를 해결해보자.
페치 조인 사용
가장 일반적인 방법이다. SQL 을 사용하여 연관된 엔티티를 함께 조회하는 방법이다.
1
2
3
select m from Member m join fetch m.orders
하이버네이트 @BatchSize
하이버네이트가 제공하는 BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size 만큼 SQL 의 IN 절을 사용해서 조회한다.
1
2
3
4
5
@org.hibernate.annotations.BatchSize(size = 5)
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<>(Order)();
만약 데이터를 조회할 개수가 5개 이하라면 SQL 이 두 번 실행되는 형태로 데이터를 가져올 수 있을 것이다. 그러나 IN 절은 성능에 좋지 않다.
하이버네이트 @Fetch(FetchMode.SUBSELECT)
연관된 데이터를 조회할 때 서브 쿼리를 사용하도록 할 수 있다.
1
2
3
4
5
select O from ORDERS O where O.member_id in (
select m. id from member m where m.id > 10
)
정리
지연 로딩을 시용하되, 여러 옵션을 고려해서 적절한 쿼리 전략을 선택하자.
읽기 전용 쿼리 성능 최적화
영속성 컨텍스트에 관리되는 엔티티는 1차 캐시부터 변경 감지까지 얻을 수 있는 혜택이 많다. 하지만, 변경 감지를 위해 스냅샷 인스턴스를 보관하는 것은 메모리 비용이 든다.
따라서, 읽기 전용으로 엔티티를 조회하면 메모리 사용을 최적화할 수 있다. 스프링 프레임워크를 사용하면 트랜잭션을 읽기 전용 모드로 설정할 수 있다.
1
2
3
@Transactional(readOnly = true)
이렇게 하면 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않는다. 따라서 트랜잭션을 커밋해도 영속성 컨택스트를 플러시 하지 않기에, 스냅샷 비교와 같은 무거운 로직을 수행하지 않는다.
배치 처리
배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야 하며, 2차 캐시를 사용하고 있다면 2차 캐시에 엔티티를 보관하지 않도록 주의해야 한다.
1
2
3
4
em.flush();
em.clear();
이처럼 개발자가 직접 영속성 컨텍스트를 초기화해줄 수 있다. 페이지 단위로 배치 처리를 해줄 수도 있고, scroll 이라는 JDBC 커서를 사용할 수도 있다
SQL 쿼리 힌트
SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다.
1
2
3
4
5
6
7
Session session = em.unwrap(session.class);
List<Member> list = session.createQuery("select m from Member m")
.addQueryHint("FULL (MEMBER)") // SQL HINT 추가
.list();
실행한 SQL 은 다음과 같다.
1
2
3
4
5
select
/*+ FULL (MEMBER) */ m.id, m.name
from Member m
쓰기 지연과 성능 최적화
JDBC 가 제공하는 SQL 배치 기능을 사용하면 SQL 을 모아서 데이터베이스에 한 번에 보낼 수 있다. JPA 는 플러시 기능이 있으므로 SQL 배치 기능을 효과적으로 사용할 수 있다.
이 기능 덕분에 db 테이블 로우에 락이 걸리는 시간을 최소화 할 수 있다.