@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;
}
- 모든 Order 조회해서 Entity로 반환
- N+1 문제 -> 프록시 객체 강제 초기화(위의 코드에서 for 부분)
- json 무한루프 -> Entity에 @JsonIgnore 추가
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
@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();
// entity를 그대로 반환 -> orderItem도 dto로 바꿔서 반환해야함
// order.getOrderItems().stream().forEach(o -> o.getItem().getName());
// orderItems = order.getOrderItems();
// 이렇게 orderItem을 dto로 바꾸서 반환
orderItems = order.getOrderItems().stream()
.map(orderItem -> new OrderItemDto(orderItem))
.collect(Collectors.toList());
}
}
@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 안에 있는 OrderItem도 Entity를 그대로 반환하지말고 DTO를 사용해서 반환 해야함!
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3(){
List<Order> orders = orderRepository.finAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
- fetch join으로 n+1 문제 해결 -> sql 1번 실행
- 1:N 관계에서 join하면 N만큼 결과가 나옴(데이터 뻥튀기)
- e.g. order가 2개에 각각 orderItem이 2개씩 있다면 결과가 4개로 나옴(중복결과) => fetch join에 distinct 추가하면 중복제거 가능
페이징 불가능!!!
- fetch join과 페이징 같이 하면 -> 메모리에서 페이징 처리해버림, 데이터가 많으면 위험함
- 1:N관계에서 join하면 N만큼 데이터가 뻥튀기되어서 거기서 페이징처리를 하면 개수가 맞지않음 -> 어쩔 수 없이 hibernate는 경고를 보내고 메모리에서 페이징 처리함
=> 1:N fetch join에서는 페이징 사용X
=> 컬렉션 fetch join은 1개만 사용 -> 컬렉션 둘 이상에 사용하면(1:N:M) 데이터가 많아져서 부정합하게 조회 될 수 있음
public List<Order> finAllWithItem() {
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();
}
jpa에서의 distinct 기능
- db에 distinct query를 날려줌
- entity가 중복인 경우 중복을 걸러서 컬렉션에 담아줌
- db에서 distinct는 한 줄이 전부 다 똑같아야 중복제거
- jpa에서 distinct는 order를 가져올 때 order가 같은 id값이면 중복을 제거해줌
- 컬렉션을 fetch join 하면 페이징이 불가능
- 1:N에서는 1을 기준으로 페이징 하는 것이 목적이지만, 데이터는 N을 기준으로 row가 생성(N만큼 데이터 생성해서 기준이 됨)
@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.findAllWithMemberDelivery(offset,limit);
for (Order order : orders) {
System.out.println("order ref=" + order + " id=" + order.getId());
}
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
- ToOne(OneToOne,ManyToOne)관계를 모두 fetch join -> ToOne 관계는 row수를 증가시키지 않으므로 페이징 query에 영향을 주지 않음
- 컬렉션은 지연로딩
- 지연 로딩 성능 최적화: hibernate.default_batch_fetch_size, @BatchSize 적용
- hibernate.default_batch_fetch_size: 글로벌 설정
- @BatchSize: 개별 최적화
- 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 in query로 조회
- Batch size에 지정한 크기 만큼의 데이터를 미리 가져옴
- 1:M:N이 1:1:1로 바뀜(데이터 크기에 따라 다름)
- fetch join 보다 많은 query 발생, 하지만 정규화된 데이터만 가져옴(중복x)
spring:
jpa:
hibernate:
default_batch_fetch_size: 100
Collection인 경우: Field에 적용
@BatchSize(size = 100)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
Collection이 아닌 경우: Class에 적용
@BatchSize(size = 100)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
적당한 Batch size?
- 100~1000개 사이 권장
- 크기가 크면 순간적으로 DB와 애플리케이션에 순간 부하가 증가할 수 있음
- DB와 애플리케이션이 순간 부하를 어디까지 견딜 수 있는 지에 따라 결정
- 100개든 1000개든 메모리 사용량은 똑같음
@GetMapping("/api/v4/orders")
public List<OrderQueryDto> ordersV4(){
return orderQueryRepository.findOrderQueryDtos();
}
- OrderQueryDto를 만들고 JPA에서 DTO를 직접 조회하는 findOrderQeuryDtos() 호출해서 dto를 반환
public List<OrderQueryDto> findOrderQueryDtos(){
List<OrderQueryDto> result = findOrders();
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
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();
}
}
- View와 관련된 OrderQueryRepository 생성(view 관련된 경우 query와 밀접하게 될 때가 많음)
- 중요한 핵심 비즈니스 로직은 orderRepository에서 해결
- Qudry: 루트 1번, 컬렉션 N번 실행
- ToOne 관계들을 join을 이용해서 한번에 조회 한 후, ToMany 관계는 각각 별도 처리 -> 여기서는 findOrderItems()를 생성해서 이용
- orders를 한번에 찾아온 후 for를 순회하면서 fidOrderItems() 호출해서 orderItem과 Item을 조회해서 저장 -> 결과적으로 N+1 문제 발생
@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;
}
}
@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;
}
}
- DTO 따로 생성
- Controller에 만들어 놓은 dto를 사용하게 되면 repository가 controller를 참조하게 됨(역행)
@GetMapping("/api/v5/orders")
public List<OrderQueryDto> ordersV5(){
return orderQueryRepository.findAllByDto_optimization();
}
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 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)
.setParameter("orderIds", orderIds)
.getResultList();
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto -> OrderItemQueryDto.getOrderId()));
return orderItemMap;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
return orderIds;
}
- Query: 루트 1번, 컬렉션 1번 => 2번 발생 (N+1 문제 해결)
- ToOne 관계를 조회(findOrders()) -> orderId만 따로 추출(toOrderIds) -> orderId로 ToMany 관계인 OrderItem을 한번에 조회하고 Map으로 변환(findOrderItemMap()) -> 순회하면서 OrderItem을 저장(setOrderItems())
@GetMapping("/api/v6/orders")
public List<OrderFlatDto> ordersV6(){
return orderQueryRepository.findAllByDto_flat();
- Query: 1개 실행
- Order로 페이징 불가능, OrderItems가 기준인 상태
- 컬렉션을 따로 처리하지 않고 한번에 JOIN 해서 가져옴 -> 데이터 중복 발생 -> Application에서 추가작업 필요 -> 아래 코드
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());
- API 스펙을 OrderQueryDto 형식으로 반환하려면 일일이 바꿔줘야함
- OrderQueryDto로 변환해서 데이터 중복되지 않도록 가능
@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
- order와 orderItem와 item 조인해서 한번에 가져올 수 있도록 dto 설계
Entity 조회
- V1: Entity 조회 후 그대로 반환 -> Entity 그대로 반환 금지!
- V2: Entity 조회 후 DTO로 변환
- V3: Fetch join으로 query 수 최적화
- V3.1: 컬렉션 페이징과 한계돌파
- 컬렉션은 fetch join시 페이징 불가
- ToOne은 fetch join
- 컬렉션은 lazy 로딩 유지하고 @BatchSize로 최적화
DTO 직접 조회
- V4: JPA에서 DTO 직접 조회
- 코드 단순, 특정 주문 한건만 조회하면 성능 잘나옴
- V5: 컬렉션은 IN 절을 활요해서 메모리에 미리 조회해서 최적화 (페이징 가능)
- 코드 복잡,여러 주문을 한번에 조회하는 경우는 V4보다 V5 방식 사용
- V6: JOIN 결과를 그대로 조회 후 Application에서 원하는 스펙으로 직접 변환(Order로 페이징 불가능)