1.Entity로 조회 시 최적화 방법
(1) 컬렉션 fetch join - 페이징 불가
public List<Order> findAllWithItem() {
return em.createQuery("select distinct o from Order o " +
"join fetch o.member m " +
"join fetch o.delivery " +
"join fetch o.orderItems oi " +
"join fetch oi.item i",Order.class)
.getResultList();
}
fetch join을 이용하여 SQL이 1번만 실행된다. 일대다 조인시 orderItem의 수에 따라 order과 join 되어 DB의 row수가 증가하게 된다. 이 떄 JPA의 distinct를 사용하면 SQL에 distinct를 추가해 줄 뿔만 아니라 같은 order엔티티가 조회되면 애플리케이션에서 중복 조회를 막아준다.
이 방법은 데이터베이스 row수가 증가하므로 페이징을 사용 할 수 없다. 페이징 사용 시 하이버네이트는 경고로그를 남기면서 모든 데이터를 DB에서 읽어오고 메모리에서 페이징을 하므로 매우 위험하다.
(2) BatchSize를 이용한 최적화 - 페이징 가능
1.application.properties
spring.jpa.properties.hibernate.default_batch_fetch_size = 100
properties나 yml 파일에서 defalut batch size를 정할 수 있다.
2.JPQL 및 쿼리
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",Order.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
@OneToOne 이나 @ManyToOne 관계는 fetch join을 사용하여 최적화하고 페이징 한다 .
OrderItem이 조회 될 때 IN절을 이용하여 batch_size만큼 DB에서 나눠서 들고온다.
Item이 조회 될 때도 마찬가지로 IN절을 이용하여 batch_size만큼 DB에서 나눠서 들고온다.
batch_size를 사용하지 않는다면 위의 예에서는 Order를 조회하는 쿼리 1개, OrderItem를 조회하는 쿼리 2개, Item을 조회하는 쿼리 4개가 나간다.
따라서 ToOne 관계를 fetchjoin하고 컬렉션조회는 batch_size를 이용한다면 1+N+M개에서 1+N/batch_size+M/batch_size 로 쿼리수를 줄일 수 있다. (N:중복되지 않는 Order 개수 M:중복되지 않는 OrderItem 개수)
batch_size의 크기는 DB의 IN절 파라미터 제한 개수로 하는것이 성능 상 가장 좋지만 DB든 애플리케이션이든 견딜 수 있는 순간 부하로 결정하는 것이 좋다.
2.Dto로 조회 시 최적화 방법
조회에 쓰일 DTO
public class OrderQueryDto {
private Long orderId;
private String name;
private OrderStatus orderStatus;
private LocalDateTime orderDate;
private Address address;
private List<OrderItemQueryDto> orderItems;
...
}
public class OrderItemQueryDto {
private Long orderId;
private String itemName;
private int orderPrice;
private int count;
...
}
public class OrderFlatDto {
private Long orderId;
private String name;
private OrderStatus orderStatus;
private LocalDateTime orderDate;
private Address address;
private String itemName;
private int orderPrice;
private int count;
...
}
(1) 컬렉션 조회 최적화
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;
}
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new inflearn.springboot_jpa.repository.order.query.OrderQueryDto(o.id,m.name,o.status,o.orderDate,d.address) " +
"from Order o " +
"join o.member m " +
"join o.delivery d",OrderQueryDto.class)
.getResultList();
}
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new inflearn.springboot_jpa.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)
.setParameter("orderIds", orderIds)
.getResultList();
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
return orderItemMap;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
List<Long> orderIds = result.stream().map(OrderQueryDto::getOrderId).collect(Collectors.toList());
return orderIds;
}
1.ToOne 관계들을 먼저 조회한다. -> findOrders()
2.ToOne관계들을 먼저 조회하고 여기서 얻은 식별자 OrderIds를 얻는다. ->toOrderIds()
3.OrderIds를 이용하여 IN절을 이용하여 ToMany관계인 OrderItem를 한꺼번에 조회하여 Map를 만든다
- >findOrderItemMap()
4.ToOne관계를 조회하여 얻은 데이터에 ToMany 관계인 OrderItem을 넣어준다.
이러한 방법을 이용하면 ToOne관계를 조회하는 쿼리가 1번나가고 ToMany 관계인 OrderItem을 조회하는 쿼리가 1번나가게 되어 최적화 할 수 있다.
(2)flat 데이터 최적화
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery(
"select new inflearn.springboot_jpa.repository.order.query.OrderFlatDto(o.id,m.name,o.status,o.orderDate,d.address,i.name,oi.orderPrice,oi.count) " +
"from Order o " +
"join o.delivery d " +
"join o.member m " +
"join o.orderItems oi " +
"join oi.item i",OrderFlatDto.class)
.getResultList();
}
public List<OrderQueryDto> ordersV6(){
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(),
o.getName(), o.getOrderStatus(),o.getOrderDate(), 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().getOrderStatus(),e.getKey().getOrderDate(),
e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
1. join을 이용하여 flat한 dto로 조회한다.
2. join으로 인해 DB에서 애플리케이션으로 전달하는 데이터에 중복하는 데이터가 추가되므로 중복되는 데이터를 걸러주는 작업을 수행 한후 반환한다.
이러한 방법을 사용하면 DB의 row수가 증가하지만 쿼리는 1번만 나가게 되어 최적화 할 수 있다. 하지만 DB의 row수가 증가하기 때문에 페이징이 불가능하고 애플리케이션에서 추가작업이 일어나므로 상황에 따라 선택하여야 한다.
권장 순서
1.Entity 조회 방식으로 우선접근
(1) fetch join으로 쿼리수를 최적화
(2) 컬렉션 최적화
- 페이징 필요(batch_size 사용)
- 페이징 필요X(fetch_join사용)
2. Entity 조회 방식으로 해결이 안될시 DTO 조회 방식 사용
3. DTO조회방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate 사용
'Spring' 카테고리의 다른 글
벌크연산시 주의할 점 (0) | 2023.04.18 |
---|---|
SpringData JPA를 활용한 페이징 및 정렬 (0) | 2023.04.18 |
Springdoc을 이용한 swagger3 구현 (0) | 2023.04.10 |
Entity를 변경하는 방법(Merge와 변경감지) (0) | 2023.04.07 |
h2-console 연동하기 (0) | 2023.04.03 |