본 문서는 인프런의 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 (김영한) 강의를 공부하며 작성한 개인 노트입니다.
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(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;
}
orderItems ~ orderItems.stream()
) 데이터를 받아서 뿌림@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList())
return collect;
}
OrderDto
@Data
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.getMember().getAddress();
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(Collectors.toList());
}
}
OrderItemDto
@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();
}
}
OrderRepository.java에 메서드 추가
public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct 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();
}
fetch join
은 데이터베이스 입장에서는 일대다 조인이 되기 때문에 데이터 row 수 증가 distinct
distinct
는 레코드가 완전 똑같아야지만 제거distinct
는 같은 id
값을 가지면 제거.setFirstResult(#)
, .setMaxResult(#)
)fetch join
하는 순간 레코드의 순서가 섞임 데이터가 부정합하게 조회될 수 있음
전부 fetch join
하기 때문에 중복데이터가 상당히 많음
페이징 + 컬렉션 엔티티 조회 방법
1. ToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 (ToOne 관계는 row수를 증가시키지 않음 > 페이징 쿼리에 영향 X)
2. 컬렉션은 지연로딩으로 조회
3. 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size
, @BatchSize
적용
hibernate.default_batch_fetch_size
: 글로벌 설정@BatchSize
: 개별 최적화@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit)
{
List<Order> orders = orderRepository.findAllWithMembersDelivery(offset, limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
in
쿼리로 데이터베이스에 있는 OrderA
와 OrderB
의 OrderItems
을 모두 가져온 것public List<Order> findAllWithMembersDelivery(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();
}
jpa.properties.hibernate.default_batch_fetch_size: 100
default_batch_fetch_size
가 in
쿼리의 개수를 결정in
쿼리는 한번만 실행중복없는 데이터 전송.
최적화된 쿼리
글로벌하게 (application.yml
에 정의) 정의하기보다는
디테일하게 정의하고 싶을 때
컬렉션일 때
...
public class Order {
...
@BatchSize(size = 1000)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
}
ToOne
관계일 때 (컬렉션 X)
@BatchSize(size = 1000)
public class Item {
}
Order
와 OrderItem
을 조인하면 Order가 OrderItem만큼 중복해서 조회됨 > 중복조회hibernate.default_batch_fetch_size
로 최적화엔티티를 조회하는 리포지토리(OrderRepository)와 api/화면 의존관계가 있는 리포지토리(OrderQueryRepository)의 분리
repository/order/query/OrderQueryRepository.class
findOrders
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, o.address)" +
" from Order o" +
" join o.member m" +
" join o.delivery d", OrderQueryDto.class)
.getResultList();
}
findOrderItems
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();
}
findOrderQueryDtos
public List<OrderQueryDto> findOrderQueryDtos() {
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
repository/order/query/OrderQueryDto.class
@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, List<OrderItemQueryDto> orderItems) {
this.orderId = orderId;
this.name = name;
this.orderDate = orderDate;
this.orderStatus = orderStatus;
this.address = address;
this.orderItems = orderItems;
}
}
repository/order/query/OrderItemQueryDto.class
@Data
public class OrderItemQueryDto {
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;
}
}
ToOne
(N:1, 1:1) 관계 먼저 조회. ToMany
(1:N) 관계는 각각 별도 처리ToOne
관계는 조인해도 데이터 row 수 증가 XToMany
(1:N) 관계는 조인하면 row 수 증가OrderQueryRepository에 생성
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> result = findOrders();
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
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
)
.setParameter("orderIds", orderIds)
.getResultList();
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
ToOne
관계 먼저 조회. 조회에서 얻은 식별자 orderId
로 ToMany
관계 조회 repository/order/query/OrderFlatDto.class
@Data
public class OrderFlatDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private String itemName;
private int orderPrice;
private int count;
}
OrderQueryDto
와 OrderQueryItemDto
의 필드 모두 포함메서드 추가 - repository/order/query/OrderRepository.class
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();
}
OrderFlatDto > OrderQueryDto
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
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());
}
ToOne
관계는 페치 조인으로 쿼리 수 최적화. 컬렉션은 페치 조인 대신 지연 로딩 유지하고 batch size 조정hibernate.default_batch_fetch_size
, @BatchSize