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
김영한
인프런
🍯 꿀팁📝 강의 정리[1]. 엔티티를 직접 노출 deprecated[2]. 엔티티를 DTO로 변환쿼리 실행 횟수[3]. Fetch Join 최적화쿼리 실행 횟수[문제점 1]. 불필요한 데이터의 중복 발생[해결 1]. DISTINCT[문제점 2]. Collection Fetch Join 사용시 페이징이 불가하다.[해결 2]. ToOne관계의 테이블만 Fetch Join을 진행하자📎 출처
🍯 꿀팁
📝 강의 정리
주문내역에 주문한 상품 정보를 추가로 조회하자
→
Order
기준으로 OrderItem
과 Item
이 필요하다. ( 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로 변환

쿼리 실행 횟수
- 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후 데이터를 파싱한다

쿼리 실행 횟수
→ 1번
[문제점 1]. 불필요한 데이터의 중복 발생
order: orderItem = 1 : N 관계에서 두 테이블의 조인에 따른 order의 불필요한 중복 발생

- 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 키워드를 제공한다.

[문제점 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