주문 + 배송정보 + 회원을 조회하는 API를 만들자
지연 로딩때문에 발생하는 성능 문제를 단계적으로 해결해보자.
1. 엔티티를 직접 노출 (실무에서는 엔티티를 노출하는 일은 없으니 가볍게 보고 넘기기로 하자.)
@GetMapping("/api/v1/simple-orders")
public List<Order> ordersV1(){
List<Order> all = orderRepository.findAll();
return all;
}
위에 코드는 엔티티를 직접 노출하는 개발 방식
양방향 연관관계일때 문제가 생김 → 무한루프에 빠져버림 (잘못된 코드)
이러한 경우 양방향이 걸리는데를 다 @JsonIgnore 를 걸어줘야함
→ 그렇다면 무한 루프가 생기는 일은 피할 수 있지만 지연로딩 관련한 문제는 해결되지 않는다.
지연로딩으로 인한 프록시 문제는 Hibernate5Module bean등록으로 막을 수 있다.
maven repository에서 라이브러리를 찾아 build.gradle에 추가 후 Bean을 등록하자!
@Bean
Hibernate5Module hibernate5Module(){
return new Hibernate5Module();
}
이러한 문제를 피하려고 지연 로딩(LAZY)를 즉시로딩(EAGER) 으로 설정하면 안된다!
즉시 로딩으로 인해서 다른 API 사용시 성능이 저하 될 수 있고 즉시 로딩으로 설정하면
나중에 성능 문제를 해결하기가 매우 어려워 진다.
항상 지연로딩을 기본으로, 성능 최적화가 필요한 경우에는 fetch join을 사용해라!
이러한 문제들도 해결할 수 있지만 애초에 엔티티를 API 외부로 노출하는 것은 좋지 않다.
따라서 Hibernate5Module 사용하기 보단 DTO 로 변환해서 사용하는 것이 더 좋은 방법이다.
아래는 DTO로 변환하는 예제이다.
2. 엔티티를 DTO로 변환( fetch join 사용X )
/**
* V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용 X)
* 단점 : 지연로딩으로 쿼리 N번 호출
*/
@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2(){
List<SimpleOrderDto> orders = orderRepository.findAll()
.stream().map(o -> new SimpleOrderDto(o)).collect(Collectors.toList());
return orders;
// 줄여서 이렇게 사용가능
/*return orderRepository.findAll()
.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(); // LAZY 초기화 -> 영속성 찾아보고 없으면 그때 쿼리 날림
orderDate = order.getOrderDate();
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
}
}
문제점 : 이렇게 사용하면 실행은 잘 되지만 쿼리가 1+N+N번 실행이 된다.
order 갯수에 따라 N이 바뀐다. → order 하나당 주문자와 주문 주소가 다 다르기 때문에 Member와 Delivery의 주소를 order 마다 select 해줘야함
- order에서 조회 1번
- order.getMember().getName() 조회시 지연로딩으로 N번
- order.getDelivery().getAddress() 조회시 지연로딩으로 N번
근데 이때 지연로딩은 영속성 컨텍스트에서 조회하는 것 이므로 중복으로 조회했던 경우엔 쿼리를 생략한다.
따라서 이때는 fetch join을 사용하는것이 좋다.
3. 엔티티를 DTO로 변환 - fetch join 최적화
/**
* V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용 O)
*/
@GetMapping("/api/v3/simple-orders")
public List<SimpleOrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithMemberDelivery();
return orders.stream().map(SimpleOrderDto::new).collect(Collectors.toList());
}
OrderRepository.java
// 한번의 쿼리로 order, member, delivery 를 다 join 해서 한번에 select 해온다.
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();
}
v2 예제 이미지처럼 쿼리가 N번 즉, 여러번 나가는 문제를 fetch join을 사용해서 한방쿼리로 성능을 최적화 할 수 있다.
또한 이렇게 fetch join을 사용하면 이미 order의 member와 order의 delivery 모두 조회 된 상태이므로 지연로딩이 필요없음
→ fetch join은 잘 활용 할수록 좋다!!
4. JPA에서 DTO로 바로 조회
new 명령어를 사용해 JPQL의 결과를 DTO로 즉시 변환한다.
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();
}
위와같은 사용은 리포지토리 재사용성이 떨어진다.
→ API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점
★ 쿼리 방식 선택 권장 순서
- 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
- 필요하면 페치 조인으로 성능을 최적화 한다. → 대부분의 성능 이슈가 해결된다.
- 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
- 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.
'JAVA' 카테고리의 다른 글
spring boot 프로젝트 생성과 사용 이유 (0) | 2022.07.04 |
---|---|
[JAVA] API 구현 시 테스트 개발 (0) | 2022.06.02 |
[JAVA] CRU API (0) | 2022.05.17 |
[JAVA] boolean @Getter (0) | 2022.03.24 |
[TEST] Memory DB 사용 (0) | 2022.03.16 |