API개발 고급(지연로딩/조회성능 최적화)

Mina Park·2022년 9월 24일
0
  • 주문조회 예시
    (1) ver1: 엔티티를 직접 노출
@GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAll(new OrderSearch());
        for (Order order : all) {
            order.getMember().getName(); //LAZY 프록시 초기화
            order.getDelivery().getAddress(); //LAZY 프록시 초기화
        }
        return all;
        //문제점: 무한루프 장애 발생(Order -> Member -> Order -> Member...)
    }
  • 📌 엔티티를 직접 노출할 경우 양방향 연관관계가 걸린 곳은 꼭! 한곳을 @JsonIgnore 처리
    • 안그러면 양쪽을 서로 호출하면서 무한 루프 발생
  • 📌 엔티티의 외부노출은 가급적 지양 => DTO로 변환하여 반환
  • 📌 지연로딩을을 피하기 위해 즉시로딩 사용은 X

(2) ver2: 엔티리를 DTO로 변환

@GetMapping("/api/v2/simple-orders")
   public List<SimpleOrderDto> ordersV2() {
       List<Order> orders = orderRepository.findAll(new OrderSearch());
       List<SimpleOrderDto> collect = orders.stream()
               .map(m -> new SimpleOrderDto(m))
               .collect(Collectors.toList());
       return collect;
       //문제점: 1+N 문제 발생
   }
   
   @Data
   static class SimpleOrderDto {
       private Long orderId;
       private String name;
       private OrderStatus orderStatus;
       private Address address;

       public SimpleOrderDto(Order order) {
           orderId = order.getId();
           name = order.getMember().getName(); //LAZY 프록시 초기화
           orderStatus = order.getStatus();
           address = order.getDelivery().getAddress(); //LAZY 프록시 초기화
       }
   }
  • 📌 N+1 문제가 발생
    • 지연로딩은 영속성 컨텍스트에서 조회하므로 이미 있을 경우 쿼리가 생략되긴 하지만, 최악의 경우를 상정했을 때 연관관계로 인해 1+N+N... 쿼리가 발생
    • 예) order 결과가 10개라면, 1(order 조회) + 10(member조회) + 10(delivery조회) 쿼리

(3) ver3: fetch join

  • 쿼리 한 번에 전부 조회
    • member, delivery 자체를 같이 가져오므로 지연로딩 X
    • 실무에서 매우 유용하게 사용
  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();
      }
  @GetMapping("/api/v3/simple-orders")
      public List<SimpleOrderDto> ordersV3() {
          List<Order> orders = orderRepository.findAllWithMemberDelivery();
          List<SimpleOrderDto> collect = orders.stream()
                  .map(o -> new SimpleOrderDto(o))
                  .collect(Collectors.toList());
          return collect;
      }
    select
          order0_.order_id as order_id1_6_0_,
          member1_.member_id as member_i1_4_1_,
          delivery2_.delivery_id as delivery1_2_2_,
          order0_.delivery_id as delivery4_6_0_,
          order0_.member_id as member_i5_6_0_,
          order0_.order_date as order_da2_6_0_,
          order0_.status as status3_6_0_,
          member1_.city as city2_4_1_,
          member1_.street as street3_4_1_,
          member1_.zipcode as zipcode4_4_1_,
          member1_.name as name5_4_1_,
          delivery2_.city as city2_2_2_,
          delivery2_.street as street3_2_2_,
          delivery2_.zipcode as zipcode4_2_2_,
          delivery2_.status as status5_2_2_ 
      from
          orders order0_ 
      inner join
          member member1_ 
              on order0_.member_id=member1_.member_id 
      inner join
          delivery delivery2_ 
              on order0_.delivery_id=delivery2_.delivery_id

(4) ver4: dto로 바로 조회

  • JPQL 결과를 dto로 즉시 변환
    • 일반적인 SQL 사용할 때처럼 원하는 필드만 선택해서 조회
@GetMapping("/api/v4/simple-orders")
    public List<OrderSimpleQueryDto> ordersV4() {
        return orderRepository.findOrderDtos();
    }
public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime orderDate, OrderStatus orderStatus, Address address) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
    }
//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();
    }
 select
        order0_.order_id as col_0_0_,
        member1_.name as col_1_0_,
        order0_.order_date as col_2_0_,
        order0_.status as col_3_0_,
        delivery2_.city as col_4_0_,
        delivery2_.street as col_4_1_,
        delivery2_.zipcode as col_4_2_ 
    from
        orders order0_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id 
    inner join
        delivery delivery2_ 
            on order0_.delivery_id=delivery2_.delivery_id

📌 ver3처럼 엔티티를 DTO로 변환 VS ver4처럼 DTO로 바로 조회

  • 전자의 경우 엔티티로 조회하면 리포지토리 재사용성이 좋고 개발도 단순
  • 후자의 경우 원하는 데이터를 직접 선택하므로 네트워크 용량이 더 최적화
    • but 리포지토리 재사용성이 떨어지고 API스펙에 맞춘 코드가 리포지토리에 들어가는 단점
    • 이렇게 화면에 맞춘 코드가 필요할 경우 쿼리용 리포지토리 별도로 분리(화면 조회 전용)
    • 기본 리포지토리는 엔티티를 순순하게 조회하는 용도


📌 쿼리방식 선택 권장순서
1. 우선 엔티티를 DTO로 변환(유지보수성)
2. 필요시 fetch join으로 성능 최적화 -> 대부분의 성능이슈는 여기서 해결
3. 그래도 안되면 DTO로 직접 조회
4. 최후의 방법은 JPA가 제공하는 네이티브 SQL, 스프링 JDBC Template을 사용해서 SQL 직접 사용

0개의 댓글