@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2(){
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all.stream().map(OrderDto::of).collect(Collectors.toList());
}
record OrderDto(
Long orderId,
String name,
LocalDateTime orderDate,
Address address,
List<OrderItems> orderItems
){
public static OrderDto of(Order order){
return new OrderDto(
order.getId(),
order.getMember().getName(),
order.getOrderDate(),
order.getDelivery().getAddress(),
order.getOrderItems().stream()
.map(OrderItems::of)
.collect(Collectors.toList())
);
}
}
record OrderItems(
String itemName,
int orderPrice,
int count
){
public static OrderItems of(OrderItem orderItem){
return new OrderItems(
orderItem.getItem().getName(),
orderItem.getOrderPrice(),
orderItem.getCount()
);
}
}
}
Order -> OrderDto
변환OrderDto안에 OrderItem -> OrderItemDto
로 변환order
: 1번member
, address
: N번(order 조회 수 만큼)orderItem
: N번(order 조회 수 만큼)item
: N번(orderItem 조회 수 만큼)// OrderRepository 클래스
public List<Order> findAllWithItem(OrderSearch orderSearch) {
return em.createQuery("select 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();
}
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3(){
List<Order> all = orderRepository.findAllWithItem(new OrderSearch());
return all.stream().map(OrderDto::of).collect(Collectors.toList());
}
...............
// 위에 코드를 실행한 결과 값 (데이터 뻥튀기 발생)
Order Size: 4
Member Name: userA |Order Id: 7
Member Name: userA |Order Id: 7
Member Name: userB |Order Id: 14
Member Name: userB |Order Id: 14
public List<Order> findAllWithItem(OrderSearch orderSearch) {
return 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();
}
// 위에 코드를 실행한 결과 값
Order Size: 2
Member Name: userA |Order Id: 7
Member Name: userB |Order Id: 14
- 컬렉션을 페치 조인하면 페이징이 불가능하다.
- 컬렉션을 페치 조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
- 일대다에서 일(1)을 기준으로 페이징을 하는 것이 목적이다. 그런데 데이터는 다(N)을 기준으로 row가 생성된다.
Order
를 기준으로 페이징하고 싶은데, 다(N)인OrderItem
을 조인하면OrderItem
이 기준이 되어 버린다.- 이 경우 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도한다. 최악의 경우 장애로 이어질 수 있다.
ToOne(OneToOne, ManyToOne)
관계를 모두 페치조인 한다. (ToOne
관계는 row 수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.)hibernate.default_batch_fetch_size
, @BetchSize
를 적용한다.hibernate.default_batch_fetch_size
: 글로벌 설정@BatchSize
: 개별 최적화// OrderRepository 클래스
public List<Order> findAllWithMemberDelivery(OrderSearch orderSearch) {
return em.createQuery("select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class)
.setFirstResult(0)
.setMaxResults(100)
.getResultList();
}
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV2(){
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all.stream().map(OrderDto::of).collect(Collectors.toList());
}
.......
// 최적화 옵션
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
개별로 설정하려면 @BatchSize
를 적용하면 된다. (컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용)
N + 1
-> 1 + 1
로 최적화 된다.Order
와 OrderItem
을 조인하면 Order
가 OrderItem
만큼 중복해서 조회된다. 이 방법은 각각 조회하므로 전송해야할 중복 데이터가 없다.)결론:
ToOne
관계는 페치 조인해도 페이징 처리에 영향을 주지 않는다. 따라서 ToOne 관계는 페치 조인으로 쿼리 수를 줄여 해결하고, 나머지는hibernate.default_batch_fetch_size
로 최적화 하자.
hibernate.default_batch_fetch_size
권장 사이즈 : 100 ~ 1000