본문 바로가기
스프링 관련/스프링 프레임워크

API 만들 때 생길 수 있는 문제 (컬렉션 조회 최적화)

by 문자메일 2022. 8. 28.
public List<Order> findAllWithItem() {
    return em.createQuery(
            // JPA에서만의 기능으로 전체 row를 보는 것이 아닌 Order 엔티티만 봐서 중복 제거해서 보여준다.
            "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 i", Order.class
    ).getResultList();
}

 

컬렉션 페치 조인을 사용하면 페이징이 불가능하다.

컬렉션 페치 조인은 1개만 사용할 수 있다.

 

 

컬렉션 조회 시 N + 1 문제 발생 해결 방법

1. 제약이 있는 fetch join 하던지 - 단점 : 페이지 기능 못씀

2. toOne 관계는 fetch join 잡고, default_batch_fetch_size 기능 쓰던지 : 페이징 기능 사용 가능

default_batch_fetch_size: 100 값 쓰면 기존처럼 1개씩 select 쿼리 날리는게 아니라, 아래 SQL문에서 볼 수 있듯이, SQL in 구문안에 조회할 조건 값을 100개 넣어서 SQL을 한 번만 실행할 수 있도록 설정할 수 있다.

 

2번 동작 적용 순서

2-1) ToOne (OneToOne, ManyToOneO) 관계를 모두 fetch join 한다. ToOne 관계는 row수를 증가시지키 않기에 페이징 쿼리에 영향을 주지 않는다.

2-2) 컬렉션은 지연 로딩으로 조회한다.

2-3) 지연 로딩 성능 최적화를 위하여 default_batch_fetch_size 나 @BatchSize 적용한다.

       해당 옵션을 설정하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.

 

3. 쌩으로 default_batch_fetch_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();
}
  jpa:
    hibernate:
      ddl-auto: create # create, none. none으로 바꾸면 table 드랍하지 않음(data base에 넣어놓은 data 그대로 쓸 수 있음)
    properties:
      hibernate:
#        show-sql: true
        format_sql: true
        default_batch_fetch_size: 100

 

2022-08-28 21:41:59.222 DEBUG 34760 --- [nio-9090-exec-1] org.hibernate.SQL                        : 
    select
        item0_.item_id as item_id2_3_0_,
        item0_.name as name3_3_0_,
        item0_.price as price4_3_0_,
        item0_.stock_quantity as stock_qu5_3_0_,
        item0_.artist as artist6_3_0_,
        item0_.etc as etc7_3_0_,
        item0_.author as author8_3_0_,
        item0_.isbn as isbn9_3_0_,
        item0_.actor as actor10_3_0_,
        item0_.director as directo11_3_0_,
        item0_.dtype as dtype1_3_0_ 
    from
        item item0_ 
    where
        item0_.item_id in (
            ?, ?
        )

 

 

 

 

private List<OrderItemQueryDto> findOrderItems(Long orderId) {
    return em.createQuery(
            "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                    " from OrderItem oi" +
                    " join oi.item i" +
                    " where oi.order.id = :orderId", OrderItemQueryDto.class)
            .setParameter("orderId", orderId)
            .getResultList();
}
// 앞의 버전에서는 모든 row를 대상으로 select query를 호출하였었는데, 이 방법에서는
// jpql where문 조건에 in을 활용해서 한 번에 가져온 다음
// 메모리에 Map으로 가져온 다음 메모리에서 매칭을 해서 값을 세팅해준다.

 

아래처럼 최적화해서 호출하면 query가 2번만 나간다.

public List<OrderQueryDto> findAllByDto_optimization() {

    List<OrderQueryDto> result = findOrders();

    List<Long> orderIds = result.stream()
            .map(o -> o.getOrderId())
            .collect(Collectors.toList());

    List<OrderItemQueryDto> orderItems =  em.createQuery(
                    "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                            " from OrderItem oi" +
                            " join oi.item i" +
                            " where oi.order.id in :orderIds", OrderItemQueryDto.class)
            .setParameter("orderIds", orderIds)
            .getResultList();

    // 앞의 버전에서는 모든 row를 대상으로 select query를 호출하였었는데, 이 방법에서는
    // jpql where문 조건에 in을 활용해서 한 번에 가져온 다음
    // 메모리에 Map으로 가져온 다음 메모리에서 매칭을 해서 값을 세팅해준다.
    Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
            .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));

    result.forEach(o->o.setOrderItems(orderItemMap.get(o.getOrderId())));

    return result;
}

댓글