본문 바로가기

카테고리 없음

JPA 실전2 - 컬렉션 조회 최적화

권장순서

1. 엔티티 조회 방식으로 우선 접근
  1. 페치조인으로 쿼리 수를 최적화
  2. 컬렉션 최적화
      1. 페이징 필요 hibernate.default_batch_fetch_size , @BatchSize 로 최적화
      2. 페이징 필요X 페치 조인 사용
2. 엔티티 조회 방식으로 해결이 안되면 DTO 조회 방식 사용
3. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate

 

우선 엔티티 조회방식을 우선 권장함.( Batch size로 해결이 안되면 사실 캐시(Redis같은거)쓰는게 맞음.)

요즘 네트워크 성능이 좋아서 엔티티방식이나 DTO방식이나 별 차이 안남.

(참고로 엔티티는 캐시에 올리면 안되고 무조건 DTO로 변환해서 DTO를 캐시해야함)

 

엔티티 조회방식은 JPA가 많은 부분을 최적화해주기때문에, 단순한 코드를 유지하면서 성능을 최적화할 수 있다.


 

API 개발 고급 정리


엔티티 조회

엔티티를 조회해서 그대로 반환: V1

엔티티 조회 후 DTO로 변환: V2
페치 조인으로 쿼리 수 최적화: V3
컬렉션 페이징과 한계 돌파: V3.1

  • 컬렉션은 페치 조인시 페이징이 불가능
  • ToOne 관계는 페치 조인으로 쿼리 수 최적화
  • 컬렉션은 페치 조인 대신에 지연 로딩을 유지하고, hibernate.default_batch_fetch_size  또는 @BatchSize 로 최적화

DTO 직접 조회

JPA에서 DTO를 직접 조회: V4
컬렉션 조회 최적화 - 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화: V5
플랫 데이터 최적화 - JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환: V6


 

나는 엔티티조회방식만 정리해놓겠다.

 

준비단계)

엔티티를 조회한 다음 엔티티를 DTO로 변환해서 반환을 하게되는데 

이때 DTO안에 엔티티가 존재하면 안된다. 아예 의존을 끊어주어야함.(Address같은 값타입은 괜찮음)

 

따라서 컨트롤러 내에 DTO클래스를 작성할 때 아래와 같이 작성해야한다.

@Getter
static class OrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    /*private List<OrderItem> orderItems; 이것조차 dto로 바꿔야함.*/
    private List<OrderItemDto> orderItems;


    public OrderDto(Order order) {
        orderId = order.getId();
        name = order.getMember().getName();
        orderDate = order.getOrderDate();
        orderStatus = order.getStatus();
        address = order.getDelivery().getAddress();
        /*order.getOrderItems().stream().forEach(o -> o.getItem().getName());*/
        orderItems = order.getOrderItems().stream()
                .map(orderItem -> new OrderItemDto(orderItem))
                .collect(toList());

    }
}
@Getter
static class OrderItemDto{
    // 필요한 스펙 넣어주기
    private String itemName; // 상품명
    private int orderPrice; // 주문 가격
    private int count; //주문 수량
    public OrderItemDto(OrderItem orderItem) {
        itemName = orderItem.getItem().getName();
        orderPrice = orderItem.getOrderPrice();
        count = orderItem.getCount();
    }
}

 

v3

 

Hibernate6에서는 페치조인을 하면 자동으로 중복제거를 해서 페치조인을 할때 distinct를 안써도 자동으로 중복제거 가능.

페치조인을 하는 경우 어마어마한 단점: 컬렉션 페치 조인은 페이징이 불가능하다. 1:다 관계는 outOfMemory터져버림.

 

만약 페이징이 필요없고 data가 얼마없는 경우에는 fetch join으로 이렇게 해도 상관은 없다. 빠르다.

@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3(){
   List<Order> orders = orderRepository.findAllWithItem();
    List<OrderDto> collect = orders.stream()
            .map(o -> new OrderDto(o))
            .collect(toList());
    return collect;
}

 

아래는 repository

public List<Order> findAllWithItem() {
    // 실무에서는 querydsl로 간단하게 짤 수 있다.
    // 하이버네이트6에서는 패치조인을 하면 자동으로 중복제거를 해준다.(distinct안써도됨)
    // distinct넣어도 DB에서는 적용안됨(값이 완전히 똑같이 않으니까), JPA에서 리스트담을때 엔티티 중복제거하는 용도임.
    return em.createQuery(
            /*"select distinct o from Order o " +*/
                    "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)
            // firstResult/maxResults specified with collection fetch; applying in memory
            // 다불러와서 페이징을 하겠다는거. outOfMemory 에러 터지기 딱 좋음.
            .setFirstResult(1)
            .setMaxResults(100)
            .getResultList();
}

 

 

v3.1

 

ToOne관계는 join fetch와 페이징이 가능하다.

ToMany관계는 지연로딩 + @BatchSize(size=1000)또는 글로벌 세팅을 해준다.

90%이상의 성능최적화는 이 level에서 해결된다.

 

페이징이 필요한 경우 이 방법을 선호 한다.

/**
 * 페이징 써야할 경우 이 방법 선호.
 * ToOne관계는 joinfetch와 페이징 가능.
 * ToMany관계는 지연로딩 + @BatchSize(size=1000) 또는 글로벌 세팅
 *  yml파일에 default_batch_fetch_size: 100~1000 사이를 권장합니다.
 *  그런데 1000개 하면 DB,애플리케이션 순간 부하가 확 올 수 있다.
 *  was,DB 순간부하 걱정되면 100~500으로 쓰기
 *
 *  90%이상의 성능최적화는 이 level에서 해결된다.
 */
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(
        @RequestParam(value = "offset",defaultValue = "0") int offset,
        @RequestParam(value = "limit",defaultValue = "100") int limit)
{
    List<Order> orders = orderRepository.findAllWithMemberDelivery(offset,limit);

    List<OrderDto> collect = orders.stream()
            .map(o -> new OrderDto(o))
            .collect(toList());
    return collect;
}

 

아래는 repository

/**
 * v3.1
 *
 * http://localhost:8080/api/v3.1/orders?offset=1&limit=100
 * ToOne관계는 joinfetch와 페이징 가능.
 * & yml파일에 default_batch_fetch_size: 배치크기 넣어주면 최적화 가능! 쿼리 3번만에 끝.
 */
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
    return em.createQuery(
            "select o from Order o " +
                    "join fetch o.member m " +
                    "join fetch o.delivery d",Order.class)
            .setFirstResult(offset)
            .setMaxResults(limit)
            .getResultList();
}

 

To One관계는 모두 페치조인을 한다.(ToOne관계는 row수를 증가시키지않아서 페이징 쿼리에 영향을 주지 않음.)

컬렉션은 지연로딩으로 조회한다.

지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size , @BatchSize 를 적용한다.

  • hibernate.default_batch_fetch_size: 글로벌 설정
  • @BatchSize: 개별 최적화

이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.

 

최적화 옵션은

spring:
   jpa:
     properties:
       hibernate:
         default_batch_fetch_size: 1000

또는 개별로 설정시 @BatchSize를 컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용한다.