Spring

컬렉션 조회 최적화

date
Jan 23, 2024
slug
스프링부트-JPA-활용2-4
status
Public
tags
실전! 스프링 부트와 JPA 활용 2
author
summary
[실전! 스프링 부트와 JPA 활용 2] 섹션 4 정리
type
Post
thumbnail
updatedAt
May 28, 2024 12:34 PM
category
Spring
김영한
인프런

🍯 꿀팁


 

📝 강의 정리


💡
주문내역에 주문한 상품 정보를 추가로 조회하자 → Order 기준으로 OrderItemItem 이 필요하다. ( 1 : N )
 

[1]. 엔티티를 직접 노출 deprecated


public List<Order> ordersV1() { List<Order> orders = orderRepository.findAllByString(new OrderSearch()); // Lazy Loading Init for(Order order: orders) { order.getMember().getName(); order.getDelievery().getAddress(); List<OrderItem> orderItems = order.getOrderItems(); orderItems.stream().forEach(o -> o.getItem().getName()); } return orders; } >>> 엔티티 순환참조로 인한 stackoverflow 발생 java.lang.StackOverflowError: null
  • 엔티티를 직접 노출하는 방식은 일단 지양하자.
  • hibernate5 모듈 import 통해 stackoverflow 해결 가능하나, 애초에 좋지 않은 해결법임
 

[2]. 엔티티를 DTO로 변환


@GetMapping("/api/v2/orders") public Result ordersV2() { List<Order> orders = orderRepository.findAllByString(new OrderSearch()); List<OrderDto> result = orders.stream() .map(OrderDto::new) .collect(Collectors.toList()); return new Result(result); } >>> 단일 order 테이블 파싱 public List<Order> findAllByString(OrderSearch orderSearch) { return em.createQuery("SELECT o FROM Order o", Order.class).getResultList(); }
  • List<Order> 로 파싱된 데이터를 OrderDto로 변환
notion image

쿼리 실행 횟수

  • order테이블 조회 + order * (member + delievery + orderItem * item)
    • → 1 + 2 * (1 + 1 + 1 + 2) → 총 11번의 쿼리가 실행됨
       

[3]. Fetch Join 최적화


public List<Order> findAllWithItem() { return em.createQuery( "SELECT o " "FROM Order o " + "JOIN FETCH o.member m " + "JOIN FETCH o.delievery d " + "JOIN FETCH o.orderItems oi " + "JOIN FETCH oi.item i", Order.class).getResultList(); }
  • Fetch Join 을 통해 한번에 fetch후 데이터를 파싱한다
notion image

쿼리 실행 횟수

→ 1번
 

[문제점 1]. 불필요한 데이터의 중복 발생


💡
order: orderItem = 1 : N 관계에서 두 테이블의 조인에 따른 order의 불필요한 중복 발생
notion image
  • JPA 에서 fetch join을 통해 실행된 쿼리를 파싱하는 경우, 조인된 테이블의 레코드를 그대로 파싱해온다. 따라서, N : 1 관계의 테이블 간의 조인에서 발생되는 데이터의 중복을 제어할 수 없다
 

[해결 1]. DISTINCT

public List<Order> findAllWithItem() { return em.createQuery( "SELECT distinct o " + // 조인에 따른 불필요한 order의 데이터 중복 방지 "FROM Order o " + "JOIN FETCH o.member m " + "JOIN FETCH o.delievery d " + "JOIN FETCH o.orderItems oi " + "JOIN FETCH oi.item i", Order.class).getResultList(); }
  • JPA 자체에서 SELECT 절에 DISTINCT 키워드를 제공한다.
notion image
 

[문제점 2]. Collection Fetch Join 사용시 페이징이 불가하다.


💡
해당 쿼리의 모든 데이터를 DB에서 읽어오고, 애플리케이션 서버 메모리 자체에서 페이징을 진행 → 서버 메모리가 고갈될 수 있다
 

[해결 2]. ToOne관계의 테이블만 Fetch Join을 진행하자


//before public List<Order> findAllWithItem() { return em.createQuery( "SELECT distinct o " + // 조인에 따른 불필요한 order의 데이터 중복 방지 "FROM Order o " + "JOIN FETCH o.member m " + "JOIN FETCH o.delievery d " + "JOIN FETCH o.orderItems oi " + "JOIN FETCH oi.item i", Order.class).getResultList(); } //after public List<Order> findAllWithMemberDelievery(int offset, int limit) { return em.createQuery( "SELECT o " + "FROM Order o " + "JOIN FETCH o.member " + "JOIN FETCH o.delievery", Order.class) .setFirstResult(offset) .setMaxResults(limit) .getResultList(); }
  • Order 테이블과 @XToOne 관계의 엔티티(member, delievery)만 fetch join을 진행하고,
    • @ToMany 관계의 엔티티(orderitem, item)은 지연 로딩을 통해 파싱하자
      → @ToOne 관계의 엔티티를 조인하더라도, 쿼리 row의 수가 증가하지 않으므로 불필요한 데이터의 중복이나, 예기치 않은 데이터 정합성의 오류가 발생하지 않는다. 따라서, @XToOne 관계의 엔티티들만 fetch join을 통해 불러온 상태에서, 지연 로딩을 통해 @XToMany 관계의 엔티티를 파싱하면 페이징 처리가 가능해진다.
 
 
orderId, name, orderDate, orderStatus, address, orderitems

📎 출처