컬렉션 조회 최적화 - JPA에서 DTO로 직접 조회 + 플랫 데이터 최적화

HotFried·2023년 11월 27일
0

이전 글 :
컬렉션 조회 최적화 - JPA에서 DTO 직접 조회
컬렉션 조회 최적화 - JPA에서 DTO 직접 조회 + 컬렉션 조회 최적화

N+1 문제를 해결하기 위해 IN절을 사용해 문제를 해결하였다.
더 나아가서 쿼리 1번으로 성능을 최적화 해보도록 한다.
-> 한번에 모든 데이터를 Join한다면?


Code

OrderApiController

@RestController
@RequiredArgsConstructor
public class OrderApiController {

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

        return flats;
    }
}

OrderQueryRepository

모든 데이터를 Join한다.

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    public List<OrderFlatDto> findAllByDto_flat() {
        // 모든 데이터를 join
        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();
    }
}

OrderFlatDto

Order, OrderItem의 필드를 모두 Dto로 변환한다.

@Data
public class OrderFlatDto {

    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private Address address;
    private OrderStatus orderStatus;

    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.address = address;
        this.orderStatus = orderStatus;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
    }
}

결과

[
  {
    "orderId": 4,
    "name": "userA",
    "orderDate": "2022-05-01T15:53:40.001454",
    "address": {
      "city": "서울",
      "street": "1",
      "zipcode": "1111"
    },
    "orderStatus": "ORDER",
    "itemName": "JPA1 BOOK",
    "orderPrice": 10000,
    "count": 1
  },
  {
    "orderId": 4,
    "name": "userA",
    "orderDate": "2022-05-01T15:53:40.001454",
    "address": {
      "city": "서울",
      "street": "1",
      "zipcode": "1111"
    },
    "orderStatus": "ORDER",
    "itemName": "JPA2 BOOK",
    "orderPrice": 20000,
    "count": 2
  },
  {
    "orderId": 11,
    "name": "userB",
    "orderDate": "2022-05-01T15:53:40.033561",
    "address": {
      "city": "진주",
      "street": "2",
      "zipcode": "2222"
    },
    "orderStatus": "ORDER",
    "itemName": "SPRING1 BOOK",
    "orderPrice": 20000,
    "count": 3
  },
  {
    "orderId": 11,
    "name": "userB",
    "orderDate": "2022-05-01T15:53:40.033561",
    "address": {
      "city": "진주",
      "street": "2",
      "zipcode": "2222"
    },
    "orderStatus": "ORDER",
    "itemName": "SPRING2 BOOK",
    "orderPrice": 40000,
    "count": 4
  }
]

Json파일을 확인해보면 OrderId가 중복으로 들어가있다.

DB를 확인해보자.

COL_0_0_OrderId에 해당하는데,
컬렉션 Fetch Join과는 다르지만 비슷하게도 OrderId가 뻥튀기 되어있는 모습을 확인할 수 있다.

-> 페이징이 불가능 하다는 결론이 나온다.


하지만, 개발자의 능력(?)으로 Json파일에서의 중복은 방지할 수 있다.


Json 파일의 중복을 방지해보자

Code

OrderId기준으로 그루핑하는 전략을 선택

OrderApiController

@RestController
@RequiredArgsConstructor
public class OrderApiController {

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

        return flats.stream()
                // orderId 기준으로 그루핑
                .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());
    }
}

OrderQueryDto

@Data
// groupingBy할 때 묶는 기준이 뭔지 알려줘야 한다.
@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;
    }
}

Controller에서 GroupingBy가 실행될 때 어떤 필드를 기준으로 그루핑할지 지정해주어야 한다.

Lombok을 이용해 @EqualsAndHashCodr(of = "orderId")로 간단하게 지정할 수 있다.

추가로, OrderQueryDto에 orderItems를 담을 수 있는 생성자를 생성해 준다.


정리

  • Query 1번으로 최적화

  • 단점

    • 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 IN절을 사용해 컬렉션을 최적화 했을 때 보다 더 느릴 수 있다.

    • 애플리케이션에서 추가 작업이 크다.

    • 페이징 불가


참고 :

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

profile
꾸준하게

0개의 댓글