Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

개발자되기 프로젝트

컬렉션 조회 최적화 :페이징 & batch_fetch 본문

인프런/[인프런] Springboot와 JPA활용 2

컬렉션 조회 최적화 :페이징 & batch_fetch

Seung__ 2021. 8. 24. 21:13

1.  페이징 한계


  • 컬렉션을 페치 조인하면 페이징이 불가능함
    • 컬렉션을 페이 조인하면 일대다 조인이 발생하여 데이트가 예측할 수 없이 증가한다.
    • 일대다에서 "일"을 기준으로 페이징을 하는 것이 목적인데
    • DATA는 "다"를 기준으로 DB에서 row가 생성된다..
    • 즉 Order 기준으로 페이징을 하고싶지만.. "다"인 OrderItem을 조인하면 "다"가 기준임..
  • 이 경우에 페이징을 시도하면 데이터를 싹 메모리에 올려서 페이징을 시도한다.
  • ㅎㅎㅎㅎㅎ심각하다.

 

2. hibernate.default_batch_fetch_size, @BatchSize


  • 그러면, 페이징 + 컬렉션 엔티티 함께 조회하고 싶으면 어떻게 해야함..
  • xToOne 관계를 모두 fetch join함. ToOne관계를 row수를 증가시키지 않아 페이징쿼리에 영향 안줌.
  • ToOne은 싹다 fetch join으로 걸어서 한방쿼리로 만들어.
  • 컬렉션은 지연로딩으로 가져와. 즉 컬렉션을 fetch join하지말고
  • hibernate.default_batch_fetch_size(항상켜둬 ㅋㅋ), @BatchSize를 적용하자.
  • application.yml에 옵션 추가
  jpa:
    hibernate:
      ddl-auto: create-drop
    Properties:
      hibernate:
        default_batch_fetch_size: 100
        show_sql: true
        format_sql: true

 

   @GetMapping("/api/v3.1/orders")
    public Result ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
                                @RequestParam(value = "limit", defaultValue = "100") int limit){
        List<Order> orders = orderRepository.findAllWitMemberDelivery(offset, limit);
        List<OrderDto> result = orders.stream().map(o -> new OrderDto(o)).collect(Collectors.toList());
        return new Result(result);
    }
  • 쿼리를 보자!
  • fetch join과 관련된 쿼리가 나가고
Hibernate: 
    select
        order0_.order_id as order_id1_6_0_,
        member1_.member_id as member_i1_4_1_,
        delivery2_.delivery_id as delivery1_2_2_,
        order0_.delivery_id as delivery4_6_0_,
        order0_.member_id as member_i5_6_0_,
        order0_.order_date as order_da2_6_0_,
        order0_.status as status3_6_0_,
        member1_.city as city2_4_1_,
        member1_.street as street3_4_1_,
        member1_.zipcode as zipcode4_4_1_,
        member1_.name as name5_4_1_,
        delivery2_.city as city2_2_2_,
        delivery2_.street as street3_2_2_,
        delivery2_.zipcode as zipcode4_2_2_,
        delivery2_.status as status5_2_2_ 
    from
        orders order0_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id 
    inner join
        delivery delivery2_ 
            on order0_.delivery_id=delivery2_.delivery_id limit ?
  • order_item을 가져오는 쿼리가 나가는데..?
  • 응? orderItems_order_id in ( ?, ?) 뭐지?
  • 파라미터를 보자.
  • orderId(id = 4, id = 11)에 해당하는 orderItems를 모두 가지고 온것.
  • 즉, userA, userB의 orderItem를 모두 가지고온다.
Hibernate: 
    select
        orderitems0_.order_id as order_id5_5_1_,
        orderitems0_.order_item_id as order_it1_5_1_,
        orderitems0_.order_item_id as order_it1_5_0_,
        orderitems0_.count as count2_5_0_,
        orderitems0_.item_id as item_id4_5_0_,
        orderitems0_.order_id as order_id5_5_0_,
        orderitems0_.order_price as order_pr3_5_0_ 
    from
        order_item orderitems0_ 
    where
        orderitems0_.order_id in (
            ?, ?
        )
        
        
 select 
 orderitems0_.order_id as order_id5_5_1_, orderitems0_.order_item_id 
 as order_it1_5_1_, orderitems0_.order_item_id as order_it1_5_0_,
 orderitems0_.count as count2_5_0_, orderitems0_.item_id as item_id4_5_0_, 
 orderitems0_.order_id as order_id5_5_0_, orderitems0_.order_price as order_pr3_5_0_ 

from order_item orderitems0_ 

where orderitems0_.order_id in (4, 11);
  • 즉 정리하면 batch_size를 지정하게되면
  • 컬렉션(여기서는 orders)를 조회할 때 해당 컬렉션(order)관련된 orderItems를 모두 가져오도록 in쿼리를 통해 한번에 가져옴
  • 그 때 in 로 가져올 개수를 지정해서 미리 땡겨서 가져옴.

  • 그럼 item은??? 
  • item도 orders와 관련된 모든 item을 한번에 가져옴
Hibernate: 
    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 (
            ?, ?, ?, ?
        )
        
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 (2, 3, 9, 10);

 

 

3. 그러면 전부 fetch join해서 가지고 오는 것 보다 장점은?


 public List<Order> findAllWithItem(){

       List<Order> resultList = 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 i", Order.class)
               .getResultList();

       return resultList;
   }

컬렉션을 싹 다 fetch join으로 직접 가지고 오면

쿼리는 한번에 끝나지만! 중복된 데이터도 많고 데이터 양 자체가 늘어난다.

 

반면 batch size를 적용하면

 

쿼리는 한 번은 아니지만 중복된 데이터도 줄고 양 자체가 줄어든다. 오히려 좋아.

3. batch_fetch를 적용하면 ToOne에 해당하는 fetch join을 빼도 최적화가 된다.

  • 이래도 된다고?
    public List<Order> findAllWitMemberDelivery(int offset, int limit) {

        return em.createQuery(
                "select o from Order o " , Order.class)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();

    }

오 in쿼리로 한번에 불러오긴 하는데... 쿼리가 많이 줄진 않네. 네트워크 많이탈듯.

 

ToOne은 그냥 fetch join으로 하자. ㅋㅋㅋ

 

 

4. 정리


  • 장점
    • 쿼리 호출 수가 1 + N -> 1 + 1 로 최적화됨
    • 조인보다 DB데이터 전송량이 최적화 된다. 
      (Order와 OrderItem을 조인하면 Order가 OrderItem만큼 중복해서 조회된다.
      이 방법은 각각 조쇠하기 때문에 전송해야할 중복 데이터가 없다.)
    • 페이 조인방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB데이터 전송량이 감소한다.
    • 컬렉션 페치 조인은 페이징이 불가능 하지만, 이 방법은 페이징이 가능하다.
  • 결론
    • ToOne 관계는 페지 조인해도 페이징에 영향을 주지 않는다. 따라서 ToOne관계는 페치 조인으로 쿼리 수르 ㄹ줄여 해결하고, 나머지(컬렉션)는 hibernate.default_batch_fetch_size로 최적화 하자.
  • 그럼 사이즈는 몇으로 해야함?
    • DB에 따라 다르긴 한데 max1000임
    • 그럼 적당한 사이즈? 100~ 1000 사이
    • 참고로 애플리케이션은 100이든, 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량은 같다.
    • 결국 db든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정해야 함.

 

5. GitHub : 210824 collection, batch_fetch


 

GitHub - bsh6463/SpringBootJPA1

Contribute to bsh6463/SpringBootJPA1 development by creating an account on GitHub.

github.com

 

Comments