본문 바로가기
Spring

JPA 지연 로딩과 조회 성능 최적화 (2)

by 아토로 2022. 3. 7.

지연 로딩 @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
  1. @ManyToOne, @OneToOne 관계는 페치 조인을 하더라도 row 수를 증가시키지 않으므로 모두 페치 조인을 한다.
  2. 컬렉션은 지연 로딩으로 조회한다.
  3. 지연 로딩 성능 최적화를 위해 batch size 옵션을 적용한다. (지연 로딩 시 지정 size 만큼 IN 조건으로 조회한다)
    • spring.jpa.properties.hibernate.default_batch_fetch_size: 글로벌 설정
    • @BatchSize: 개별 설정

정리

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
    • 페치 조인으로 쿼리 수 최적화
    • 컬렉션 최적화
      • 페이징 필요: batch size 옵션 사용
      • 페이징 필요 없음: 페치 조인 사용
  2. 그래도 안되면 DTO를 직접 조회하는 방법을 사용한다.
  3. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

2022.03.06 - [Spring] - JPA 지연 로딩과 조회 성능 최적화 (1)

댓글