public class OrderQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간
private OrderStatus orderStatus;
private Address address;
private List<OrderItemQueryDto> orderItems;
}
위와 같은 DTO를 JPA에서 직접 조회해보도록 하겠다.
Order안에 있는 OrderItems 역시 Entity이기 때문에, orderItems를 담을 DTO를 하나 더 만들어야한다.
@Data
public class OrderItemQueryDto {
@JsonIgnore
private Long orderId; //주문번호
private String itemName;//상품 명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderItemQueryDto(Long orderId, String itemName, int orderPrice, int count) {
this.orderId = orderId;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
그 전에, 이 둘의 관계를 살펴보면 Order와 orderItems는 1:N 관계라는것을 알 수 있다. 따라서, Order를 조회하고 이 결과에 OrderItems를 따로 조회해서 값을 넣어주어야한다.
public List<OrderQueryDto> findOrderQueryDtos() {
//루트 조회(toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//루프를 돌면서 컬렉션 추가(추가 쿼리 실행)
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
이 방식은 쿼리가 n+1번 나가게된다.
위의 방식대로 하면, Order를 찾고 각 order당 orderItem을 찾기 위한 쿼리를 한번씩 더 날리는것을 볼 수 있다. 이를 방지하기 위한 방법을 적용해보겠다.
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> result = findOrders();
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
다음과 같은 과정을 거치면 1회의 조회로 OrderItem을 전부 가지고 올 수 있다.
이제 아예 한번에 조회를 하는 쿼리를 만들어볼 수 있다.
@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.orderStatus = orderStatus;
this.address = address;
this.itemName = itemName;
this.orderPrice = orderPrice;
this.count = count;
}
}
다음과같은 DTO에 담을것이다.
orderItem을 담기위한 변수들이 지정된것을 볼 수 있다.
정리
엔티티 조회
엔티티를 조회해서 그대로 반환: V1
엔티티 조회 후 DTO로 변환: V2
페치 조인으로 쿼리 수 최적화: V3
컬렉션 페이징과 한계 돌파: V3.1
컬렉션은 페치 조인시 페이징이 불가능
ToOne 관계는 페치 조인으로 쿼리 수 최적화
컬렉션은 페치 조인 대신에 지연 로딩을 유지하고, hibernate.default_batch_fetch_size , @BatchSize 로 최적화
DTO 직접 조회
JPA에서 DTO를 직접 조회: V4
컬렉션 조회 최적화 - 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화: V5
플랫 데이터 최적화 - JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환: V6
권장 순서
1. 엔티티 조회 방식으로 우선 접근
1. 페치조인으로 쿼리 수를 최적화
2. 컬렉션 최적화
1. 페이징 필요 hibernate.default_batch_fetch_size , @BatchSize 로 최적화
2. 페이징 필요X 페치 조인 사용
2. 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
3. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate