실전! 스트링 부트와 JPA 활용 2 - 컬렉션 조회 최적화 2

이태휘·2022년 11월 8일
0
post-custom-banner

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

-> 컬렉션이 있을 때 하는 방법 다루기
-> OrderQueryDto 를 OrderQueryRepository에 구현함

  • Repository/order/query/OrderQueryRepository.class
    -> 이렇게 패키지를 나눈 이유는 오더리포지토리는 오더 엔티티를 조회하는 용도고, 쿼리쪽은 화면이나 API에 의존관계 있는 애들을 쓸때
    -> 즉, 엔티티는 리포지토리, 엔티티아닌 특정 쿼리는 여기로!
    -> 화면관련된건 쿼리랑 밀접하기때문에 중요한 핵심 비즈니스 로직이랑 화면 API랑 분리해서 다룰려고 이렇게 하는 것임.

1) repository/order/query 안

  • OrderItemQueryDto
package jpabook.jpashop.repository.order.simplequery;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;

import java.time.LocalDateTime;

@Data
public class OrderSimpleQueryDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate; //주문시간
    private OrderStatus orderStatus;
    private Address address;


    public OrderSimpleQueryDto(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;
    }
}
  • OrderQueryDto
package jpabook.jpashop.repository.order.query;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;

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

@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;
    }
}
  • OrderQueryRepository
package jpabook.jpashop.repository.order.query;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    //이렇게 따로만들어준 이유가
    //만약 api안에있는 orderDto쓰면 리포지토리가 컨트롤러에 의존하는 순환관계발생해
    //본인이 만드는 애여서 그냥 같은 패키지에 넣음
    public List<OrderQueryDto> findOrderQueryDtos(){
        List<OrderQueryDto> result = findOrders();
        //이제 루프를 돌려서 orderitems에서 받아와
        //컬렉션부분은 루프 돌면서 직접 채워주기
        result.forEach(o -> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }

    //1대다 해결하려고 이렇게함
    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() {
        //new 오퍼레이션쓰면 플랫하게 데이터를 한줄밖에 못넣어서 리스트를 매개변수로 못넣음
        //orderitmes는 1대다여서 데이터 뻥튀기돼서 한번에 못넣어서 다음에 넣어줘
        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();
    }
}

2) v4함수

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

컬렉션이랑 디티오랑 다르게! 아이템때문에 따로 가져오려고 orderItemQueryDto 만들어준것임

[
    {
        "orderId": 4,
        "name": "userA",
        "orderDate": "2022-11-08T17:33:05.845134",
        "orderStatus": "ORDER",
        "address": {
            "city": "서울",
            "street": "1",
            "zipcode": "1111"
        },
        "orderItems": [
            {
                "orderId": 4,
                "itemName": "Jpa1 Book",
                "orderPrice": 10000,
                "count": 1
            },
            {
                "orderId": 4,
                "itemName": "Jpa2 Book",
                "orderPrice": 20000,
                "count": 2
            }
        ]
    },
    {
        "orderId": 11,
        "name": "userB",
        "orderDate": "2022-11-08T17:33:05.87042",
        "orderStatus": "ORDER",
        "address": {
            "city": "진주",
            "street": "2",
            "zipcode": "2222"
        },
        "orderItems": [
            {
                "orderId": 11,
                "itemName": "Spring1 Book",
                "orderPrice": 10000,
                "count": 1
            },
            {
                "orderId": 11,
                "itemName": "Spring2 Book",
                "orderPrice": 20000,
                "count": 2
            }
        ]
    }
]

-> 잘 나온다!

-> 단점이 쿼리가 오더한번, 오더아이템 2개해서 총 3번
-> 즉, n+1 번 나가! 루프때문에 그런거임. 컬렉션은 루프로 직접 넣어줘서!

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

  • OrderQueryRepository 안
public List<OrderQueryDto> findAllByDto_optimization() {
        //오더가져오는것까지는 동일해서 findOrders()는 그대로 사용함
        List<OrderQueryDto> result = findOrders();
        //orderIds는 orderQueryDto에서 나온걸 stream으로 아이디만 다 뽑는것
        //주문가져온걸 스트림으로 아이디들을 리스트로 받아
        List<Long> orderIds = result.stream()
                        .map(o -> o.getOrderId())
                        .collect(Collectors.toList());

        //V4의 단점은 루프를 도는건데, 이번엔 한번에 가져옴
        //아이디를 하나씩가져오는게아니라 in절로 한번에 가져오기
        //오더아이디 리스트를 파라미터 인자로 넣어서 오더아이템 관련된거 뽑혀져 나올것임
        //쿼리한번으로 오더아이템 해결!
        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();
        //최적화하기
        //쿼리한번 날리고 메모리에서 매칭해서 값 세팅해주기
        //이렇게하면 쿼리가 총 두번나가 findOrders랑 위의 쿼리문
        java.util.Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
                .collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
        result.forEach(o->o.setOrderItems(orderItemMap.get(o.getOrderId())));

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

-> 얘도 잘나가!
-> 쿼리가 두번만 나가! 맨처음 findOrders()랑 그 뒤에 아이디로 애들 다 데려오기
-> in query를 써서 id랑 관련된것들 다 가져오고 메모리에서 매치시킨것
-> 리팩토링해서 더 가시성좋게 함수로 만들어서 사용도 가능!

-> 페이징 불가능하다는 단점 존재!

직접 jpql 로 작성하는게 번거로운데, 패치조인보다 sql이 적다는 이점 존재!

  • 다음 시간은 쿼리 한번으로 해결하기 다룰 것

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

매번하는 new operation은 쿼리dsl 쓰면 정말 간단하게 해결 가능!

  • OrderFlatDto
//이렇게하면 큰 쿼리 1개만 나가게돼
    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();
    }
  • API
@GetMapping("/api/v6/orders")
    public List<OrderFlatDto> ordersV6(){
        return orderQueryRepository.findAllByDto_flat();
    }

-> 이렇게하면 중복을 포함해서 반환이 돼

쿼리가 1번만 나갔다는 장점, 페이징을 할 수 있다는 장점도잇음
-> 벗 오더를 기준으로 페이징은안돼 orderitems기준으로돼
-> 따라서 제대로 페이징 못하는 단점!
-> flatDto 스펙으로 와서 orderQueryDto로 스펙이 안맞아

  • 중복없이 OrderQueryDto로 하기
@GetMapping("/api/v6/orders")
    public List<OrderFlatDto> 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());
    }

@EqualsAndHashCode(of = "orderId") 이거 붙이면 오더아이디 기준으로 묶어서 중복 제거해줌

-> 오더를 기준으론 페이징이 안된다는 단점! 디비에서는 데이터가 중복돼서!

API 개발 고급 정리

profile
풀스택 개발자 가보자구~
post-custom-banner

0개의 댓글