[JPA 활용 2편] 3. 컬렉션 조회 최적화

HJ·2024년 2월 16일
0

JPA 활용 2편

목록 보기
3/4
post-thumbnail
post-custom-banner

김영한 님의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의를 보고 작성한 내용입니다.


[ OrderItem ]

public class OrderItem {
    @Id @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    private int orderPrice; // 주문 가격
    private int count;
}

주문한 상품 정보 조회를 위해 @OneToMany 관계인 Order ➜ OrderItems 에 대한 컬렉션 조회 내용입니다.


1. 엔티티를 직접 노출

@RestController
@RequiredArgsConstructor
public class OrderApiController {
    private final OrderRepository orderRepository;

    @GetMapping("/api/v1/orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName();
            order.getDelivery().getAddress();
            List<OrderItem> orderItems = order.getOrderItems();
            orderItems.forEach(oi -> oi.getItem().getName());
        }
        return all;
    }
}

Lazy 초기화를 통해 Order 에 있는 Member, Delivery, OrderItem 과 OrderItem 에 있는 Item 정보를 함께 출력하도록 합니다. OrderItem 도 Entity 이기 때문에 forEach() 를 통해 초기화하는 과정이 필요합니다.

Hibernate5Module 이 스프링 빈으로 등록되어 있고, 기본 설정 자체가 Lazy 설정된 객체들은 프록시 객체이기 때문에 데이터를 뿌리지 않습니다. 강제 초기화하게 되면 데이터가 존재하게 되기 때문에 데이터가 뿌려지게 됩니다.

추가로 양방향 관계인 Member, Delivery, OrderItem 에서 Order 에 대한 참조에 @JsonIgnore 를 붙여주고, OrderItem 과 Item 은 단방향 관계이기 때문에 필요하지 않습니다.




2. 엔티티를 DTO로 변환

[ OrderApiController ]

@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
    List<Order> orders = orderRepository.findAllByString(new OrderSearch());
    return orders.stream().map(order -> new OrderDto(order)).toList();
}

이전 게시글과 마찬가지고 Entity 를 받아서 필요한 정보만을 가진 DTO 를 생성하여 반환합니다.


2-1. 잘못된 DTO

@Data
static class OrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    private List<OrderItem> orderItems;

    public OrderDto(Order order) {
        orderId = order.getId();
        name = order.getMember().getName();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress();
        order.getOrderItems().forEach(oi -> oi.getItem().getName());
        orderItems = order.getOrderItems();
    }
}

OrderItem 은 DTO 안에 있다고 해도 Entity 이기 때문에 따로 분리해서 OrderItem 도 DTO 로 변환해야 합니다.


2-2. 올바른 DTO

[ OrderDto ]

@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();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress();
        orderItems = order.getOrderItems()
                    .stream().map(oi -> new OrderItemDto(oi)).toList();
    }
}

[ OrderItemDto ]

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

OrderDto 에서 Order Entity 를 받고, OrderItem Entity 를 뽑아내서 OrderItemDto 로 전달하여 Entity 를 DTO 로 변환합니다.


2-3. 문제점

이전 게시글과 마찬가지로 지연 로딩이 많아서 너무 많은 수의 SQL 이 실행됩니다.

  • Order 조회
    • Member 조회 ( Order 의 조회 수 만큼 )
    • Delivery 조회 ( Order 의 조회 수 만큼 )
    • OrderItem 조회 ( Order 의 조회 수 만큼 )
      • Item 조회 ( OrderItem 의 조회 수 만큼 )




3. 패치 조인 최적화

3-1. 개발 및 확인

[ OrderApiController ]

@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
    List<Order> orders = orderRepository.findAllWithItem();
    return orders.stream().map(order -> new OrderDto(order)).toList();
}

[ OrderRepository ]

public List<Order> findAllWithItem() {
    return em.createQuery(
            "select 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();
}

[ Join 확인 ]

Order 에는 2개의 데이터가 있고, OrderItem 은 4개의 데이터가 있습니다. 조인하면 1 : N 이기 때문에 4개의 데이터가 됩니다. 즉, Order 조회 결과가 2개에서 4개로 증가되는 것입니다.

스프링부트 3 버전부터는 Hibernate 6 버전을 사용하고 있습니다. Hibernate 6 버전은 패치 조인 사용 시 distinct 를 적용하지 않아도 자동으로 중복 제거를 하도록 변경되었다고 합니다.

distinct 는 DB 에 distinct 를 날려줌과 동시에 엔티티가 중복인 경우에 중복을 걸러서 컬렉션에 담아주게 됩니다. 즉, order 가 컬렉션 패치 조인 때문에 중복 조회 되는 것을 막아줍니다.

그래서 API 를 호출해보면 4개의 데이터가 아닌 2개의 데이터만 출력되는 것을 확인할 수 있습니다. 참고로 패치 조인으로 인해 쿼리는 한 번만 실행됩니다.


3-2. 주의점

3-2-1. 컬렉션 패치 조인 제한

컬렉션 패치 조인( 1 : N 조인 )은 1개만 사용할 수 있습니다. 컬렉션 둘 이상에 패치 조인을 사용하면 안됩니다. 1 : M : N 이 되어버려 데이터가 부정합하게 조회될 수도 있습니다.


3-2-2. 페이징 불가능

컬렉션에 패치 조인을 사용하면 페이지이 불가능한데 컬렉션을 패치 조인하면 1 : N 조인이 발생해서 데이터가 예측할 수 없이 증가하게 됩니다.

1 : N 에서 1 을 기준으로 페이징을 하는 것이 목적인데 N 을 기준으로 row 가 생성되어 데이터가 늘어갑니다.

예시로 보자면 Order 를 기준으로 페이징 하고 싶은데, N 에 해당하는 OrderItem 을 조인하면 OrderItem 이 기준이 되어버립니다.

또 하이버네이트는 이러한 경우 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버려 최악의 경우 장애가 발생합니다.




4. 페이징과 한계 돌파

컬렉션을 패치조인을 사용했을 때 쿼리 한 번으로 모든 데이터를 가져오지만 페이징이 불가능하기 때문에 이를 해결하는 방법을 제시합니다.

4-1. 해결 방법

  1. @xToOne( OneToOne, ManyToOne ) 관계를 모두 패치조인합니다. xToOne 관계는 row 수를 증가시키지 않으므로 페이징에 영향을 주지 않습니다.

  2. 컬렉션은 지연로딩으로 조회합니다.

  3. 지연로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size, @BatchSize 를 적용합니다.

    • 해당 옵션을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회합니다.

    • hibernate.default_batch_fetch_size : 글로벌 설정

    • @BatchSize : 특정 엔티티에 세팅, 개별 최적화


4-2. xToOne 패치 조인

[ OrderApiController ]

@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();   
        return orders.stream().map(order -> new OrderDto(order)).toList();
}

[ OrderRepository ]

public List<Order> findAllWithMemberDelivery() {
    return em.createQuery(
                "select o from Order o " +
                "join fetch o.member m " +
                "join fetch o.delivery d", Order.class)
            .getResultList();
}

findAllWithMemberDelivery()fetch join 으로 Member 와 Delivery 만을 가지고 오고, OrderItem 에 대한 처리가 되지 않습니다.

OrderDto 를 생성하면서 내부에서 지연 로딩이 발생한 OrderItems 를 프록시를 초기화하면서 데이터를 가져오는데 이때 하나의 OrderItem 에 대한 정보를 가지고 옵니다.

이 OrderItem 에는 두 개의 Item 이 존재하고, OrderItemDto 에서 Item 을 각각 가지고 오게 되어 또 2번의 쿼리가 실행되게 됩니다.

OrderItems 가 2개이기 때문에 위의 과정이 또 반복됨으로써 Order, Member, Delivery 를 패치조인하는 처음 1번 + OrderItem 1번 + Item 2번 + OrderItem 1번 + Item 2번 해서 총 7번의 쿼리가 실행됩니다.


4-3. xToOne 에 페이징 적용

[ 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) {
        // Order, Member, Delivery 패치조인
        List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);   
        // OrderItem, Item 을 가져오는 코드
        return orders.stream().map(order -> new OrderDto(order)).toList();
}

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

Order 입장에서 살펴보면 Member 와 Delivery 가 @xToOne 관계이기 때문에 패치조인을 걸어도 되며, 페이징 처리도 가능합니다.

만약 Delivery 가 다른 Entity 와 @xToOne 관계여도 데이터가 증가하지 않기 때문에 패치조인을 걸어도 됩니다.


[ 쿼리 로그 ( 처음 패치조인 쿼리 ) ]

select 
    o1_0.order_id,
    d1_0.delivery_id,
    m1_0.member_id
    ...
from orders o1_0 
join member m1_0 
    on m1_0.member_id=o1_0.member_id 
join delivery d1_0 
    on d1_0.delivery_id=o1_0.delivery_delivery_id 
offset 1 rows 
fetch first 50 rows only;

위의 SQL 은 localhost:8080/api/v3.1/orders?offset=1&limit=50 으로 요청을 보냈을 떄 동작하는 쿼리인데 살펴보면 파라미터로 넘겨준 offset 값과 limit 값이 잘 적용된 것으로 보아 Order, Member, Delivery 에 대해서는 페이징이 적용된 것을 확인할 수 있습니다.

그래도 아직 OrderItem 과 Item 까지 조회하는 것을 위해 쿼리가 7번이나 실행 되는 것은 동일합니다.


4-4. 컬렉션 페이징 적용 - 글로벌 설정

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

위의 속성을 yml 에 설정하면 7번( 처음 1번, OrderItem 2번, Item 4번 ) 수행되던 쿼리가 3번의 실행으로 확 줄어들게 됩니다. 처음 패치 조인은 바로 위에 있으니 참고하면 됩니다.

[ 설정 전 쿼리 ]

-- OrderItem 쿼리
select 
	oi1_0.order_id
	oi1_0.order_item_id
	oi1_0.count
	oi1_0.item_id
	oi1_0.order_price 
from order_item oi1_0 
where oi1_0.order_id = 1;

-- Item 쿼리
select 
	i1_0.item_id
	i1_0.dtype
	i1_0.name
	i1_0.price
	i1_0.stock_quantity
	...
from item i1_0 
where i1_0.item_id = 1;

위의 쿼리는 설정 하기 전, OrderItem 과 Item 을 가져오는 쿼리 로그입니다.
쿼리를 보면 where 절에 = 을 사용해서 데이터를 하나씩 가져오는 것을 확인할 수 있습니다.


[ 설정 후 쿼리 ]

-- OrderItem 쿼리
select 
	oi1_0.order_id
	oi1_0.order_item_id
	oi1_0.count
	oi1_0.item_id
	oi1_0.order_price 
from order_item oi1_0 
where oi1_0.order_id in (1, 2, NULL ...);

-- Item 쿼리
select 
	i1_0.item_id
	i1_0.dtype
	i1_0.name
	i1_0.price
	i1_0.stock_quantity
	...
from item i1_0 
where i1_0.item_id in (1, 2, 3, 4, NULL ...);

OrderItem 을 조회하는 쿼리를 보면 where 절 안에 IN 으로 해서주문 번호가 들어있는 것을 확인할 수 있습니다. 이 쿼리로 인해 userA 의 OrderItem 과 userB 의 orderItem 을 한 번에 가져온 것입니다.

4-3 에 있는 Order, Member, Delivery 를 패치조인쿼리에서 orderId 를 가져오기 때문에 이를 이용하여 IN 쿼리를 날릴 수 있게 되는 것입니다.

default_batch_fetch_size 가 바로 IN 쿼리에 몇 개의 데이터를 넣을 것인지를 말합니다. 예를 들어, 현재 데이터가 100개가 있는데 10개로 설정하면 IN 쿼리가 10번 날라가게 됩니다.

DB에 Order 가 2개 밖기 없기 때문에 첫 번째 쿼리에서 1, 2 를 제외하고 NULL 이 들어가는 것을 확인할 수 있습니다.


참고로 findAllWithMemberDelivery() 의 쿼리를 select o from Order o 만 작성한 경우, Member 와 Delivery 를 가져올 때도 IN 쿼리로 가져오게 됩니다.


4-5. 컬렉션 페이징 적용 - 개별 설정

[ Order ]

public class Order {
    ...
    @BatchSize(size = 100)
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
}

컬렉션에 적용할 때는 필드에 적용합니다.

[ Item ]

@BatchSize(size = 100)
public abstract class Item { 
    ...
}

xToOne 관계일 때는 클래스 레벨에 적용합니다.


4-6. 차이점 및 정리

4-6-1. 차이점

모든 것을 패치조인하는 4-2의 경우, 쿼리는 한 번 나가지만 전부 조인을 하기 때문에 1 : N 조인으로 인해 Order 데이터의 중복이 발생합니다.

4-4의 경우, 세 개의 쿼리 모두 중복이 없도록 쿼리가 수행되어 데이터 전송량이 줄어들게 됩니다.


4-6-2. 정리

일반적인 쿼리를 사용하면 N + 1 문제로 인해 쿼리의 수가 너무 많다
➜ 패치조인으로 인해 한 번의 쿼리만으로 해결 가능
➜ 컬렉션 조회의 경우 패치조인을 사용하면 페이징 처리가 불가능
➜ 설정을 지정해서 IN 쿼리가 사용되도록 변경한다

@xToOne 관계는 패치조인을 해도 페이징에 영향을 주지 않기 때문에 패치조인을 사용해서 쿼리 수를 줄인다.




5. JPA 에서 DTO 직접 조회

[ OrderQueryDto ]

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

JPQL 에서 entity 나 collection 을 바로 넣을 수 없기 때문에 생성자에 컬렉션은 존재하지 않습니다.

OrderQueryDto 객체를 생성한 후에 직접 OrderItemQueryDto 를 생성해서 리스트를 초기화해주는 방식을 사용해야 합니다.

[ OrderItemQueryDto ]

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

[ OrderApiController ]

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

[ OrderQueryRepository ]

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findOrderQueryDtos() {
        List<OrderQueryDto> result = findOrders();
        // OrderQueryDto 의 컬렉션에 값을 채우기 위한 과정
        result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }

    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
                "select new jpabook.jpashop.api.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();
    }

    private List<OrderItemQueryDto> findOrderItems(Long orderId) {
        return em.createQuery(
                        "select new jpabook.jpashop.api.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();
    }
}

findOrders() 에서 Order 를 조회하면서 컬렉션을 제외한 Member 와 Delivery 를 조회하고 OrderQueryDto 를 반환합니다.

findOrderItems() 에서 컬렉션은 OrderItem 을 조회합니다. 이때 @xToOne 관계인 Item 과 조인을 해서 조회하고, OrderItemQueryDto 를 반환합니다.

findOrderQueryDtos() 에서 OrderQueryDto 를 반복하면서 초기화되지 않은 컬렉션 객체를 OrderItemQueryDto 로 채우게 됩니다.

즉, @xToOne 관계는 row 수가 증가하지 않아 조인으로 최적화 하기 쉬우므로 한번에 조회하고, @xToMany 관계는 최적화 하기 어려우므로 별도의 메서드로 조회합니다.


[ 쿼리 로그 ]

-- Order 1 에 대한 OrderItem, OrderItem 에 대한 Item 조회
select 
	oi1_0.order_id
	i1_0.name
	oi1_0.order_price
	oi1_0.count 
from order_item oi1_0 
join item i1_0 
	on i1_0.item_id=oi1_0.item_id 
where oi1_0.order_id=1;

-- Order 2 에 대한 OrderItem, OrderItem 에 대한 Item 조회
select 
	oi1_0.order_id
	i1_0.name
	oi1_0.order_price
	oi1_0.count 
from order_item oi1_0 
join item i1_0 
	on i1_0.item_id=oi1_0.item_id 
where oi1_0.order_id=2;

이전과 다르게 IN 쿼리로 OrderItem 을 한 번에 조회하고, Item 을 한 번에 조회하는 것이 아닌 하나의 OrderItem 을 조회하면서 @xToOne 관계인 Item 도 함께 조회합니다.

이렇게 해서 처음 1번 + 컬렉션 2번( Order 가 2개이므로 OrderQueryDto 가 2개 )을 해서 총 3번의 쿼리가 수행됩니다. 즉. N + 1 문제가 발생한 것입니다.




6. DTO 직접 조회 - 컬렉션 조회 최적화

[ OrderApiController ]

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

[ OrderQueryRepository ]

public List<OrderQueryDto> findAllByDto_optimization() {
    List<OrderQueryDto> result = findOrders();
    List<Long> orderIds = result.stream().map(o -> o.getOrderId()).toList();
    // IN 을 이용해서 쿼리를 한 번에 날림
    List<OrderItemQueryDto> orderItems = em.createQuery(
                        "select new jpabook.jpashop.api.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 으로 변환 / key = orderId
    Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream().collect(Collectors.groupingBy(OrderItemQueryDto -> OrderItemQueryDto.getOrderId()));
    // result 값 세팅
    result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
    return result;
}

IN 쿼리를 사용하기 위해 findOrders() 의 결과에서 orderId 만을 뽑아냅니다. 이를 OrderItem 과 Item 을 조회하는 쿼리에 전달하여 IN 쿼리로 한 번에 조회될 수 있도록 합니다.

그 후 OrderQueryDto 의 컬렉션에 매칭될 수 있도록 조회 결과를 Map 으로 변환하고, OrderQueryDto 를 반복하면서 OrderItemQueryDto 를 매핑합니다.


[ 쿼리 로그 ]

select 
	oi1_0.order_id	
	i1_0.name
	oi1_0.order_price
	oi1_0.count 
from order_item oi1_0 
join item i1_0 
	on i1_0.item_id=oi1_0.item_id 
where oi1_0.order_id in (1, 2);

5번에서 Order 의 수만큼 반복되던 쿼리를 IN 쿼리를 이용해서 한 번의 쿼리로 조회되도록 줄일 수 있습니다. 그래서 Order, Member, Delivery 를 조회하는 쿼리 1번, OrderItem, Item 을 조회하는 쿼리 1번 총 2번의 쿼리가 실행됩니다.




6. DTO 직접 조회 - 플랫 데이터 최적화

[ OrderFlatDto ]

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

조회할 때 필요한 정보를 한 번에 가지고 있는 새로운 DTO 를 생성합니다.

[ OrderQueryRepository ]

public List<OrderFlatDto> findAllByDto_flat() {
    return em.createQuery(
                "select new jpabook.jpashop.api.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();
}

모든 테이블을 join 해서 한 번에 데이터를 가져오기 때문에 쿼리는 한 번만 실행됩니다.

[ OrderQueryDto ]

@EqualsAndHashCode(of = "orderId")
public class OrderQueryDto {
    ...
    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;
    }
}

OrderItem 을 가진 OrderQueryDto 를 한 번에 생성하기 위한 생성자와, 객체를 묶어줄 때 이를 구분할 수 있도록 해주는 @EqualsAndHashCode 를 추가합니다.

[ OrderApiController ]

@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
    @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());
    }
}

OrderFlatDto 는 하나에 조회하고자 하는 정보가 전부 들어있습니다. 이를 그대로 반환하면 중복 데이터가 많아지기 때문에 OrderQueryDto 로 변환합니다.

쿼리는 한 번만 날라가지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 5번 보다 더 느릴 수 도 있으며 페이징이 불가능합니다.

post-custom-banner

0개의 댓글