[스프링 활용2] 주문 조회 V6 : JPA에서 DTO로 직접 조회, 플랫 데이터 최적화

atdawn·2024년 8월 19일

SPRING BOOT+JPA

목록 보기
48/49

참고 : [실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] - 김영한


이번에는 쿼리 1개로 모든 데이터를 조회해보자.

  • DB에서 한번에 다 가져옴
  • Order 와 OrderItem , Item JOIN (flat 하게 한줄로)
  • 이후 어플리케이션에서 맞게 변형

OrderApiController

   @GetMapping("/api/v6/orders")
    public List<OrderFlatDto> ordersV6() {
        return orderQueryRepository.findAllByDto_flat();
    }

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

OrderQueryRepository

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

post맨을 통해 json을 살펴보면

이처럼 orderId나 username이 중복되어 나타난다.

api를 요청하였을때 실행되는 쿼리를 DB에 직접 넣어보면,

이처럼 데이터가 중복으로 생성된다. (join을 사용하였기 때문에)
그렇기 때문이 json은 이 데이터를 그대로 한줄로 뿌린 것이다.

  • 장점은 한번의 쿼리로 데이터를 조회 가능하다.
  • 하지만 페이징이 불가능하다
  • 쿼리는 한번이지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 V5 보다 더 느릴 수도 있다.

API 스펙을 OrderQueryDto로 맞추려면?

OrderApiController

@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());
    }
  • join된 flat 데이터를 가지고 루프를 돌려 OrderFlatDto를 OrderQueryDto로 바꾼다
    • Flat 데이터를 Grouping: OrderQueryDto로 데이터를 그룹핑하여 중복된 데이터를 하나의 Order로 묶는다.
    • Nested 구조로 변환: 각 Order에 속하는 OrderItem들을 OrderItemQueryDto 리스트로 변환해 OrderQueryDto에 설정한다.

OrderQueryDto
@EqualsAndHashCode(of = "orderId")
어노테이션을 추가해주어 orderId 값이 동일하면 두 객체는 동일한 것으로 간주되고, 해시코드도 동일하게 계산하도록 한다.


그 결과 우리가 원하는 스펙으로 받아온 것을 확인할 수 있다.


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

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

  • 엔티티는 절대 캐시에 올리면 안됨, DTO를 올려야한다.

참고: 개발자는 성능 최적화와 코드 복잡도 사이에서 줄타기를 해야 한다. 항상 그런 것은 아니지만, 보통 성능 최 적화는 단순한 코드를 복잡한 코드로 몰고간다.
엔티티 조회 방식은 JPA가 많은 부분을 최적화 해주기 때문에, 단순한 코드를 유지하면서, 성능을 최적화 할 수 있다.
반면에 DTO 조회 방식은 SQL을 직접 다루는 것과 유사하기 때문에, 둘 사이에 줄타기를 해야 한다.

DTO 조회 방식의 선택지

  • DTO로 조회하는 방법도 각각 장단이 있다. V4, V5, V6에서 단순하게 쿼리가 1번 실행된다고 V6이 항상 좋은 방법인 것은 아니다.
  • V4는 코드가 단순하다. 특정 주문 한건만 조회하면 이 방식을 사용해도 성능이 잘 나온다. 예를 들어서 조회한 Order 데이터가 1건이면 OrderItem을 찾기 위한 쿼리도 1번만 실행하면 된다.
  • V5는 코드가 복잡하다. 여러 주문을 한꺼번에 조회하는 경우에는 V4 대신에 이것을 최적화한 V5 방식을 사용해 야 한다. 예를 들어서 조회한 Order 데이터가 1000건인데, V4 방식을 그대로 사용하면, 쿼리가 총 1 + 1000번 실행된다. 여기서 1은 Order 를 조회한 쿼리고, 1000은 조회된 Order의 row 수다. V5 방식으로 최적화 하면 쿼리가 총 1 + 1번만 실행된다. 상황에 따라 다르겠지만 운영 환경에서 100배 이상의 성능 차이가 날 수 있다.
  • V6는 완전히 다른 접근방식이다. 쿼리 한번으로 최적화 되어서 상당히 좋아보이지만, Order를 기준으로 페이징 이 불가능하다. 실무에서는 이정도 데이터면 수백이나, 수천건 단위로 페이징 처리가 꼭 필요하므로, 이 경우 선택 하기 어려운 방법이다. 그리고 데이터가 많으면 중복 전송이 증가해서 V5와 비교해서 성능 차이도 미비하다.
profile
복습 복습 복습

0개의 댓글