<컬렉션> Entity를 Dto로 변환

컴공생의 코딩 일기·2023년 1월 31일
0

JPA

목록 보기
10/14
post-thumbnail
post-custom-banner

<컬렉션> Entity를 Dto로 변환

@RestController
@RequiredArgsConstructor
public class OrderApiController {
    private final OrderRepository orderRepository;

    @GetMapping("/api/v2/orders")
    public List<OrderDto> ordersV2(){
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        return all.stream().map(OrderDto::of).collect(Collectors.toList());
    }
   
     record OrderDto(
            Long orderId,
            String name,
            LocalDateTime orderDate,
            Address address,
            List<OrderItems> orderItems
    ){
        public static OrderDto of(Order order){
            return new OrderDto(
                    order.getId(),
                    order.getMember().getName(),
                    order.getOrderDate(),
                    order.getDelivery().getAddress(),
                    order.getOrderItems().stream()
                            .map(OrderItems::of)
                            .collect(Collectors.toList())
            );
        }
    }

    record OrderItems(
            String itemName,
            int orderPrice,
            int count
    ){
        public static OrderItems of(OrderItem orderItem){
            return new OrderItems(
                    orderItem.getItem().getName(),
                    orderItem.getOrderPrice(),
                    orderItem.getCount()
            );
        }

    }
}
  • 모든 Entity 클래스를 Dto로 변환해야 한다. (Dto 클래스 안에 Entity 클래스도 Dto 로 변환해야 한다.)
    • Order -> OrderDto 변환
    • OrderDto안에 OrderItem -> OrderItemDto로 변환
  • 이렇게 실행할 경우 문제점
    • SQL 실행 수
      • order: 1번
      • member, address: N번(order 조회 수 만큼)
      • orderItem: N번(order 조회 수 만큼)
      • item: N번(orderItem 조회 수 만큼)

Entity를 Dto로 변환 - Fetch Join 최적화

// OrderRepository 클래스
public List<Order> findAllWithItem(OrderSearch orderSearch) {
        return em.createQuery("select o from Order o " +
                "join fetch o.member m " +
                "join fetch o.delivery d " +
                "join fetch o.orderItems oi " +
                "join fetch oi.item i", Order.class)
                .getResultList();
    }
@GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV3(){
        List<Order> all = orderRepository.findAllWithItem(new OrderSearch());
        return all.stream().map(OrderDto::of).collect(Collectors.toList());
    }
	...............
  • Fetch Join으로 SQL이 1번만 실행 된다.
  • 하지만 이렇게 사용할 경우 데이터 뻥튀기 현상이 발생한다.
// 위에 코드를 실행한 결과 값 (데이터 뻥튀기 발생)
Order Size: 4
Member Name: userA |Order Id: 7
Member Name: userA |Order Id: 7
Member Name: userB |Order Id: 14
Member Name: userB |Order Id: 14

Entity를 Dto로 변환 - Fetch Join 최적화(distinct 사용)

 public List<Order> findAllWithItem(OrderSearch orderSearch) {
        return em.createQuery("select distinct o from Order o " +
                "join fetch o.member m " +
                "join fetch o.delivery d " +
                "join fetch o.orderItems oi " +
                "join fetch oi.item i", Order.class)
                .getResultList();
    }
// 위에 코드를 실행한 결과 값
Order Size: 2
Member Name: userA |Order Id: 7
Member Name: userB |Order Id: 14
  • distinct로 중복을 제가 할 수 있다.
    • SQL의 distinct는 중복된 결과를 제거하는 명령(값이 하나라도 다르면 중복 제거X)
    • JPQL의 distinct는 2가지 기능 제공
      • SQL에 distinct를 추가
      • 애플리케이션에서 엔티티 중복 제거

Entity를 Dto로 변환 - Fetch Join 최적화 (페이징과 한계 돌파)

  • 컬렉션을 페치 조인하면 페이징이 불가능하다.
    • 컬렉션을 페치 조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
    • 일대다에서 일(1)을 기준으로 페이징을 하는 것이 목적이다. 그런데 데이터는 다(N)을 기준으로 row가 생성된다.
    • Order를 기준으로 페이징하고 싶은데, 다(N)인 OrderItem을 조인하면 OrderItem이 기준이 되어 버린다.
  • 이 경우 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도한다. 최악의 경우 장애로 이어질 수 있다.

Fetch Join 한계 해결 방법

  • 먼저 ToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 한다. (ToOne 관계는 row 수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.)
  • 컬렉션은 지연 로딩으로 조회한다.
  • 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size, @BetchSize를 적용한다.
    • hibernate.default_batch_fetch_size : 글로벌 설정
    • @BatchSize : 개별 최적화
    • 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.
// OrderRepository 클래스
    public List<Order> findAllWithMemberDelivery(OrderSearch orderSearch) {
        return em.createQuery("select o from Order o" +
                " join fetch o.member m" +
                " join fetch o.delivery d", Order.class)
                .setFirstResult(0)
                .setMaxResults(100)
                .getResultList();
    }
    @GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV2(){
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        return all.stream().map(OrderDto::of).collect(Collectors.toList());
    }
    .......
// 최적화 옵션
spring:
 jpa:
   properties:
    hibernate:
     default_batch_fetch_size: 1000

개별로 설정하려면 @BatchSize를 적용하면 된다. (컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용)

장점

  • 쿼리 호출 수가 N + 1 -> 1 + 1로 최적화 된다.
  • 조인보다 DB 데이터 전송량이 최적화 된다. (OrderOrderItem을 조인하면 OrderOrderItem 만큼 중복해서 조회된다. 이 방법은 각각 조회하므로 전송해야할 중복 데이터가 없다.)
  • 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
  • 컬렉션 페치 조인은 페이징이 불가능 하지만 이 방법은 페이징이 가능하다.

결론: ToOne 관계는 페치 조인해도 페이징 처리에 영향을 주지 않는다. 따라서 ToOne 관계는 페치 조인으로 쿼리 수를 줄여 해결하고, 나머지는 hibernate.default_batch_fetch_size로 최적화 하자.
hibernate.default_batch_fetch_size 권장 사이즈 : 100 ~ 1000

profile
더 좋은 개발자가 되기위한 과정
post-custom-banner

0개의 댓글