지연 로딩 @OneToMany 연관관계에 대한 성능 최적화 내용이다.
엔티티 직접 노출
엔티티를 API의 응답 값으로 반환하는 방법이다.
@GetMapping("/api/v1/orders")
public List<Order> orderV1() {
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getMember().getName();
order.getDelivery().getAddress();
List<OrderItem> orderItems = order.getOrderItems();
orderItems.forEach(o -> o.getItem().getName());
}
return orders;
}
@OneToMany 연관 관계인 엔티티의 값을 가져오기 위해서 강제 초기화를 해야 한다.
엔티티를 DTO로 변환
엔티티를 API 스펙에 맞는 DTO로 변환하여 반환하는 방법이다.
@GetMapping("/api/v2/orders")
public List<OrderDto> orderV2() {
List<Order> orders = orderRepository.findAll();
return orders.stream()
.map(OrderDto::new)
.collect(Collectors.toList());
}
private 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(OrderItemDto::new)
.collect(Collectors.toList());
}
}
private 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();
}
}
엔티티를 DTO로 변환하는 과정에서 프록시 초기화가 일어나므로 지연 로딩에 의한 N + 1 문제가 발생한다. (Order 조회 1번, Member 지연 로딩 조회 N번, Delivery 지연 로딩 조회 N번, OrderItem 지연 로딩 조회 N번, Item 지연 로딩 조회 N번(OrderItem 조회 수 만큼))
엔티티를 DTO로 변환 - 페치 조인 최적화
지연 로딩에 의한 N + 1 문제를 방지하기 위해 페치 조인을 사용하는 방법이다.
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();
}
1대다 관계인 테이블을 조인하게 되면 row 수가 증가하므로, 중복 row를 제거하기 위해서 distinct를 추가해야 한다.
페치 조인 사용 시 페이징 불가능
@OneToMany 연관관계에서 페치 조인을 사용하면 페이징이 불가능하다. 만약 페치 조인을 하고 페이징을 사용하려고 하면 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고 메모리에서 페이징을 하게 된다. 이 경우 장애의 위험이 매우 높으니 주의해야 한다.
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
엔티티를 DTO로 변환 - 페이징과 한계 돌파
페치 조인 사용 시 페이징이 불가능하므로, 페이징이 필요한 경우 해결할 수 있는 방법이다.
@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);
return orders.stream()
.map(OrderDto::new)
.collect(Collectors.toList());
}
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();
}
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
- @ManyToOne, @OneToOne 관계는 페치 조인을 하더라도 row 수를 증가시키지 않으므로 모두 페치 조인을 한다.
- 컬렉션은 지연 로딩으로 조회한다.
- 지연 로딩 성능 최적화를 위해 batch size 옵션을 적용한다. (지연 로딩 시 지정 size 만큼 IN 조건으로 조회한다)
- spring.jpa.properties.hibernate.default_batch_fetch_size: 글로벌 설정
- @BatchSize: 개별 설정
정리
- 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
- 페치 조인으로 쿼리 수 최적화
- 컬렉션 최적화
- 페이징 필요: batch size 옵션 사용
- 페이징 필요 없음: 페치 조인 사용
- 그래도 안되면 DTO를 직접 조회하는 방법을 사용한다.
- 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
'Spring' 카테고리의 다른 글
OSIV와 성능 최적화 (0) | 2022.03.25 |
---|---|
JPA 지연 로딩과 조회 성능 최적화 (1) (0) | 2022.03.06 |
Log4j에서 SLF4J + Logback 으로 전환하기 (0) | 2022.02.03 |
@Transactional의 propagation(전파옵션) (0) | 2022.01.24 |
댓글