JPA 컬렉션 페치조인과 paging 처리

yeon·2021년 9월 13일
2

컬렉션 페치조인과 paging 처리

현재 진행 중인 ITDA 프로젝트에 사용자가 주문한 내역을 조회하는 기능 구현한 내용을 정리했다. OrderSheet 는 주문서로 보면 된다. 이 주문서 객체와 연관된 Order (개별 제품 당 주문내역이다. 제품과 수량을 갖고있다.), Product 까지 세개의 엔티티를 사용자를 기준으로 함께 조회해야한다.

아래는 엔티티 클래스들이다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class OrderSheet extends Core {

		...

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "orderSheet", cascade = CascadeType.ALL)
    private final List<Order> orders = new ArrayList<>();

   ...
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "orders")
@Getter
public class Order extends Core {

    private Integer quantity;

    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_sheet_id")
    private OrderSheet orderSheet;

}

엔티티들의 관계를 살펴보자.

OrderSheet
Order 는 OneToMany 관계로 설정되어 있다.
User 는 ManyToOne, 지연로딩으로 설정되었다.

Order
Product 는 ManyToOne 관계이고 Order라는 테이블은 제품 당 수량 이라고 보면 된다. 때문에 Product 가 항상 필요할 것이라고 생각하여 즉시로딩으로 설정했다.

이제 OrderSheet를 기준으로 OneToMany로 설정된 컬렉션 orders 과 orders에 매핑된 product 를 가져와야한다.

컬렉션을 페치조인 하게되면(여기서는 OrderSheetOrders를 페치조인하면) one to many의 many만큼의 데이터 row가 생긴다.

one을 기준으로 페이징하는 것이 목적이기 때문에 컬렉션을 페치조인하면 하이버네이트는 메모리에서 페이징을 하고 warn 로그를 남긴다.

컬렉션 페치조인으로 시도해보기

먼저 OrderSheet와 연관된 엔티티 중 필요한 것을 모두 fetch join으로 가져오고 어떻게 실행되는지 확인해봤다.

public interface OrderSheetRepository extends JpaRepository<OrderSheet, Long> {

    @Query("select distinct os from OrderSheet os" +
            " left join fetch os.orders o" +
            " left join fetch o.product p" +
            " where os.user.id = ?1")
    List<OrderSheet> findByUserId(Long userId, Pageable pageable);
}
2021-09-13 22:43:59.906  WARN 33840 --- [nio-8080-exec-5] o.h.h.internal.ast.QueryTranslatorImpl   : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
Hibernate: 
    select
        distinct ordersheet0_.id as id1_3_0_,
        orders1_.id as id1_2_1_,
        product2_.id as id1_4_2_,
        ordersheet0_.created_at as created_2_3_0_,
        ordersheet0_.updated_at as updated_3_3_0_,
        ordersheet0_.paid_yn as paid_yn4_3_0_,
        ordersheet0_.shipping_info_id as shipping6_3_0_,
        ordersheet0_.total_price as total_pr5_3_0_,
        ordersheet0_.user_id as user_id7_3_0_,
        orders1_.created_at as created_2_2_1_,
        orders1_.updated_at as updated_3_2_1_,
        orders1_.order_sheet_id as order_sh5_2_1_,
        orders1_.product_id as product_6_2_1_,
        orders1_.quantity as quantity4_2_1_,
        orders1_.order_sheet_id as order_sh5_2_0__,
        orders1_.id as id1_2_0__,
        product2_.created_at as created_2_4_2_,
        product2_.updated_at as updated_3_4_2_,
        product2_.account as account4_4_2_,
        product2_.account_holder as account_5_4_2_,
        product2_.bank as bank6_4_2_,
        product2_.capacity as capacity7_4_2_,
        product2_.delivery_description as delivery8_4_2_,
        product2_.delivery_fee as delivery9_4_2_,
        product2_.description as descrip10_4_2_,
        product2_.image_url as image_u11_4_2_,
        product2_.main_category_id as main_ca19_4_2_,
        product2_.notice as notice12_4_2_,
        product2_.origin as origin13_4_2_,
        product2_.package_type as package14_4_2_,
        product2_.price as price15_4_2_,
        product2_.sales_unit as sales_u16_4_2_,
        product2_.seller as seller20_4_2_,
        product2_.sub_title as sub_tit17_4_2_,
        product2_.title as title18_4_2_ 
    from
        order_sheet ordersheet0_ 
    left outer join
        orders orders1_ 
            on ordersheet0_.id=orders1_.order_sheet_id 
    left outer join
        product product2_ 
            on orders1_.product_id=product2_.id 
    where
        ordersheet0_.user_id=? 
    order by
        ordersheet0_.created_at asc

firstResult/maxResults specified with collection fetch; applying in memory! warning이 발생했다.

one to many 관계인 컬렉션(orders) 페치조인을 하면 데이터 row 수가 증가하는데 필자는 ordersheet를 기준으로 페이징하는 것을 원한다. 때문에 페이징 쿼리를 날리면 기대하는것과 다른 결과가 나올 수 있다. 따라서, 하이버네이트는 모든 결과를 가져오고 메모리에서 페이징 처리한다.

이 경우에, 데이터 row수가 많으면 장애로 이어질 수 있다. 따라서 컬렉션은 따로 가져와서 페이징 쿼리가 발생하도록 한다.

하이버네이트의 batch size 설정 사용하기

hibernate.default_batch_fetch_size 혹은 @BatchSize를 지정하면 연관된 엔티티를 size만큼 한꺼번에 IN쿼리로 조회한다.

(The default value is 1 which explains why hibernate issues a select query for each id.)

지연로딩으로 설정한 ManyToOne 관계 엔티티에 대해서도 마찬가지로 batch size를 지정해주면 IN쿼리가 나가서 N+1문제가 발생하지 않는다.

이때 batch size를 적절한 값으로 지정해줘야 하는데 너무 낮은 값으로 설정하면 쿼리가 여러번 발생할 수 있어서 성능적인 이점이 부족할 수 있다. 반면, MySQL같은 데이터베이스는 IN 절에 1000개가 넘는 것을 허용하지 않는다.

batch size 설정 코드와 쿼리 결과

application.yml에서 batch size를 100으로 설정했다.

그리고 repository의 코드도 다음과 같이 변경했다.

@Query("select os from OrderSheet os" +
            " where os.user.id = ?1")
List<OrderSheet> findByUserId(Long userId, Pageable pageable);

ordersheet에 limit절이 포함된 것을 보니 Pagination API가 잘 적용되었고 warn도 사라졌다.

orders로 IN쿼리로 가져오는 것을 볼 수 있다.

Hibernate: 
    select
        ordersheet0_.id as id1_3_,
        ordersheet0_.created_at as created_2_3_,
        ordersheet0_.updated_at as updated_3_3_,
        ordersheet0_.paid_yn as paid_yn4_3_,
        ordersheet0_.shipping_info_id as shipping6_3_,
        ordersheet0_.total_price as total_pr5_3_,
        ordersheet0_.user_id as user_id7_3_ 
    from
        order_sheet ordersheet0_ 
    where
        ordersheet0_.user_id=? 
    order by
        ordersheet0_.created_at asc limit ?
Hibernate: 
    select
        orders0_.order_sheet_id as order_sh5_2_3_,
        orders0_.id as id1_2_3_,
        orders0_.id as id1_2_2_,
        orders0_.created_at as created_2_2_2_,
        orders0_.updated_at as updated_3_2_2_,
        orders0_.order_sheet_id as order_sh5_2_2_,
        orders0_.product_id as product_6_2_2_,
        orders0_.quantity as quantity4_2_2_,
        product1_.id as id1_4_0_,
        product1_.created_at as created_2_4_0_,
        product1_.updated_at as updated_3_4_0_,
        product1_.account as account4_4_0_,
        product1_.account_holder as account_5_4_0_,
        product1_.bank as bank6_4_0_,
        product1_.capacity as capacity7_4_0_,
        product1_.delivery_description as delivery8_4_0_,
        product1_.delivery_fee as delivery9_4_0_,
        product1_.description as descrip10_4_0_,
        product1_.image_url as image_u11_4_0_,
        product1_.main_category_id as main_ca19_4_0_,
        product1_.notice as notice12_4_0_,
        product1_.origin as origin13_4_0_,
        product1_.package_type as package14_4_0_,
        product1_.price as price15_4_0_,
        product1_.sales_unit as sales_u16_4_0_,
        product1_.seller as seller20_4_0_,
        product1_.sub_title as sub_tit17_4_0_,
        product1_.title as title18_4_0_,
    from
        orders orders0_ 
    left outer join
        product product1_ 
            on orders0_.product_id=product1_.id 
    where
        orders0_.order_sheet_id in (
            ?, ?
        )


참고: https://prasanthmathialagan.wordpress.com/2017/04/20/beware-of-hibernate-batch-fetching/

https://tecoble.techcourse.co.kr/post/2021-07-26-jpa-pageable/

김영한님 JPA 활용2 강의

https://thorben-janssen.com/pagination-jpa-hibernate/

0개의 댓글