스프링데이터 + JPA/API 개발

12. 컬렉션 -> 엔티티 join fetch 최적화

sdafdq 2023. 11. 15. 07:18

그냥 join fetch 해서 가져올 거다.

 

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

 

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", Order.class)
            .getResultList();
}

이러면 쿼리가 이제 하나로 나간다.

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

이렇게 하면, 결국 DB는 조회한 모든 데이터를 가져와야 하기 때문에,

 

이런 식으로 데이터가 뻥튀기 된다.

뭐 이게 잘못된 게 아니다. 당연히 올바른 동작이다.

여기서 distinct를 써도 그거는 데이터가 완전히 다 같아야 중복 제거가 되기 때문에 결과는 똑같다.

근데, 이 DB -> 객체로 오면서 생기는 문제가 있다.

저 DB가 객체로 오게 되면서 중복된 것도 그냥 List에 넣어진다. 즉, order들을 조회 해 왔는데 order_id 1인게 2개, 2인개 2개 이렇게 해서 총 4개가 된다.

객체는 그럴 필요가 없다.

참조로 관계가 정의되기 때문에 그냥 order 객체에  order_item이 2개 List로 넣어져 있으면 된다.

근데 result는 4개로 나와진다.

 

그래서, JPQL에서는 distinct를 쓰면 저런 주로 조회한 것의 id가 겹치는 것을 애플리케이션 상에서 따로 중복제거를 해준다.

 

그래서 써야 했었는데, 하이버네이트 6 오면서 패치가 되었다. distinct를 안써도 이제 그냥 알아서 애플리케이션 상에서 중복제거를 해준다.

 

그런데, DB상에서는 저렇게 쿼리가 나간다는 걸 알아야 한다.

저렇게 order_id 입장에서 중복된 결과가 나오는 건 정상적인 동작이다. 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", Order.class)
            .setFirstResult(0)
            .setMaxResults(100)
            .getResultList();
}

 

첫번째 데이터부터 100번째 데이터까지.

 

잘 감이 안오면, 뭐 0번부터 1번까지라고 생각해보자.

 

그럼 어떻게 나올 것 같나?

 

DB상에서는 이 상태에서 페이징 쿼리를 날리게 되면, 

중간에 짤린다.

DB상에서는 총 4개의 데이터가 나왔다.

근데 여기서 sql에 페이징 쿼리를 날리게 되면 우리가 의도했던 데로 order_id가 1번인 것, order_id가 2번인 것이 나오는 게 아니라, order_id가 1번인 것만 2개 나오고 짤릴 것이다.

DB상에서는 그게 정상이다.

 

그래서, JPQL에서 저렇게 페이징을 쓰면,

2023-11-15T07:09:40.100+09:00  WARN 23912 --- [nio-8080-exec-1] org.hibernate.orm.query                  : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

firstResult/maxResults specified with collection fetch; applying in memory

firstResult, maxResult 그러니까 페이징 명령? 이 collection fetch와 함께 사용되었다고,

메모리 상에서 진행한다고 경고가 뜬다.

즉, 이렇게 collection에 fetch를 사용한 상태에서 페이징을 하면, 따로 sql에 페이징 쿼리가 나가는 게 아니라, 일단은 다 가져온 다음에, 메모리에서 처리 한다.

 

즉, 일단 다 가져온 다음에, 애플리케이션에서 가져온 Order를 기준으로 중복제거를 하고, 0번, 1번 그러니까 List<Order>에서 0번째 인덱스부터 1번째 인덱스까지 가져오는 것이다.

 

이게 생각보다 굉장히 치명적일 수 있는게, 데이터가 엄청나게 많은 경우 out of memory로 이어질 수 있다.

 

DB의 동작 따로, 애플리케이션의 동작 따로 생각해야 한다.

'스프링데이터 + JPA > API 개발' 카테고리의 다른 글

14. 컬렉션 Dto로 직접 조회  (0) 2023.11.16
13. 컬렉션 페이징 한계돌파  (0) 2023.11.15
11. 컬렉션 엔티티를 Dto로  (0) 2023.11.14
10. 컬렉션 조회 최적화  (0) 2023.11.14
9. 바로 Dto로 받기  (0) 2023.11.14