컬렉션 조회 최적화
1. 엔티티 조회 - 페치 조인으로 쿼리 수 최적화
OrderApiController
/**
* 페치 조인
*/
@GetMapping("/api/v3/orders")
public ResponseEntity<Result<List<OrderDto>>> orderV3() {
// N 만큼 데이터 나옴
List<Order> orders = repository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.toList();
return ResponseEntity.ok(new Result<>(result));
}
Order 엔티티 객체로 쿼리 결과를 받아서
Dto 로 변환작업을 한다음 반환
OrderRepository
findAllWithItem()
public List<Order> findAllWithItem() {
return em.createQuery(
"SELECT distinct o from Order o " + // distinct : 엔티티 중복시 엔티티를 걸러서 컬렉션에 담아준다.
" JOIN FETCH o.member m" + // ToOne (페치조인)
" JOIN FETCH o.delivery d" + // ToOne (페치조인)
" JOIN FETCH o.orderItems oi" + // ToMany
" JOIN FETCH oi.item i", Order.class)
.getResultList();
}
" JOIN FETCH o.orderItems oi" + // ToMany
이런식의 쿼리는 컬렉션 페치조인 이기 때문에 페이징 불가능
해당 컬렉션조회(ToMany) 는 List와 같은 컬렉션이기 때문에 row 수를 증가시켜서 페이징 쿼리에 영향을 줌.
그래서 페이징을 사용할 수 없다.
OrderDto
@Getter
static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
public OrderDto(Order order) {
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.toList();
}
}
order.getOrderItems() 에서 지연로딩 발생
Result
@Data
@AllArgsConstructor
public class Result<T> {
int count;
private T data;
public Result(T data) {
this.data = data;
}
}
API 요청 결과
2. Dto 쿼리 조회
컬렉션을 JPA에서 DTO 직접 조회
@GetMapping("/api/v4/orders")
public ResponseEntity<Result<List<OrderQueryDto>>> orderV4() {
List<OrderQueryDto> orderQueryDtos = queryRepository.findOrderQueryDtos();
return ResponseEntity.ok(new Result<>(orderQueryDtos));
}
OrderQueryDto
@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;
}
}
findOrderQueryDtos
public List<OrderQueryDto> findOrderQueryDtos() {
List<OrderQueryDto> result = findOrders(); // query 1번 -> N개
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId()); // query N번
o.setOrderItems(orderItems);
});
return result;
}
단점은 N + 1 : 쿼리 1번 조회시 루프를 돌면서 N개의 쿼리가 날라감.
List<OrderItemQueryDto>
OrderItemQueryDto
@Data
@AllArgsConstructor
public class OrderItemQueryDto {
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
}
OneToMany 연관관계
findOrders
public List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new com.inflearn.optimization.domain.order.repository.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();
}
ToOne 쿼리 조회
offset과 limit으로 페이징 가능하게 변경가능
public List<Order> findAllWithMemberDelivery(String offset, String limit) {
return em.createQuery(
"SELECT o FROM Order o" +
" JOIN FETCH o.member m" +
" JOIN FETCH o.delivery d", Order.class)
.setFirstResult(Integer.parseInt(offset))
.setMaxResults(Integer.parseInt(limit))
.getResultList();
}
findOrderItems
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new com.inflearn.optimization.domain.order.repository.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();
}
쿼리 조회
API 요청 결과
3. Dto 쿼리 조회 - IN 쿼리로 메모리 사용으로 O(1) 최적화 조회
일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화
findAllByDto_optimization
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> orderDtos = findOrders();
List<Long> orderIds = orderDtos.stream().map(o -> o.getOrderId()).collect(Collectors.toList());
// ctrl alt v
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new com.inflearn.optimization.domain.order.repository.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)
.setParameter("orderIds", orderIds)
.getResultList();
// 최적화 - 메모리에서 매칭해서 set
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));// id를 기준으로 map 으로
orderDtos.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId()))); // set OrderItem
return orderDtos;
}
groupingBy: 키를 기준으로 값에 List 컬렉션으로 Map 생성
하나의 주문Id 에 여러 Item List 를 Map으로 감쌈
- Query는 루트 1번, 컬렉션 1번
- ToOne 관계들을 먼저 조회 후 여기서 얻은 식별자(OrderId)로 ToMany 관계인 OrderItem을 한꺼번에 조회
- Map을 사용해서 매칭 성능 향상 O(1)
권장 순서
- 엔티티 조회 방식으로 우선 접근
- 페치조인으로 쿼리 수를 최적화
- 컬렉션 최적화
- 페이징 필요 : batch size 로 최적화
- 페이징 X : 페치 조인 사용
- 엔티티 조회 방식으로 해결이 안되면, DTO 조회 방식사용
- DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 jdbcTemplate
* 엔티티 조회 방식은 JPA가 많은 부분을 최적화 해주기 때문에
단순한 코드를 유지하면서, 성능을 최적화 할 수 있는 장점이 있다.
* DTO를 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.
DTO 캐시를 하거나
트래픽이 너무 많으면 보통 캐시를 쓴다.
'Back-end > Spring Boot + REST API' 카테고리의 다른 글
스프링부트 API JPA 최적화 페치조인, 페이징 default_batch_fetch_size (0) | 2023.12.23 |
---|---|
스프링부트 API JPA 최적화 ToOne 관계 (N+1 문제) , 페치 조인 (0) | 2023.12.19 |
Spring Boot 시큐리티 + JWT 흐름 정리 (0) | 2022.11.23 |
Spring Boot 실행 프로세스와 Embedded Servers , CLI 실행 (0) | 2022.09.09 |
Spring boot - blog application (REST API) : AWS RDS, Elastic Beanstalk (0) | 2022.09.08 |
댓글