이전 글 1 편에서는 x to One (ManyToOne, OneToOne) 관계에서 API를 개발하는 방법을 알아봤다. 이번에는 x to Many (ManyToMany, OneToMany) 관계에서 API를 어떻게 개발하는지 알아보자.
기본적으로 엔티티를 DTO로 변환해서 사용한다.
@GetMapping("api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
@Data
@AllArgsConstructor
static class OrderDto {
private Long orderId;
private String name;
private LocalDate 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(Collectors.toList());
}
}
@Data
@AllArgsConstructor
static calss OrderItemDto {
private String itemName;
private int orderPrice;
private int count;
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
OrderDto 생성자 내부에서 orderItems를 선언할 때도 OrderItemDto를 사용한다. 단순히 덮어씌우는 것이 아니라 엔티티는 모두 DTO로 변환해서 사용해야 엔티티에 의존하지 않고 사용할 수 있다.
❗ 잘못된 방법
@Data
@AllArgsConstructor
static class OrderDto {
private List<OrderItem> orderItems;
public OrderDto(Order order) {
orderItems = order.getOrderItems()
}
}
OrderItem 엔티티에 의존하기 때문에 잘못된 방법이다. 엔티티를 DTO로 변환해서 사용하는 것이 올바른 방법이다.
❗ 단순히 DTO로 변환해서 사용하면 N+1 문제가 발생한다.
@GetMapping("api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
// OrderRepository
public List<Order> findAllWithItem() {
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();
}
fetch join을 사용해 쿼리 1번으로 데이터를 조회했다. 하지만, 실제로 데이터를 조회해보면 원하던 결과가 아니다. order가 2개라서 2개의 데이터가 조회될 줄 알았는데 DB에서는 4개의 데이터가 조회됐다.

❗ 페이징 불가능
컬렉션 fetch join을 사용하면 order 기준이 아닌 orderItems 기준으로 row가 변경되기 때문에 조회하는 데이터 수가 변경된다. ( DB에서 조회한 데이터랑 Postman으로 조회한 데이터가 수가 다른것처럼 )
따라서, 컬렉션 fetch join을 사용해 페이징 한다면 Hibernate는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징한다. -> 사용하면 안된다.
❗ 컬렉션 fetch join은 1개만 사용할 수 있다.
2개 이상 사용한다면 (1:N):N 관계로 데이터가 부정합하게 조회될 수 있다.
페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까 ?
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
BatchSize 설정은 JPA 성능 개선을 위한 옵션 중 하나이다.
여러 개의 프록시 객체를 조회할 때 WHERE 절이 같은 여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어준다.
📌 참고
default_batch_fetch_size의 크기는 적당한 사이즈를 골라야한다. 보통 100 ~ 1000 사이를 선택하는 것을 권장한다. DB에 따라 IN 절 파라미터를 1000으로 제한하기도 하기때문이다. 또한, 1000개의 데이터를 한번에 가져온다면 순간 부하가 증가할 수 있기에 순간 부하와 성능을 고려해서 설정해야한다.