이전 글 :
컬렉션 조회 최적화 - JPA에서 DTO 직접 조회
컬렉션 조회 최적화 - JPA에서 DTO 직접 조회 + 컬렉션 조회 최적화
N+1
문제를 해결하기 위해 IN
절을 사용해 문제를 해결하였다.
더 나아가서 쿼리 1번
으로 성능을 최적화 해보도록 한다.
-> 한번에 모든 데이터를 Join한다면?
@RestController
@RequiredArgsConstructor
public class OrderApiController {
@GetMapping("/api/v6/orders")
public List<OrderFlatDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats;
}
}
모든 데이터를 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();
}
}
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파일에서의 중복은 방지할 수 있다.
OrderId기준으로 그루핑하는 전략을 선택
@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());
}
}
@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절을 사용해 컬렉션을 최적화 했을 때 보다 더 느릴 수 있다.
애플리케이션에서 추가 작업이 크다.
페이징 불가
참고 :