Yebali

Spring을 이용한 API 개발 - Collection 조회 최적화 본문

Spring

Spring을 이용한 API 개발 - Collection 조회 최적화

예발이 2021. 10. 11. 20:35

Collection조회란 Entity에서 일대다 관계(@OneToMany)를 조회하는 것이다.

fetch join을 사용한 컬렉션 조회

Controller

@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
      List<Order> orders = orderRepository.findAllWithItem();
      List<OrderDto> result = orders.stream()
              .map(o -> new OrderDto(o))
              .collect(toList());
      return result;
}

Repository

public List<Order> findAllWithItem() {
    return em.createQuery(
    "select distinct o from Order o" +
        " join fetch o.member m" +
        " join fetch o.delivery d" +
        " join fetch o.orderItems oi" + //Collection fecth join
        " join fetch oi.item i", Order.class)
    .getResultList();
}

OrderItems까지 fetch join으로 한 번에 조회한다. -> SQL이 1번만 실행된다.
'distinct'를 사용하지 않으면 일대다(OneToMany) 조회 시 조회 결과가 카테시안 곱이 되어 조회된다. JPA의 distinct는 SQL에도 distinct를 추가하고 Order 엔티티가 조회되면 애플리케이션에서 중복을 제거하고 orderItems는 Collection에 모아준다.

 

이 방법의 가장 큰 단점은 페이징이 불가능하다는 것이다.
Collection fetch join을 했을 때 페이징을 하면, 모든 데이터를 DB에서 읽은 후 메모리에서 페이징 처리한다.
그리고 Collection fetch join은 1개만 사용해야 한다. 둘 이상의 fetch join을 사용하면 데이터가 부정합 하게 조회될 수 있다.

+ 페이징 처리

페이징 처리와 + Collection 조회를 같이하는 것은 아래 방법으로 대부분 가능하다.

  1. *ToOne관계를 모두 fecth join 한다.
  2. Collection은 지연 로딩으로 조회한다.
  3. 지연 로딩 성능 최적화를 위해 'hibernate.default_batch_fetch_size'나 '@BatchSize'를 적용한다.
    hibernate.default_batch_fetch_size : 글로벌 설정
    @BatchSize : 개별 최적화
    위의 옵션을 사용하면 Collection이나 proxy객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회한다.

Controller

@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;
}

Repository

@Repository
public class OrderRepository {
	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();
        }
}

*ToOne관계는 fetch join으로 최적화.

application.yml

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

장점

  1. 쿼리 호출 수가 N+1 -> 1+1로 최적화된다.
  2. Join보다 DB 데이터 전송량이 최적화된다.
  3. fetch join에 비해 쿼리 호출 수가 약간 증가하지만 전체적인 DB 데이터 전송량이 감소한다.
  4. 페이징이 가능하다.

ToOne관계는 fetch join을 해도 페이징에 영향을 주지 않는다.

따라서 ToOne관계는 fetch join으로 쿼리 수를 줄이고 나머지는 위의 옵션으로 해결하기를 추천한다.

JPA에서 DTO로 바로 조회, 플랫 데이터 최적화

Controller

@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
    //모든 데이터를 join해서 하나로 받은 후
    List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();

    //개발자가 직접 API 스펙에 맞는 DTO에 매핑함.
    return flats.stream()
            .collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
            		mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
            )).entrySet().stream()
            .map(e -> new OrderQueryDto(e.getKey().getOrderId(),
            		e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
            		e.getKey().getAddress(), e.getValue()))
            .collect(toList());
}

Repository

public List<OrderFlatDto> findAllByDto_flat() {
    return em.createQuery(
            "select new" +
                    " jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
                    " from Order o" +
                    " join o.member m" +
                    " join o.delivery d" +
                    " join o.orderItems oi" +
                    " join oi.item i", OrderFlatDto.class)
            .getResultList();
}

장점

  1. 쿼리 한 번에 모든 조회가 끝나버린다.

단점

  1. 쿼리는 한 번이지만 join으로 인해 더 느릴 수 있다.
  2. 애플리케이션에서 데이터를 일일이 DTO에 매핑하는 추가 작업이 크다.
  3. 페이징이 불가능하다.

그냥 참고
양방향 연관관계 조회 시 무한루프에 걸리지 않게 하려면 한 곳에 '@JsonIgnore'를 추가해야 할 수 있다.

'Spring' 카테고리의 다른 글

Spring Data JPA란?  (0) 2021.10.11
Spring JPA의 OSIV  (0) 2021.10.11
Spring을 이용한 API개발 - 조회 성능 최적화  (0) 2021.10.11
Spring을 이용한 API개발 - 기본  (0) 2021.10.11
Spring JPA 준영속 엔티티 수정하기  (0) 2021.10.11