실전! 스프링 부트와 JPA 활용2 (API 개발과 성능 최적화) - 김영한

  •  Rest API를 구현하며 알아보는 Entity를 반환하는 6가지 방식

 

Version 1. 엔티티를 직접 노출하는 방식

    • [문제]
    • 엔티티를 직접 반환하는 방식은 중요한 정보가 노출되거나
      필요하지 않은 데이터까지 불러 데이터 구조가 비대해질 수 있다.
      연관관계까지 호출할 경우, 역시 모든 속성을 호출함으로 비대해진다.
    • 양방향 연관관계일 경우 한 쪽 컬럼에 @JsonIgnore을 설정하여 무한루프가 빠지지 않도록 주의해야한다.
      연관관계는 fetch option을 lazy로 설정하면 호출되지 않기 때문에
      lazy로 설정한 후 필요한 경우 속성을 호출하는 방식으로 사용하는 것을 권장한다.

 

Version 2. 엔티티를 DTO로 변환하는 방식

    • stream을 이용한 entity -> dto 변환
      stream에서 filter, map을 사용해 중간변환을 하고 find 또는 collection 형태로 최종 가공.
    • 필요한 속성 중심으로 가공할 수 있어 깔끔하게 전달 가능
    • [문제] 여전히 쿼리 반환 시 모든 데이터가 호출되기 때문에 성능 상 문제가 생길 수 있음

 

Version 3. 엔티티를 DTO로 변환하며 페치 조인으로 최적화

    • jpql에서는 join fetch
      querydsl에서는 join() 후 fetchJoin()
    • fetch join을 이용하여 한 번의 쿼리로 연관관계까지 모두 호출하도록 한다.
      그렇게 되면 지연 호출(lazy)로 인한 추후 연관관계 호출 시
      추가적인 쿼리 발생이 일어나지 않으며,
      크로스 조인(cross join)으로 인한 데이터 중복 호출도 발생하지 않는다.
    • 일대다 컬렉션 조인이 있을 경우, select distinct로 데이터 뻥튀기를 해결해줘야 한다.
    • [문제]
    • fetch join의 경우, 일대다 컬렉션 연관관계는 한 개 밖에 사용하지 못한다는 한계가 있다.
    • 또한 컬렉션 페치 조인을 사용하면 페이징이 불가능하다.
      모든 데이터를 읽어온 후, 메모리에서 페이징을 한다;;

 

Version 3.1. 컬렉션 페치 조인 - 페이징 한계 돌파

    • 일대다 관계에서 페이징을 하려면 일(1)을 기준으로 해야되지만
      컬렉션 페치 조인을 사용할 경우 다(N)이 기준이 되어 row가 생성된다.
      -> 하이버네이트는 이러한 문제로 메모리에서 페이징을 진행한다;
    • [해결]
    • ToOne 연관관계는 fetch join으로 호출한다.
    • ToMany 컬렉션 연관관계는 지연로딩으로 조회한다.
    • 이 때, hibernate.default_batch_fetch_size 또는 @BatchSize을 이용한다.
      이 옵션은 컬렉션 또는 프록시 객체를 설정한 size만큼 in query로 조회한다.
      1 + N 쿼리에서 1 + 1 쿼리로 최적화 된다.

 

Version 4. JPA에서 DTO 직접 조회

    • 특정 용도에 필요한 컬럼들로만 구성한 DTO로 조회
    • jpql의 경우, select new package명.dto.ResponseDto(컬럼...) 및 클래스 명시
      querydsl의 경우, select(QueryProjections.constructor(Dto.class, 컬럼...))
    • 역시, 컬렉션 조회 시 1+N 문제가 있고, 해당 문제는 아래에서 다룸

 

Version 5. JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화

    • 컬렉션 연관관계는 일(1)의 id 목록을 파라미터로 전달하여 in query 별도 조회
      stream groupingBy로 일(1)의 id로 다(N)를 묶은 후 -> 일(1)의 속성에 넣어줌
      /** order과 order_item으로 살펴보는 예시 */
      
      // 1. order 목록 호출
      List<OrderQueryDto> result = findOrders();
      
      // 2. order의 id 추출
      List<Long> orderIds = result.stream()
          .map(o -> o.getOrderId())
          .collect(Collectors.toList());
      
      // 3. order id로 order item 호출
      List<OrderItemQueryDto> orderItems = em.createQuery(
          "select new ...OrderItemQueryDto(컬럼...)" +
          " from OrderItem oi" +
          " join oi.item i" + // 참고로, fetch join은 엔티티 조회에서만 가능
          " where oi.order.id in :orderIds", OrderItemQueryDto.class)
          .setParameter("orderIds", orderIds)
          .getResultList();
      
      // 4. order item을 order id로 grouping
      Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
          .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));
      
      // 5. order에 order item 매핑해주기
      result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));

 

Version 6. JPA에서 DTO로 직접 조회 - 플랫 데이터 최적화

    • 쿼리 한 번으로 모든 데이터 조회
    • [문제]
    • 조인으로 인해 중복 데이터 생성될 수 있다.
      쿼리에서는 데이터를 모두 호출하기 때문에 애플리케이션에서 추가 작업이 발생할 수 있다.
      페이징이 불가능하다.

댓글을 작성해보세요.