주문 + 배송정보 + 회원을 조회하는 API를 만들기
-> 지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해볼것
private final OrderRepository orderRepository;
//이건 그냥 반환하면 큰 문제가 생김 - 무한루프에 빠짐
//오더에서 멤버갓다가 멤버에서 오더갓다가 왓다갓다해서 무한루프
//따라서 이렇게 쓸려면 한쪽에 @JsonIgnore을 써줘야해
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1(){
List<Order> all = orderRepository.findAllByString(new OrderSearch());
return all;
}
-> order domain 에서 멤버가 지연로딩으로 되어있음 그래서 new해서 멤버객체가져오는게 아니라 멤버는 손을 안댐. 지연로딩이라
-> 따라서 hibernate는 proxy멤버를 생성해서 넣어놔 가짜멤버를 넣어놓고 멤버객체를 실제로 사용하려할때 디비에 그때 접근해서 가져옴
-> 근데 저 V1은 프록시로 자리만 대체해놨기 때문에 JSON이 값을 가지고 올 수 없어서 오류가 난거임.
하이버네이트 모듈 설치하기
-> 지연로딩 무시하라는 의미를 가지게됨
@SpringBootApplication
public class JpashopApplication {
public static void main(String[] args) {
SpringApplication.run(JpashopApplication.class, args);
}
@Bean
Hibernate5Module hibernate5Module(){
return new Hibernate5Module();
}
}
-> 이러면 아까 proxy 관련 문제를 해결할 수 있는데 가볍게 듣자! 왜냐하면 v1은 안좋은 방법이기 때문
-> 엔티티를 그대로 노출해서 API 스펙 노출하기도하고 성능상으로도 안좋아서
-> force lazy loading을 해주기 싫으면 getmapping안에 iteration 사용해서 강제로 getname() 해주는 방법도 있음
즉, DTO로 변환해서 반환하는 습관을 들이자!
//v2
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2(){
return orderRepository.findAllByString(new OrderSearch()).stream()
.map(SimpleOrderDto::new)
.collect(Collectors.toList());
}
//배송관련 정보
@Data
static class SimpleOrderDto{
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
//생성자로 필요한거 완성시키기
public SimpleOrderDto(Order order){
orderId = order.getId();
name = order.getMember().getName();
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
-> 주문 두개 잘 나오는게 보임!
-> API 스펙에 맞춰서 최적화돼서 잘 나옴
-> 이렇게하면 엔티티값 바뀌어도 돼고 엔티티 속성 이름 바껴도 바로 컴파일 오류나서 캐치를 할 수 있으니까 편함!
하지만 v1, v2 모두 lazy로딩으로 인해 쿼리가 너무 많이 호출된다는 단점이 있어. 즉, 테이블을 3개나 조회를 해야하기때문!
-> 오더, 멤버, 딜리버리 세개를 조회해야해
1) Order -> SQL 1번 실행하는데 결과 주문수 2개 나와서 루프를 두번 돌아 (주문 하나를 위한 DTO 생성될때마다)
-> 주문 한번당 2번 쿼리 돌려서 총 5번의 쿼리가 나가게돼
이걸 N+1 문제라해.
-> 첫번쨰 쿼리(1)을 날리고, 그거의 결과로 N번만큼(주문수) 쿼리 실행돼
-> 여기선 회원당 배송이 N 번이라 1+N+N해서 5번 쿼리터짐
-> 성능이 많이 안좋아져!
-> 즉, 같은 멤버에 대한 쿼리는 생략을 함. 영속성 컨텍스트에서 조회해서!
쨌든 성능이 안좋긴해.
//오더를 조인하는데 멤버랑 딜리버리를 한번에 조인해서 다 가져오는 것
//lazy 무시하고 다 값을 채워서가져옴
public List<Order> findAllWithMemberDelivery() {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).getResultList();
}
-> 새로운 함수 만들어줘서 fetch join 함
-> 실무에서 정말 자주함. fetch join은 백프로 이해를 하고 사용을 하자!
-> 기본적인건 lazy로 깔고, 내가 필요한거만 fetch join으로 디비에서 가져오면 대부분의 성능 문제가 해결이 됨
//패치조인쓰는 버전
//v2, v3는 결과적으론 똑같지만 날리는 쿼리의 개수가 다름
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
List<SimpleOrderDto> result = orders.stream()
.map(o -> new SimpleOrderDto(o))
.collect(Collectors.toList());
return result;
}
같은 결과를 얻는데 v2는 5번의 쿼리를 날리는 반면, v3는 쿼리를 한번만 날림.
-> 페치 조인으로 order -> member , order -> delivery 는 이미 조회 된 상태 이므로 지연로딩X
-> 패치조인 적극적으로 활용하자!
-> 엔티티를 DTO로 중간에 변환하는 과정없이 바로 JPA 에서 데이터로 가져오는것
Repository 에 함수 만들어줄때 리포지토리랑 컨트롤러 의존관계 생기는거 주의하자! 지워줘야 안생김!! 생기면 큰일!!
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OrderSimpleQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
//생성자로 필요한거 완성시키기
public OrderSimpleQueryDto(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;
}
}
public List<OrderSimpleQueryDto> findOrderDtos() {
return em.createQuery(
"select new jpabook.jpashop.repository.OrderSimpleQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
"from Order o" +
" join o.member m" +
" join o.delivery d", OrderSimpleQueryDto.class
).getResultList();
}
@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
return orderRepository.findOrderDtos();
}
v3랑 결과는 똑같은데 select절에서 내가 원하는것만 셀렉트가 됨
-> 쿼리를 할 때 sql 짜듯이 내가 원하는애들만 가져온거
-> 벗 v3는 엔티티를 가져온거라 변경이 가능한데 얘는 DTO로 조회해서 변경이 안된다는 단점이있음
네트워크 상황과 API 상황을 보고 결정을 하자! 어떤 방법을 사용할지!