티스토리 뷰

먼저 알아야 할 것이 있다. 패키지 구조를 봤을 때에 같은 order 영역에서도 repository를 나눈 것을 확인 할 수 있다.

- entity를 조회하거나 직접 저장하는 경우에는 일반적인 orderRepository를 쓴다.

- 화면에 따라 fit하게, 상황에 맞게 쿼리를 정의해야 하는 경우에는 따로 repository(예:OrderQueryRepository)를 만들어 관리한다.

장점 : 화면에 따라 보이는 것과 핵심 비지니스로직을 따로 관리할 수 있다.

 

DTO 직접 조회

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;


    public List<OrderQueryDto> findOrders() {
        return em.createQuery("select new jpabook2.jpashop2.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();
    }
}
@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;
    }
}

 

  1. DTO를 자세히 살펴보면 orderItems는 생성자에 들어 있지 않으며, repository 쿼리에도 포함되어있지 않다.
  2. orderItems는 일대다이며, DTO로 불러올 때에는 row수의 증가를 막기 위해 일단 플랫하게 일대다는 빼고 불러온다.

해결

package jpabook2.jpashop2.repository.order.query;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@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 jpabook2.jpashop2.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();

    }

    public List<OrderQueryDto> findOrders() {
        return em.createQuery("select new jpabook2.jpashop2.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();
    }
}

 

 

- 결국 일대다는 findOrderQuertDtos 메소드에서 보이듯이 orderItem을 먼저 가져온 후에 for문을 돌려 아이템을 각각 가져와 set해주는 방식으로 사용하게 된다.

- 하지만 for문을 사용하는 방식.. 그리고 1개의 쿼리에 대한 일대다 쿼리를 가져와야하는 1 + N의 문제를 가져왔다. 몇백개의 쿼리가 날아갈지 모르는 상황이다. 최적화가 필요하다.

 

최적화

    @GetMapping("/api/v5/orders")
    public List<OrderQueryDto> ordersV5(){
        return orderQueryRepository.findAllByDto_optimization();
    }
package jpabook2.jpashop2.repository.order.query;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@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 jpabook2.jpashop2.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();

    }

    public List<OrderQueryDto> findOrders() {
        return em.createQuery("select new jpabook2.jpashop2.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();
    }

    public List<OrderQueryDto> findAllByDto_optimization() {
        List<OrderQueryDto> result = findOrders();

        List<Long> orderIds = result.stream().map(o -> o.getOrderId()).collect(Collectors.toList());
        Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds);

        result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));

        return result;
    }

    public Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds){
        List<OrderItemQueryDto> orderItems =  em.createQuery(
                "select new jpabook2.jpashop2.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())); // key값은 orderId가 되고 value는 OrderItemQueryDto가 된다.

        return orderItemMap;
    }
}

 

- 마지막 2개의 메소드만 주목해서 보면 된다.

- 먼저 findAllByDto_optimization()을 보면 findOrders로 일단 불러온다. 

- 그 후에 orderId들을 묶은 리스트 orderIds를 사용해 쿼리문에 in 을 이용해서 한방쿼리로 사용한다.

- 그 뒤에 맵으로 key(id값) value 로 전환한 뒤에 for문을 사용해서 매칭 시켜서 객체에 담아준다.

- 코드 상으로는 복잡도가 증가하지만, 성능으로는 1방 쿼리로 가능하기에 많이 최적화 된다.

 

 

 

최적화2

    @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());
        
    }
package jpabook2.jpashop2.repository.order.query;

import jpabook2.jpashop2.domain.Address;
import jpabook2.jpashop2.domain.OrderStatus;
import lombok.Data;

import java.time.LocalDateTime;

@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;
    }
}
    public List<OrderFlatDto> findAllByDto_flat() {

        return em.createQuery("select new jpabook2.jpashop2.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();

    }

 

- 먼저 컨트롤러의 저 복잡한 코드를 요약하자면, flatDTO가 key값이 1의 dto가 되고, 그 키값으로 orderItem을 빼내오면서 키값도 같이 넣어 DTO를 불러온다. 이때 group by의 키 값의 dto에 작성한다.

package jpabook2.jpashop2.repository.order.query;

import jpabook2.jpashop2.domain.Address;
import jpabook2.jpashop2.domain.OrderStatus;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.time.LocalDateTime;
import java.util.List;

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

 

- EqualsAndHashCode(of = "orderId")

- 결국 불러온 flat한 데이터 (그냥 일대다 쿼리로 불러온거다) 지지고 볶아서 만들어 내는 방식

- 이 방법은 쿼리 한방에 처리된다. 하지만 join이 많기 때문에 데이터 양이 많으면 성능이 떨어질 수도 있다.

- 페이징을 할 수 없다.

- 딱히 쓰기에 좋은 방법은 아닌거 같다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함