5️⃣ 주문 조회 V4 : JPA에서 DTO 직접 조회
- OrderRepository : Order 엔티티 조회용으로 사용하는 것
- Query 관련 Repository : 화면이나 API에 의존관계가 있는 것들 전용
❓ OrderDto(OrderApiConroller 속)가 아닌 OrderQueryDto를 사용하는 이유
→ OrderDto를 참조하면 Repository가 Controller를 의존하게 되는 순환관계가 생겨버린다.
→ 어차피 OrderQueryRepository가 알아야 하기에 같은 패키지에 다시 만들었다.
- Query : 루트 1번(order 가져오기), 컬렉션 N번 실행
- ToOne 관계는 먼저 조회하고 ToMany 관계는 각각 처리했다
- ToOne 관계는 조인해도 데이터 row 수가 증가하지 않는다.
- ToMany 관계는 조인하면 데이터 row 수 증가한다.
- row 수가 증가하지 않는 ToOne 관계는 조인으로 최적화 하기 쉽고,
row 수가 증가하는 ToMany 관계는 최적화 하기 어려우므로 ‘findOrderItems()’ 같은 별도의 메서드로 조회한다
api.OrderApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
private final OrderQueryRepository orderQueryRepository;
...
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> orderV4(){
return orderQueryRepository.findOrderQueryDtos();
}
}
repository.order.query.OrderQueryRepository
package jpabook.jpashop.repository.order.query;
...
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
public List<OrderQueryDto> findOrderQueryDtos(){
List<OrderQueryDto> result = findOrders(); // order query 1번 -> order 데이터 N개
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId()); // 각 order에 대한 orderItem N개 쿼리 발생 -> N + 1
o.setOrderItems(orderItems);
});
return result;
}
private List<OrderItemQueryDto> findOrderItems(Long orderId) { // 루프를 돌릴 때 마다 쿼리를 날린다.
return em.createQuery("select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderprice, oi.count) " +
"from OrderItem oi " +
"join oi.item i " +
"where oi.order.id = :orderId", OrderItemQueryDto.class)
.setParameter("orderId", orderId)
.getResultList();
}
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address) " +
"from Order o " +
"join o.member m " +
"join o.delivery d", OrderQueryDto.class)
.getResultList();
}
}
repository.order.query.OrderQueryDto
package jpabook.jpashop.repository.order.query;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
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;
}
}
repository.order.query.OrderItemQueryDto
package jpabook.jpashop.repository.order.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
@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;
}
}
6️⃣ 주문 조회 V5 : JPA에서 DTO 직접 조회- 컬렉션 조회 최적화
- 데이터 select 하는 양은 줄어든다.
- 쿼리 수가 줄어든다
- ToOne 관계들(member, delivery)을 먼저 조회(findOrders)하고, 여기서 얻은 orderId로 ToMany 관계인 OrderItem 을 한꺼번에 조회(findAllByDto_optimization)한다.
- Map을 이용해서 매칭 성능 향상
api.OrderApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
private final OrderQueryRepository orderQueryRepository;
...
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> orderV5(){
return orderQueryRepository.findAllByDto_optimization();
}
}
repository.order.query.OrderQueryRepository
package jpabook.jpashop.repository.order.query;
...
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
...
public List<OrderQueryDto> findAllByDto_optimization() {
**List<OrderQueryDto> result = findOrders(); // order에 관한 쿼리 1번
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
result.forEach(o ->o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDto> orderItems = em.createQuery("select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderprice, oi.count) " +
"from OrderItem oi " +
"join oi.item i " +
"where oi.order.id in :orderIds", OrderItemQueryDto.class) // 1번의 쿼리를 날려 모두 가져온다.
.setParameter("orderIds", orderIds)
.getResultList();
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream() // 메모리에 map으로 다 가져온 다음 메모리에서 매칭을 해서 값을 세팅해 주는 것 -> 쿼리 총 2번
.collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));
return orderItemMap;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}**
}
- V4
- order에 관한 쿼리 1번 날림 ( findOrders ) → 데이터 개수 n개
- 각 orderid와 관련된 orderitems 쿼리 날림 → n번
- 총 N + 1번 쿼리
- V5
- order에 관한 쿼리 1번 날림 ( findOrders ) → 데이터 개수 n개
- orderid를 컬렉션으로 list형식으로 묶어버림 → map을 이용해서
- 그 리스트에 있는 모든 orderid와 관련된 orderitem을 한 번에 가져오기 → 쿼리 1번
- 결과
[
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-11-09T23:44:57.963477",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderItems": [
{
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
}
]
},
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-11-09T23:44:57.985578",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING BOOK1",
"orderPrice": 20000,
"count": 3
},
{
"itemName": "SPRING BOOK2",
"orderPrice": 40000,
"count": 4
}
]
}
]
7️⃣ 주문 조회 V6 : JPA에서 DTO 직접 조회, 플랫 데이터 최적화
- Order와 OrderItem join하고, OrderItem이랑 Item이랑 join해서 1번에 다 가져오는 방법
- 장점 : 쿼리가 1번만 나간다.
- 단점 : 페이징을 할 수 없다
api.OrderApiController
package jpabook.jpashop.api;
...
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
private final OrderQueryRepository orderQueryRepository;
...
@GetMapping("/api/v6/orders")
public List<OrderFlatDto> orderV6(){
return orderQueryRepository.findAllByDto_flat();
}
}
repository.order.query.OrderQueryRepository
package jpabook.jpashop.repository.order.query;
...
@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {
private final EntityManager em;
...
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();
}
}
- 결과
[
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-11-09T23:44:57.963477",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-11-09T23:44:57.963477",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
},
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-11-09T23:44:57.985578",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"itemName": "SPRING BOOK1",
"orderPrice": 20000,
"count": 3
},
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-11-09T23:44:57.985578",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"itemName": "SPRING BOOK2",
"orderPrice": 40000,
"count": 4
}
]
api 스펙 - OrderQueryDto로 하고 DB Dto는 OrderFlatDto로 하려면?
장점 : 쿼리가 1번이다.
단점
- 페이징이 불가능하다.
- 애플리케이션에서 조각조각 다시 맞춰야 한다는 점에서 복잡하다.
- 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 느릴 수 도 있다.
api.OrderApiController
package jpabook.jpashop.api; ... @RestController @RequiredArgsConstructor public class OrderApiController { private final OrderRepository orderRepository; private final OrderQueryRepository orderQueryRepository; ... @GetMapping("/api/v6/orders") public List<OrderQueryDto> orderV6(){ 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()); } }
- 결과
[
{
"orderId": 11,
"name": "userB",
"orderDate": "2022-11-10T00:11:58.2532",
"orderStatus": "ORDER",
"address": {
"city": "진주",
"street": "2",
"zipcode": "2222"
},
"orderItems": [
{
"itemName": "SPRING BOOK1",
"orderPrice": 20000,
"count": 3
},
{
"itemName": "SPRING BOOK2",
"orderPrice": 40000,
"count": 4
}
]
},
{
"orderId": 4,
"name": "userA",
"orderDate": "2022-11-10T00:11:58.232188",
"orderStatus": "ORDER",
"address": {
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderItems": [
{
"itemName": "JPA1 BOOK",
"orderPrice": 10000,
"count": 1
},
{
"itemName": "JPA2 BOOK",
"orderPrice": 20000,
"count": 2
}
]
}
]
8️⃣ API 개발 고급 정리
엔티티 조회
- 엔티티를 조회해서 그대로 반환 : V1 → 여러 테이블 조인하면 성능이 안 나옴
- 엔티티 조회 후 DTO로 변환 : V2 → 여러 테이블 조인하면 성능이 안 나옴
- 페치 조인으로 쿼리 수 최적화 : V3
- 컬렉션 페이징과 한계 돌파 : V3.1
- 컬렉션은 페치 조인시 페이징이 불가능
- ToOne 관계는 페치 조인으로 쿼리 수 최적화
- 컬렉션은 페치 조인 대신에 지연 로딩을 유지하고hibernate.default_batch_fetch_size
,@BatchSize
로 최적화
DTO 직접 조회
- JPA에서 DTO를 직접 조회 : V4
- 컬렉션 조회 최적화 - 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화 : V5
- 플랫 데이터 최적화 - JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 반환 : V6
권장 순서
- 엔티티 조회 방식으로 우선 접근
→ 코드를 수정하지 않고, 옵션만 약간 변경해서 다양한 성능 최적화 시도 가능
1. 페치 조인으로 쿼리 수를 최적화
2. 컬렉션 최적화
1. 페이징 필요 →hibernate.default_batch_fetch_size
,@BatchSize
로 최적화
2. 페이징 필요X → 페치 조인 사용- 엔티티 조회 방식으로 해결이 안 되면 DTO 조회 방식 사용
→ 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.- DTO 조회 방식으로 해결이 안 되면 Native SQL or JdbcTemplate
- DTO 조회 방식
- V4
- 코드가 단순하다.
- 특정 주문 한 건만 조회하면 이 방식을 사용해도 성능이 잘 나온다.
ex - order 데이터가 1건이면 orderitem을 찾기 위한 쿼리도 1번만 실행하면 된다.- V5
- 코드가 복잡하다
- 여러 주문을 한꺼번에 조회하는 경우에 최적화된 방법이다.
ex - 조회한 order 데이터가 1000건이면 V4는 쿼리 수가 1+ 1000, V5는 1 +1 번이다.- V6
- 쿼리는 1번이다.
- Order를 기준으로 페이징이 불가능하다.
- 데이터가 많으면 중복 전송이 증가해서 V5와 비교해서 성능차이가 미비하다.
➕ 엔티티는 직접 캐싱하면 안 된다.
- 영속성 컨텍스트를 관리하고 있는데 영속성 컨텍스트가 캐시에 있으면 삭제가 불가능하므로
- 캐시하는 것은 오직 DTO로 해야 한다.