[JPA 활용2] API 개발 고급 - 컬렉션 조회 최적화 ①

kiteB·2021년 11월 25일
1

JPA

목록 보기
26/28
post-thumbnail

지금까지는 xxxtoOne(OneToOne, ManyToOne) 관계만 있었는데,
이번에는 컬렉션인 일대다 관계(OneToMany)를 조회하고 최적화하는 방법을 알아보자!

[ 주문 조회 V1: 엔티티 직접 노출 ]

OrderApiController

@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 강제 초기화

            List<OrderItem> orderItems = order.getOrderItems();
            orderItems.stream().forEach(o -> o.getItem().getName());    //Lazy 강제 초기화
        }
        return all;
    }

}
  • orderItem, item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성한다.
  • 양방향 연관관계인 경우, 무한 루프에 걸리지 않게 한곳에 @JsonIgnore를 추가해야 한다.
  • V1은 엔티티를 직접 노출하므로 좋은 방법이 아니다!!

실행 결과


[ 주문 조회 V2: 엔티티를 DTO로 변환 ]

OrderApiController

@RestController
@RequiredArgsConstructor
public class OrderApiController {

    private final OrderRepository orderRepository;

    ...

    /**
     * V2. 엔티티를 DTO로 변환
     */
    @GetMapping("/api/v2/orders")
    public List<OrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<OrderDto> collect = orders.stream()
                .map(o -> new OrderDto(o))
                .collect(toList());

        return collect;
    }

    @Getter
    static class OrderDto {

        private Long orderId;
        private String name;
        private LocalDateTime orderDate;    //주문시간
        private OrderStatus orderStatus;
        private Address address;
        private List<OrderItemDto> orderItems;  //Dto 변환 필수!!

        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());
        }
    }

    @Getter
    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();
        }
    }
}
  • 지연 로딩으로 너무 많은 SQL 실행된다.
    • order 1번
    • member, address N번 (order 조회 수 만큼)
    • orderItem N번 (order 조회 수 만큼)
    • item N번 (orderItem 조회 수 만큼)

실행 결과

🚫 주의

Dto 안에는 엔티티가 있으면 안된다.
OrderDto 안에 있는 orderItems 조차도 Dto로 변환해주어야 한다.


[ 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화]

OrderApiController

@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;
}

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();
}
  • 페치 조인으로 SQL이 1번만 실행된다.
  • distinct를 사용하는 이유?
    • 1대다 조인이 있으므로 데이터베이스 row가 증가한다. 이로 인해 같은 order 엔티티의 조회 수도 증가하게 된다.
    • JPA의 disctinct는 SQL에 distinct를 추가하고, 더해서 같은 엔티티가 조회되면, 애플리케이션에서 중복을 걸러준다. 즉, order가 조인 때문에 중복 조회되는 것을 막아준다.
  • 단점: 컬렉션 페치 조인을 사용하면 페이징이 불가능
    • 조회할 데이터가 적을 때는 문제가 없지만, 데이터가 커지게 되면 out of memory 예외가 발생할 수도 있고 매우 치명적이다.

실행 결과

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글