정리
엔티티 조회
- 엔티티를 조회해서 그대로 반환: V1
- 엔티티 조회 후 DTO로 변환: V2
- 페치 조인으로 쿼리 수 최적화: V3
- 컬렉션 페이징과 한계 돌파: V3.1
- 컬렉션은 페치 조인시 페이징이 불가능
- ToOne 관계는 페치 조인으로 쿼리 수 최적화
- 컬렉션은 페치 조인 대신에 지연 로딩을 유지하고, hibernate.default_batch_fetch_size , @BatchSize 로 최적화
- DTO 직접 조회
- JPA에서 DTO를 직접 조회: V4
- 컬렉션 조회 최적화 - 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화: V5
- 플랫 데이터 최적화 - JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환: V6
권장 순서
1. 엔티티 조회 방식으로 우선 접근
- 1. 페치조인으로 쿼리 수를 최적화
- 2. 컬렉션 최적화
- 1. 페이징 필요 hibernate.default_batch_fetch_size , @BatchSize 로 최적화
- 2. 페이징 필요X 페치 조인 사용
2. 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
3. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate
참고: 엔티티 조회 방식은 페치 조인이나, hibernate.default_batch_fetch_size , @BatchSize 같이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다. 반면에 DTO를 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.
참고: 개발자는 성능 최적화와 코드 복잡도 사이에서 줄타기를 해야 한다. 항상 그런 것은 아니지만, 보통 성능 최적화는 단순한 코드를 복잡한 코드로 몰고간다. > 엔티티 조회 방식은 JPA가 많은 부분을 최적화 해주기 때문에, 단순한 코드를 유지하면서, 성능을 최적화 할 수 있다. > 반면에 DTO 조회 방식은 SQL을 직접 다루는 것과 유사하기 때문에, 둘 사이에 줄타기를 해야 한다.
대부분의 어플리케이션은 fetch join으로 해결이 되지만 정말 사용자가 많은 어플리케이션은 DTO 방법보다는
캐시를 사용하는게 더 좋은 방법일 수 있다.
참고로 엔티티는 직접 캐싱을 하면 안된다.
DTO로 캐시를 해야한다.
이 포스트에서는 fetch join 만 코드로 구현해본다.
아래 v3 버전은 distinct로 fetch join하지만 페이징 처리가 불가능하다.
// fetch join(패치 조인)으로 쿼리 수 최적화 V3
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(OrderDto::new)
.collect(toList());
return result;
}
// /api/v3/orders 에서 호출 ( 컬렉션 조회할 때(일대다) fetch join ), 근데 페이징이 안됨
public List<Order> findAllWithItem() {
/*
페치 조인으로 SQL이 1번만 실행됨
distinct 를 사용한 이유는 1대다 조인이 있으므로 데이터베이스 row가 증가한다.
그 결과 같은 order 엔티티의 조회 수도 증가하게 된다.
JPA의 distinct는 SQL에 distinct를 추가하고, 더해서 같은 엔티티가 조회되면,
애플리케이션에서 중복을 걸러준다. 이 예에서 order가 컬렉션 페치 조인 때문에 중복 조회 되는것을 막아준다.
단점 - 페이징 불가능
*/
return em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m"+
" join fetch o.delivery d"+
" join fetch o.orderItems oi" +
" join fetch oi.item", Order.class)
.getResultList();
}
페이징 처리를 하려면 V3.1 버전을 해야한다.
이버전은 default_batch_fetch_size 나 @BatchSize를 사용한다.
/**
* V3.1 엔티티를 조회해서 DTO로 변환 페이징 고려
* - ToOne 관계만 우선 모두 페치 조인으로 최적화
* - 컬렉션 관계는 hibernate.default_batch_fetch_size, @BatchSize로 최적화
*/
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
// 컬렉션 조회할 때 페이징 처리 버전 - batch size 가 있어야 한다.
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
참고: default_batch_fetch_size 의 크기는 적당한 사이즈를 골라야 하는데, 100~1000 사이를 선택하는 것을 권장한다. 이 전략을 SQL IN 절을 사용하는데, 데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기도 한다. 1000으로 잡으면 한번에 1000개를 DB에서 애플리케이션에 불러오므로 DB 에 순간 부하가 증가할 수 있다. 하지만 애플리케이션은 100이든 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다. 1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정하면 된다.
'개발일지 > JPA' 카테고리의 다른 글
JPA 인덱스 걸기 (0) | 2022.09.26 |
---|---|
JPA 사용 시 쿼리 방식 선택 권장 순서(컬렉션 조회 안할 때) (0) | 2022.08.03 |
@JsonIgnore 사용과 지연로딩 에러 (0) | 2022.08.01 |
다양한 연관관계 매핑 (0) | 2022.07.07 |
JPA에서 엔티티 설계 시 주의 사항 (0) | 2022.04.27 |