카테고리 없음

JPA(2) - 지연 로딩과 조회 성능 최적화

now0204 2024. 1. 22. 11:31

 

 

1. 엔티티 직접 노출 

 

 private final OrderRepository orderRepository;
 /**
 * V1. 엔티티 직접 노출
 * - Hibernate5Module 모듈 등록, LAZY=null 처리
 * - 양방향 관계 문제 발생 -> @JsonIgnore
 */
 @GetMapping("/api/v1/simple-orders")
 public List<Order> ordersV1() {
 List<Order> all = orderRepository.findAllByString(new OrderSearch());
 for (Order order : all) {
 order.getMember().getName(); //Lazy 강제 초기화
 order.getDelivery().getAddress(); //Lazy 강제 초기환
 }
 return all;
 }

 

- 일단 엔티티 직접 노출부터 문제

- 가져온 것을 바로 리턴하면 무한 루프 문제 발생합니다.

   

   해결법1: @JsonIgnore

 Order와 관련된 모든 Lazy로딩 (프록시 객체)에 @JsonIgnore를 사용해야함

 양방향은 한쪽에 JsonIgnore를 붙여준다. 

	//Order
    @JsonIgnore
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @JsonIgnore
    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;
    
    //Member
    @JsonIgnore
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
    
    //orderItem
    @JsonIgnore
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
    //..

 

해결법2: Hibernate5JakartaModule 등록 -> 지연로딩일 경우 response를 제외시켜준다.

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5-
jakarta'

 

- 초기화된 프록시 객체만 노출 초기화 안된건 노출 안함 

- Json으로 만들 때 강제 지연로딩 설정 : 다 불러와서 초기화하라고 설정하는 것 

 Hibernate5Module hibernate5Module = new Hibernate5Module();
 //강제 지연 로딩 설정
 hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING, 
true);
 return hibernate5Module;

 

해결법 3: 리턴전에 Lazy강제 초기화 -> N+1문제 발생 (애플리케이션 레벨에서 지연로딩 풀어주기)

 

 - 물론 이 옵션을 켜면 order -> member, member -> orders 양방향 연관관계를 계속 로딩해야함 따라서 @JsonIgnore를 한 곳에 주어야함 (엔티티 직접 노출 + 양방향 관계에선 반드시 한쪽에 @JsonIgnore 걸어야함)

 

- em.find는 성능과 별개 (어차피 식별자로 불러오는거라) 즉시로딩은 jpql에서 성능문제 발생 (연관관계만큼 select)

- 항상 지연로딩 -> 성능 최적화 필요시 페치 조인을 사용 

 

 * 3가지 해결법 모두 구린 방법! 결국 엔티티를 그냥 리턴하기위한 궁리일 뿐이다.

 

2. 엔티티 DTO로 변환 

 

@GetMapping("/api/v2/simple-orders")
public List<SimpleOrderDto> ordersV2() {
 List<Order> orders = orderRepository.findAllByString(new OrderSearch());
 List<SimpleOrderDto> result = orders.stream()
 .map(o -> new SimpleOrderDto(o))
 .collect(toList());
 return result;
}
@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();
 }
}

 

 - 엔티티를 DTO로 바꾸는 일반적인 방법임 

 - 하지만 지연로딩때문에 1 + N + N 실행 

     -> order 1번조회 (member 지연로딩 2개, 주소 지연로딩 2개) 

     -> 두 지연로딩을 풀기위해 추가로 select 4번 나감 

 

 

3. DTO 변환 + 페치조인 최적화 

 

@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(toList());
 return result;
}


//repository
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();
}

 

- 패치조인용으로 따로 메서드를 생성했다.

- 패치 조인을 통해 쿼리가 1번만 나가서 엔티티를 채웠다! (이미 조회 상태이므로 지연로딩 x)

 

4. 한발 더 나아가서 DTO로 직접 조회하기 

@GetMapping("/api/v4/simple-orders")
public List<OrderSimpleQueryDto> ordersV4() {
 return orderSimpleQueryRepository.findOrderDtos();
}

public List<OrderSimpleQueryDto> findOrderDtos() {
 return em.createQuery(
 "select new 
jpabook.jpashop.repository.order.simplequery.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();
 }

 

 - value object나 Entity 제외하고, jqpl의 리턴값을 만드려면 new 연산자를 사용해줘야함 (queryDSL에서 이를 해결한다함)

- DTO를 바로 만들어서 리턴하면, 애플리케이션 레벨에서 변환 로직이 빠지고, 조회결과 데이터 수가 줄어서 성능 최적화를 노려볼 수 있음 

 - 하지만 리포지토리 재사용성이 떨어지고, API 스펙에 맞춘 코드가 리포지토리에 들어감 (상충관계있음)

 

 

* 권장 순서 

  엔티티 -> DTO는 무조건 지켜야한다.

  엔티티에 지연로딩이 걸려있어 성능 최적화 필요시 페치조인을 사용한다.

  더 나아가 성능을 최적화하고 싶을 때만 DTO직접조회 방식을 사용해본다.

  최후의 방법은 네이티브 SQL 혹은 JDBC Template를 사용해서 직접 SQL을 사용한다.