주문 조회 V1 : 엔티티 직접 노출
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByCriteria(new OrderSearch());
for (Order order : all) {
order.getMember().getName();
order.getDelivery().getAddress();
//프록시 강제 초기화 코드
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName());
}
return all;
}
마지막 줄의 코드는 다음과 같이 풀어쓸 수 있다
//프록시 강제 초기화 코드
for (OrderItem orderItem : orderItems) {
orderItem.getItem().getName();
}
getItem.().getName()을 하는 이유
Name 필드가 필요한 것이 아니라, getXXX()를 함으로써 지연로딩된 엔티티 Item을 조회하여 Item 객체를 가져와 필드의 값을 모두 알 수 있다
주문 조회 V2 : 엔티티 -> DTO
DTO를 사용하는 이유에 대해서는 아래 링크를 참고하자
DTO를 사용하는 이유
@Data
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime localDateTime;
private OrderStatus orderStatus;
private Address address;
private List<OrderItem> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
localDateTime = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems();
}
}
이렇게 DTO를 생성해도 결국
엔티티가 모두 노출되기 때문에, API 스펙의 변동에 있어 자유롭지 못하다 (흔한 실수)
따라서 각 엔티티(이 코드에서는 orderItems) 모두 DTO로 만들어줘야 한다.
@Getter
static class OrderItemDto {
private String itemName; //상품명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrderItemDto(OrderItem orderItem) {
itemName = orderItem.getItem().getName();
orderPrice = orderItem.getOrderPrice();
count = orderItem.getCount();
}
}
OrderDto와 유사하게 OrderItemDto를 생성해줬다.
그렇다면 이제 내가 원하는 값만 뽑아낼 수 있다
마지막으로 select 쿼리가 몇 번 나갈지 생각해보자.
처음에 Orders 에서 주문 2개를 확인. (select 쿼리 한 번) // 1
Member1 한 명에 대해 발생 (select 쿼리 한 번) // 1-1
이 멤버의 Delivery에 대해 쿼리 발생 (select 쿼리 한 번)
OrderItems에 대해 쿼리 발생 (select 쿼리 한 번) // 2
OrderItems의 두 개의 목록에 대해 각각의 쿼리 발생(select 쿼리 두 번)// 2-1, 2-2
Member2 한 명에 대해 발생 (select 쿼리 한 번) // 1-2
이 멤버의 Delivery에 대해 쿼리 발생 (select 쿼리 한 번)
OrderItems에 대해 쿼리 발생 (select 쿼리 한 번) // 2
OrderItems의 두 개의 목록에 대해 각각의 쿼리 발생(select 쿼리 두 번) //2-1, 2-2
orders와 orderItems 모두 컬렉션(List)이기 때문에 각각의 세부 목록까지 쿼리가 발생한다.
총 11번의 select 쿼리 발생
주문 조회 V3 : 엔티티 -> DTO : 페치조인 최적화
fetch join을 통해 쿼리가 1번만 실행된다
//OrderApiController
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3(){
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
//OrderRepository
public List<Order> findAllWithItem() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m " +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i", Order.class)
.getResultList();
}
JPA의 distinct를 사용하여 SQL에 distinct를 추가하고, 조인해서 같은 엔티티가 조회된다면 애플리케이션에서 중복을 걸러준다.
DB에서는 1:N 관계에서 N 기준으로 데이터가 뻥튀기가 되어버려서 페이징 사용시 hibernate는 WARN을 발생시키고, 메모리에서 작업을 수행한다.
(ex) order와 orderItems에서 후자를 기준으로 데이터가 맞춰짐
1:N 을 fetch join 하는 순간 페이징 쿼리가 아예 나가지 않는다
주문 조회 V3.1 : 엔티티 -> DTO : 페이징과 한계 돌파
컬렉션은 페치조인을 사용하지 않고, toOne 관계인 엔티티만 페치조인으로 묶어서 코드를 작성하자
//orderRepository
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
Order 기준에서 toOne 관계인 member, delivery를 페치조인으로 묶었다.
총 쿼리 수는,
Order 1번 (두 개의 주문 조회)
member 1번 (두 명의 멤버 조회: 멤버1, 멤버2)
delivery 1번 (멤버1에 대해 조회)
orderItems 1번 (멤버1의 두 개의 아이템 조회: JPA1, JPA2)
member 1번 (두 명의 멤버 조회: 멤버1, 멤버2)
delivery 1번 (멤버2에 대해 조회)
orderItems 1번 (멤버2의 두 개의 아이템 조회: SRPING1, SPRING2)
중복되는 쿼리들을 압축시키고,
from
item item0_
where
item0_.item_id in (
?, ?, ?, ?
)
위와 같이 item을 한 번에 IN 쿼리로 표기하기 위해 applicaiton.yml을 다음과 같이 수정한다
//application.yml
//필요한 데이터를 100개씩 받아옴
//N+1 문제에서 어느정도 해방이 가능하다
default_batch_fetch_size: 100
OR
//Order
//위처럼 글로벌하게 말고 각각의 필드에서도 적용 가능
@BatchSize(size=1000)
@OneToMany(mappedBy = "order",cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
주문 조회 V4 : JPA에서 DTO 직접 조회
public List<OrderQueryDto> findOrdersQueryDtos() {
//findOrders(): 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
List<OrderQueryDto> result = findOrders();
//findOrderItems(): 1:N 관계인 orderItems 조회
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
ToOne(N:1, 1:1) 관계들을 먼저 조회하고, ToMany(1:N) 관계는 각각 별도로 처리한다.
1:N 관계인 order<->orderItems,
orderId 필드는 @JsonIgnore 처리 해줘도 된다.
주문 조회 V5 : JPA에서 DTO 직접 조회 : 컬렉션 조회 최적화
Query: 루트 1번, 컬렉션 1번
ToOne 관계들을 먼저 조회하고, 여기서 얻은 식별자 orderId로 ToMany 관계인 OrderItem을 한꺼번
에 조회
MAP을 사용해서 매칭 성능 향상(O(1))
주문 조회 V6 : JPA에서 DTO 직접 조회, 플랫 데이터 최적화
어려우므로 스킵
API 개발 고급 정리
컬렉션은 페치 조인시 페이징이 불가능
ToOne 관계는 페치 조인으로 쿼리 수 최적화
컬렉션은 페치 조인 대신에 지연 로딩을 유지하고,
hibernate.default_batch_fetch_size , @BatchSize 로 최적화
DTO 직접 조회보다 엔티티 조회방식을 권장
그냥 이 2개가 고트
hibernate.default_batch_fetch_size: //전역적
OR
@BatchSize //지역적
많은 도움이 되었습니다, 감사합니다.