JPA/JPA 활용

컬렉션 조회 최적화 2 - fetch join으로 최적화

chanhee01 2023. 8. 10. 16:35

OrderRepository에 finAllWithItem 추가

public List<Order> findAllWithItem() {
    return em.createQuery("select 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();
}

 

컨트롤러

@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
    List<Order> orders = orderRepository.findAllWithItem();

    List<OrderDto> collect = orders.stream()
            .map(o -> new OrderDto(o))
            .collect(Collectors.toList());

    return collect;
}

fetch join으로 가져온 데이터들을 컨트롤러에서 넘겨준다. 이 때의 Dto는 이전의 Dto를 그대로 사용했다.

원래 같은 경우에는 Order만 쿼리로 조회하기 때문에 Lazy 로딩을 통해서 orderItem이나 delivery 등의 조회가 필요할 시 쿼리를 계속해서 내보냈기 때문에 성능이 좋지 않았다. 하지만 이번 방식은 fetch join을 사용하기 때문에 처음 쿼리에서 한 번에 필요한 데이터 그래프들을 모두 가져온다.

 

중요한건 저번 포스팅과 컨트롤러의 코드 차이는 아무것도 없다. 단지, repository의 메서드만 변경되었을 뿐인데 쿼리가 훨씬 적게 나가서 성능이 훨씬 좋다는 장점이 있다. 맨 아래에서 실제 api를 호출하면 나가는 쿼리를 첨부했다.

 

(하이버네이트 5까지는 order가 중복해서 4번이 나왔지만 하이버네이트 6부터는 fetch join 시에 자동으로 중복 제거를 해주기 때문에 이러한 점은 고려하지 않아도 된다.)

 

 

 

단점

일대 다 fetch join(컬렉션 페치 조인)를 하는 순간 페이징이 불가능해진다.

하이버네이트는 db에 쿼리를 내보내는 것이 아니라 메모리에 데이터를 가져온 다음에 처리를 한다. 데이터 양이 방대하다면 메모리 에러가 발생할 것이다.

public List<Order> findAllWithItem() {
        return em.createQuery("select 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)
                .setFirstResult(1)
                .setMaxResults(10)
                .getResultList();
    }

위의 쿼리에서 o.member이나 o.delivery나 oi.item i만 있을 때에는 상관 없다. 하지만 컬렉션 페치 조인인 o.orderItems oi 같은 경우에는 Order 입장에서 일대 다 fetch join이기 때문에 하이버네이트가 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다.

 

 

콘솔에 뜬 에러

WARN 14628 --- [nio-8080-exec-2] org.hibernate.orm.query    : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

 

 

 

 

 

 

 

실제로 api를 호출하면 나가는 쿼리

    select
        o1_0.order_id,
        d1_0.delivery_id,
        d1_0.city,
        d1_0.street,
        d1_0.zipcode,
        d1_0.status,
        m1_0.member_id,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.name,
        o1_0.order_date,
        o2_0.order_id,
        o2_0.order_item_id,
        o2_0.count,
        i1_0.item_id,
        i1_0.dtype,
        i1_0.name,
        i1_0.price,
        i1_0.stock_quantity,
        i1_0.artist,
        i1_0.etc,
        i1_0.author,
        i1_0.isbn,
        i1_0.actor,
        i1_0.director,
        o2_0.order_price,
        o1_0.status 
    from
        orders o1_0 
    join
        member m1_0 
            on m1_0.member_id=o1_0.member_id 
    join
        delivery d1_0 
            on d1_0.delivery_id=o1_0.delivery_id 
    join
        order_item o2_0 
            on o1_0.order_id=o2_0.order_id 
    join
        item i1_0 
            on i1_0.item_id=o2_0.item_id