이전 글 : 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화(컬렉션 조회 최적화 - 엔티티를 DTO로 변환 + 최적화)
-> 이 경우 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리
에서 페이징을 시도한다.
X To One
(OneToOne, ManyToOne) 관계는 모두 Fetch Join
(X To One 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.)
컬렉션은 Fetch Join 대신 지연 로딩
읽으면 좋을 글 : 객체지향 쿼리 언어 - 중급 문법 (fetch join) [매우 중요]
최적화를 위해 hibernate.default_batch_fetch_size
, @BatchSize
적용
- hibernate.default_batch_fetch_size: 글로벌 설정
- @BatchSize: 개별 최적화
@RestController
@RequiredArgsConstructor
public class OrderApiController {
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page() {
return orderRepository.findAllWithMemberDelivery().stream()
.map(OrderDto::new)
.collect(Collectors.toList());
}
}
@Repository
@RequiredArgsConstructor
public class OrderRepository {
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
}
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
제일 먼저 Order를 조회할 때 X To One
관계에 해당하는 member
, Delivery
엔티티를 한 번의 쿼리로 조회하기 때문에 성능 개선이 된다.
하지만, 컬렉션인 OrderItems
를 조회할 경우 수 많은 쿼리가 아래 처럼 발생한다.
select orderitems0_.order_id as order_id5_5_0_,
orderitems0_.order_item_id as order_it1_5_0_,
orderitems0_.order_item_id as order_it1_5_1_,
orderitems0_.count as count2_5_1_,
orderitems0_.item_id as item_id4_5_1_,
orderitems0_.order_id as order_id5_5_1_,
orderitems0_.order_price as order_pr3_5_1_
from order_item orderitems0_
where orderitems0_.order_id = ?
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 = ?
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 = ?
OrderItem
그리고 item
의 개수에 따라 수 많은 쿼리가 나가기 때문에 성능이 저하된다.
@RestController
@RequiredArgsConstructor
public class OrderApiController {
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit) {
return orderRepository.findAllWithMemberDelivery(offset, limit).stream()
.map(OrderDto::new)
.collect(Collectors.toList());
}
}
@Repository
public class OrderRepository {
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
}
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
default_batch_fetch_size: 100 // 추가한 부분
필드 별로 Batch_Size를 적용하고 싶으면 해당 엔티티의 필드에
@BatchSize
어노테이션을 달아준다.
참고:
default_batch_fetch_size
의 크기는 100~1000 사이를 선택한다.
데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기에 최대 값을 1000으로 둔다.
어떤 크기를 설정하더라도, 결국 모든 데이터를 로딩해야 하므로 메모리 사용량은 같다.
따라서, 순간 부하를 견딜 수 있는 정도로 결정한다.
...delivery_id=delivery2_.delivery_id limit 100 offset 1;
쿼리를 살펴보면 Batch_Size를 지정한 뒤에 정상적으로 페이징이 작동한 것을 확인할 수 있다.
item0_.dtype as dtype1_3_0_ from item item0_ where item0_.item_id in (9, 10);
또한 Batch_Size
에 맞게 데이터를 in절로 가져온다.
쿼리 호출 수가 1 + N
-> 1 + 1
로 최적화
조인보다 DB 데이터 전송량이 최적화
(컬렉션을 조회하면 1
에 해당하는 엔티티가 N
만큼 중복 조회된다.
X To One
만 Fetch Join하면 중복 데이터가 없다.)
컬렉션 Fetch Join 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
컬렉션 Fetch Join은 페이징이 불가능 하지만 이 방법은 페이징이 가능하다.
X To One
관계는 Fetch Join을 해도 페이징에 영향을 주지 않는다.
-> X To One
관계는 Fetch Join으로 쿼리 수를 줄인다.
X To Many
관계는 지연 로딩으로 페이징을 가능하게 하고,
hibernate.default_batch_fetch_size
로 쿼리 수를 줄인다.
참고 :