[실전! 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] API 개발 고급(3): 컬렉션 조회 최적화(2)

2024. 8. 27. 16:29·Back-End/JPA

[작성일: 2023. 10. 05]

 

컬렉션 조회 최적화(2)

주문 조회 V4: JPA에서 DTO 직접 조회

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderRepository orderRepository;
    private final OrderQueryRepository orderQueryRepository;

    // ... 코드생략

    @GetMapping("/api/v4/orders")
    public List<OrderQueryDto> ordersV4(){
        return orderQueryRepository.findOrderQueryDtos();
    }
}

 

@Data
public class OrderQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDto> orderItems;

    public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
}

 

@Data
public class OrderItemQueryDto {
    private Long orderId;
    private String itemName;
    private int orderPrice;
    private int count;

    public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) {
        this.orderId = orderId;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
    }
}

 

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findOrderQueryDtos() {
        List<OrderQueryDto> result = findOrders();
        result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }

    private List<OrderItemQueryDto> findOrderItems(Long orderId) {
        return em.createQuery(
                "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                        " from OrderItem oi" +
                        " join oi.item i" +
                        " where oi.order.id = :orderId", OrderItemQueryDto.class)
                .setParameter("orderId", orderId)
                .getResultList();
    }

    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address) from Order o" +
                                " join o.member m" +
                                " join o.delivery d", OrderQueryDto.class)
                .getResultList();
    }
}

 

 

Query는 루트 1번, 컬렉션 N번 실행된다.

ToOne(N:1, 1:1) 관계들을 먼저 조회하고 Tomany(1:N) 관계는 각각 별도로 처리한다. 이런 방식을 선택한 이유는 ToOne 관계는 조인해도 데이터 row 수가 증가하지 않지만 ToMany(1:N) 관계는 조인하면 row 수가 증가한다.

row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화 하기 쉬우므로 한 번에 조회하고, ToMany 관계는 최적화 하기 어려우므로 findOrderItems() 같은 별도의 메서드로 조회한다.

 

 

 

 

 

주문 조회 V5: JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderRepository orderRepository;
    private final OrderQueryRepository orderQueryRepository;

    // ... 코드 생략
    
    @GetMapping("/api/v5/orders")
    public List<OrderQueryDto> ordersV5(){
        return orderQueryRepository.findAllByDto_optimization();
    }
}

 

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    // ... 코드 생략

    private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
        List<OrderItemQueryDto> orderItems = em.createQuery(
                        "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                                " from OrderItem oi" +
                                " join oi.item i" +
                                " where oi.order.id in :orderIds", OrderItemQueryDto.class)
                .setParameter("orderIds", orderIds)
                .getResultList();

        Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
                .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));
        return orderItemMap;
    }

    private static List<Long> toOrderIds(List<OrderQueryDto> result) {
        List<Long> orderIds = result.stream().map(o -> o.getOrderId())
                .collect(Collectors.toList());
        return orderIds;
    }
}

 

Query는 루트 1번, 컬렉션 1번 싫애된다.

ToOne 관계들을 먼저 조회하고, 여기서 얻은 식별자 orderId로 ToMany 관계인 OrderItem을 한꺼번에 조회한다. 

MAP을 사용하면 매칭 성능이 향상된다.(O1)

 

 

 

 

 

주문 조회 V6: JPA에서 DTO 직접 조회 - 플랫 데이터 최적화

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderRepository orderRepository;
    private final OrderQueryRepository orderQueryRepository;

    // ... 코드 생략
    
    @GetMapping("/api/v6/orders")
    public List<OrderQueryDto> ordersV6(){
        List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();

        return flats.stream()
                .collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(),
                                o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
                        mapping(o -> new OrderItemQueryDto(o.getOrderId(),
                                o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
                )).entrySet().stream()
                .map(e -> new OrderQueryDto(e.getKey().getOrderId(),
                        e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(),
                        e.getKey().getAddress(), e.getValue()))
                .collect(toList());
    }
}

 

@Data
public class OrderFlatDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;

    private String itemName;
    private int orderPrice;
    private int count;

    public OrderFlatDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, String itemName, int orderPrice, int count) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
    }
}

 

@Data
@EqualsAndHashCode(of = "orderId")
public class OrderQueryDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItemQueryDto> orderItems;

    public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }

    public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address, List<OrderItemQueryDto> orderItems) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
        this.orderItems = orderItems;
    }
}

 

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    // ... 코드 생략
    
    public List<OrderFlatDto> findAllByDto_flat() {
        return em.createQuery(
                "select new" +
                        " jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
                        " from Order o" +
                        " join o.member m" +
                        " join o.delivery d" +
                        " join o.orderItems oi" +
                        " join oi.item i", OrderFlatDto.class)
                .getResultList();
    }
}

 

 

이 방법은Query가 한 번만 실행된다는 장점이 있지만 단점도 있다.

조인으로 인해  DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 V5 방법보다 더 느릴 수도 있다.

또한 애플리케이션에서 추가 작업이 크며, 페이징 처리가 불가능하다.

 

 

 

 

 

API 개발 고급 정리

엔티티 조회

  • 엔티티를 조회해서 그대로 반환(V1)
  • 엔티티 조회 후 DTO로 반환(V2)
  • 페치 조인으로 쿼리 수 최적화(V3)
  • 컬렉션 페이징과 한계 돌파(V3.1)
    • 컬렉션은 페치 조인 시 페이징이 불가능하다.
    • ToOne 관계는 페치 조인으로 쿼리 수를 최적화한다.
    • 컬렉션은 페치 조인 대신에 지연 로딩을 유지하고, hibername.default_batch_fetch_size나  @BatchSize로 최적화한다.

 

 

DTO 직접 조회

  • JPA에서 DTO 직접 조회(V4)
  • 컬렉션 조회 최적화: 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화(V5)
  • 플랫 데이테 최적화: JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환(V6)

 

 

 

권장 순서

  1. 엔티티 조회 방식으로 우선 접근한다.
    1. 페치 조인으로 쿼리 수 최적화
    2. 컬렉션 최적화
      1. 페이징 필요 시 hibernate.default_batch_fetch_size, @BatchSize로 최적화
  2. 엔티티 조회 방식으로 해결이 안 되면 DTO 조회 방식을 사용한다.
  3. DTO 조회 방식으로 해결이 안 되면 NativeSQL이나 스프링 JdbcTemplate를 사용한다.

 

 

참고:
엔티티 조회 방식은 페치 조인이나 hibernate.default_batch_fetch_size, @BatchSize 같이 코드를 거의 수정하지 않고 옵션만 약간 변경해서 다양한 성능 최적화를 시도할 수 있다. 반면에 DTO를 직접 조회하는 방식은 성능을 최적화하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.

엔티티 조회 방식은 JPA가 많은 부분을 최적화해주기 때문에 단순한 코드를 유지하면서 성능을 최적화 할 수 있다. 반면에 DTO 조회 방식은 SQL을 직접 다루는 것과 유사하다.

 

 

 

 

 

 

 

 

 

 


🐣 출처: 인프런 김영한님 강의

 

이 글은 인프런의 김영한님 JPA 강의를 보고 작성한 글입니다.

강의를 들으면서 정리한 글이므로 틀린 내용이나 오타가 있을 수 있습니다.

 

저작자표시 비영리 변경금지 (새창열림)
'Back-End/JPA' 카테고리의 다른 글
  • [실전! 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] 스프링 데이터 JPA와 Querydsl 맛보기
  • [실전! 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] API 개발 고급(4): OSIV와 성능 최적화
  • [실전! 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] API 개발 고급(3): 컬렉션 조회 최적화(1)
  • [실전! 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] API 개발 고급(2): 지연 로딩과 조회 성능 최적화
뚜비
뚜비
1년차 백엔드&iOS 개발자의 감자 탈출 블로그 🥔🥔
  • 뚜비
    뚜비의 개발로그
    뚜비
  • 전체
    오늘
    어제
  • 글쓰기     관리
    • Devlog
      • Back-End
        • Java
        • Spring
        • JPA
        • HTTP
        • Security
        • Back-End
        • Front-End
      • 알고리즘
      • iOS
        • Swift
      • Database
      • Tips
        • Git & GitHub
        • A to Z
      • 프로젝트
      • 생각정리
  • 태그

    jsp
    변수
    html
    자바
    성능최적화
    DB
    김영한
    프로그래머스
    Java
    JPA
    HTTP
    객체
    스프링
    의존성주입
    Database
    Security
    데이터베이스
    생성자
    알고리즘
    Spring Security
    게시판만들기
    최주호
    자바스크립트
    백준
    sql
    MVC
    Swift
    spring
    다형성
    javascript
  • hELLO· Designed By정상우.v4.10.0
뚜비
[실전! 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화] API 개발 고급(3): 컬렉션 조회 최적화(2)
상단으로

티스토리툴바