현재 진행 중인 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
를 가져와야한다.
컬렉션을 페치조인 하게되면(여기서는 OrderSheet
에 Orders
를 페치조인하면) 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수가 많으면 장애로 이어질 수 있다. 따라서 컬렉션은 따로 가져와서 페이징 쿼리가 발생하도록 한다.
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개가 넘는 것을 허용하지 않는다.
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/