
주문내역에서 추가로 주문한 상품 정보를 추가로 조회해보자🙂
Order 기준으로 컬렉션인 OrderItem 와 Item 이 필요하다!
앞의 예제에서는
XtoOne(OneToOne, ManyToOne)관계만 있었다.
이번에는 컬렉션인 일대다 관계(OneToMany)를 조회하고, 최적화하는 방법을 알아보도록 하자☺️
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리 *
* - 양방향 관계 문제 발생 -> @JsonIgnore
*/
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); // Lazy 강제 초기화
order.getDelivery().getAddress(); // Lazy 강제 초기화
// 내가 알고싶은 정보는 orderItem과 그 안에 있는 Item의 정보!
// 강제 초기화 필요
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName());
}
return all;
}
}
orderItem , item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성한다.@JsonIgnore 를 추가해야 한다!
➡️ 엔티티가 직접 노출되는 것을 확인
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
// Dto로 변환
.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();
}
}
order 1번member , address N번(order 조회 수 만큼) orderItem N번(order 조회 수 만큼)item N번(orderItem 조회 수 만큼)
➡️orderItems 부분에 Dto가 잘 적용된 것을 확인
📌 참고
- 지연 로딩은 영속성 컨텍스트에 있으면 영속성 컨텍스트에 있는 엔티티를 사용하고 없으면 SQL을 실행함
- 따라서 같은 영속성 컨텍스트에서 이미 로딩한 회원 엔티티를 추가로 조회하면 SQL을 실행하지 않음
일대다기 때문에, 데이터가 예측할 수 없이 '다'로 증가가 될 수 있다🧐
➡️ 예를 들어 Order와 Item을 조인 하려고 한다면, Item 개수가 더 많기 때문에 DB 입장에서 조인을 하면 Item 기준으로 여러개가 조인이 됨!
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
// distinct : 쿼리 중복 제거, (같은 pk값이면 중복 제거해줌)
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();
}


페치 조인으로 SQL이 1번만 실행됨!
distinct
order 엔티티의 조회 수도 증가하게 된다. distinct는 SQL에 distinct를 추가하고, 더해서 같은 엔티티가 조회되면, 애플리케이션에서 중복을 걸러준다!! order가 컬렉션 페치 조인 때문에 중복 조회 되는 것을 막아줌단점 : 페이징이 불가능해진다🥲
📌 참고
- 컬렉션 페치 조인을 사용하면 페이징이 불가능하다.
- 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다(매우 위험)
💡 주의
- 컬렉션 페치 조인은 1개만 사용할 수 있다.
- 컬렉션 둘 이상에 페치 조인을 사용하면, 데이터가 부정합하게 조회될 수 있으니 조심!
OrderItem을 조인하면 OrderItem이 기준이 되어버린다ㅠ🤔 : 그럼 페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야해❓
🤗 : 지금부터 코드도 단순하고, 성능 최적화도 보장하는 매우 강력한 방법을 소개해줄게^^ 대부분의 페이징 + 컬렉션 엔티티 조회 문제는 이 방법으로 해결할 수 있다구~ㅎ_
1️⃣ 먼저 XToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 한다.
XToOne 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.2️⃣ 컬렉션은 지연 로딩으로 조회한다.
3️⃣ 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size , @BatchSize 를 적용한다.
hibernate.default_batch_fetch_size: 글로벌 설정@BatchSize : 개별 최적화
1️⃣ 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) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
2️⃣ 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();
}

➡️ 조회 결과는 이전과 같지만,


➡️ 쿼리는 달라진 것을 확인!!
➡️ 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회
spring: jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
➡️ 개별로 설정하려면 @BatchSize 를 적용하면 된다.
(컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용)
😀 장점
1+N ➡️ 1+1로 최적화 된다.😎 결론
XToOne 관계는 페치 조인해도 페이징에 영향을 주지 않는다. XToOne 관계는 페치조인으로 쿼리 수를 줄이고 해결하고, 나머지는 hibernate.default_batch_fetch_size 로 최적화 하자!📌 참고
default_batch_fetch_size의 크기는 적당한 사이즈를 골라야 하는데, 100~1000 사이를 선택하는 것을 권장함!
머리가 참 어지럽군요 ㅋ