컬렉션을 fetch join하면 페이징이 불가능함
컬렉션을 페치조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가함
따라서 Order를 기준으로 페이징을 하고 싶은데, 다(N)인 OrderItem을 조인하면 OrderItem이 기준이 되어버림
이 경우에 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도함
ToOne(OneToOne, ManyToOne) 관계를 모두 fetch join함
ToOne관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않음
- 컬렉션은 지연 로딩으로 조회
- 지연 로딩 최적화를 위해
hibernate.default_batch_fetch_size,@BatchSize를 적용함
hibernate.default_batch_fetch_size: 글로벌 설정@BatchSize: 개별 최적화- 이 옵션을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회함
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
...
@GetMapping("/api/v3.1/orders")
public List<OrderDto> orderV3_page() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(toList());
}
}
@Data
static class OrderItemDto {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
}
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.getResultList();
}
Member와 delivery는 확실하게 ToOne 관계이기 때문에 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않음
ToOne 관계는 계속 fetch join을 걸어도 됨컬렉션 형태인 OrderItems는 fetch join을 걸지 않고 지연 로딩으로 조회
페이징 처리를 해도 문제가 발생하지 않음
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
...
/**
* V3.1 엔티티를 조회해서 DTO로 변환 페이징 고려
* * - ToOne 관계만 우선 모두 페치 조인으로 최적화
* * - 컬렉션 관계는 hibernate.default_batch_fetch_size, @BatchSize로 최적화
* @return List<OrderDto>
*/
@GetMapping("/api/v3.1/orders")
public List<OrderDto> orderV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit){
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
...
}
package jpabook.jpashop.repository;
...
@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();
}
}

offset을 1로 넘겼기 때문에 2번 order 정보가 출력이 됨application.yml 파일 수정
spring:
...
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
...
위 옵션은 글로벌 설정이므로, 개발 최적화를 설정하고 싶다면 @BatchSize 어노테이션을 붙여줘야 함
만약 Order와 OrderItems와 같이 OneToMany 관계의 경우, Order Entity의 OrderItems 필드 바로 위에 @BatchSize(size = 100)를 추가하면 됨
OrderItem과 Item과 같이 ManyToOne 관계의 경우, 필드가 아닌 Item Entity class에 @BatchSize(size = 100) 어노테이션을 붙여줘야 함


이전에는 OrderItems를 유저 A와 유저 B 각각 날려서 두 건씩 조회함
하지만 application.yml에 default_batch_fetch_size: 100를 추가하면 한꺼번에 IN 쿼리로 조회를 수행함
쿼리 호출 수가 1 + N에서 1 + 1로 최적화가 됨
API 수행 시에 호출되는 Query를 직접 조회해보면, 데이터가 중복없이 정확하게 필요한 데이터만 조회해오는 것을 확인 할 수 있음



쿼리 호출 수가 1 + N -> 1 + 1로 최적화됨
조인보다 DB 데이터 전송량이 최적화 됨
Order와 OrderItem을 조인하면 Order가 OrderItem만큼 중복해서 조회됨페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소함
컬렉션 페치 조인은 페이징이 불가능하지만 이 방법은 페이징이 가능함
ToOne 관계는 페치 조인을 해도 페이징에 영향을 주지 않음
따라서 ToOne 관계는 페치 조인으로 쿼리 수를 줄이고 해결함
나머지는 hibernate.default_batch_fetch_size로 최적화
100-1000 사이를 선택하는 것이 권장됨
데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기도 함
1000으로 잡으면 한 번에 1000개를 DB에서 애플리케이션에 불러오므로 DB에 순간 부하가 증가할 수 있음
1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정하면 됨