본문 바로가기
Back-end/Spring Boot + REST API

스프링부트 API JPA 최적화 페치조인, 페이징 default_batch_fetch_size

by javapp 자바앱 2023. 12. 23.
728x90

 

 

페치조인, 페이징

 

 

ERD

 

 

1. 페이징

엔티티 조회

* 선호하는 방법 중 하나 

@GetMapping("/api/v3-1/orders")
public ResponseEntity<Result<List<OrderDto>>> orderV3_page(
        @RequestParam(value = "offset", defaultValue = "0") String offset,
        @RequestParam(value = "limit", defaultValue = "100") String limit
) {
    // N 만큼 데이터 나옴
    List<Order> orders = repository.findAllWithMemberDelivery(offset, limit); // ToOne 관계이기 때문에 페이징 가능
    List<OrderDto> result = orders.stream()
            .map(o -> new OrderDto(o))
            .toList();
    return ResponseEntity.ok(new Result<>(result));
}

컬렉션을 페치 조인하면 페이징이 불가능

 

Hibernate: 
    select
        distinct o1_0.order_id,
        d1_0.delivery_id,
        d1_0.city,
        d1_0.street,
        d1_0.zipcode,
        d1_0.status,
        m1_0.member_id,
        m1_0.city,
        m1_0.street,
        m1_0.zipcode,
        m1_0.name,
        o1_0.order_date,
        o2_0.order_id,
        o2_0.order_item_id,
        o2_0.count,
        i1_0.item_id,
        i1_0.dtype,
        i1_0.name,
        i1_0.price,
        i1_0.stock_quantity,
        i1_0.artist,
        i1_0.etc,
        i1_0.author,
        i1_0.isbn,
        i1_0.actor,
        i1_0.director,
        o2_0.order_price,
        o1_0.status 
    from
        orders o1_0 
    join
        member m1_0 
            on m1_0.member_id=o1_0.member_id 
    join
        delivery d1_0 
            on d1_0.delivery_id=o1_0.delivery_id 
    join
        order_item o2_0 
            on o1_0.order_id=o2_0.order_id 
    join
        item i1_0 
            on i1_0.item_id=o2_0.item_id

한번의 쿼리로 조회

단점 : 페이징 불가능

 

참고: 컬렉션 페치 조인을 사용하면 페이징이 불가능하다.

하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고(위험함), 메모리에서 페이징 해버린다(매우 위험하다).

Order의 기준자체가 틀어져버림

 

자세한 내용은 자바 ORM 표준 JPA 프로그래밍의 페치 조인 부분을 참고하자.

 

참고: 컬렉션 페치 조인은 1개만 사용할 수 있다. 컬렉션 둘 이상에 페치 조인을 사용하면 안된다. 데이터가 부정합하게 조회될 수 있다.

자세한 내용은 자바 ORM 표준 JPA 프로그래밍을 참고하자

 

주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화

19:33~

 

 

 

페이징 방법

 

 

 

 

1. ToOne (OneToOne, ManyToOne) 관계는 모두 페치조인

ToOne 관계인 delivery, member는 페치조인이나 한방쿼리

페이징에 영향주지 않음 

 

 

public List<Order> findAllWithMemberDelivery(String offset, String limit) {
    return em.createQuery(
                    "SELECT o FROM Order o" +
                            " JOIN FETCH o.member m" +
                            " JOIN FETCH o.delivery d", Order.class)
            .setFirstResult(Integer.parseInt(offset))
            .setMaxResults(Integer.parseInt(limit))
            .getResultList();
            // SELECT o FROM Order o
            // 이렇게 하는 것보다 member, delivery 에 나가는 쿼리를 줄일 수 있다.
}

 

 

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

3. 지연 로딩 성능 최적화

hibernate.default_batch_fetch_size : 글로벌 설정

@BatchSize : 특정 최적화

컬렉션, 프록시 객체를 한꺼번에 설정한 size 만큼 in 쿼리로 조회

갯수만큼 미리 당겨옴

@Getter
static class OrderDto {
    private Long orderId;
    private String name;
    private LocalDateTime orderDate;
    private OrderStatus orderStatus;
    private Address address;
    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();
        
        // ToMany관계, 조회시 지연로딩(Lazy)으로 조회 - 프록시 초기화 되면서 가져옴 
        orderItems = order.getOrderItems().stream()
                .map(orderItem -> new OrderItemDto(orderItem))
                .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.getOrderPrice();
    }
}

 

 

배치 사이즈 설정

application.yml

spring:
	jpa:
        show_sql: true
        hibernate:
            ddl-auto: update
        properties:
            hibernate:
                format_sql: true
                default_batch_fetch_size: 100 # ToMany 관계 페이징 + 성능최적화 ( where in 으로 조건 쿼리)

N + 1 문제 해결 (in 쿼리로 인해)

지연로딩으로 OrderItem 을 가져올 때 

orderItems = order.getOrderItems()

.stream().map(orderItem -> new OrderItemDto(orderItem)).toList();

 

 

개별적용도 가능

    @BatchSize(size = 1000) // 개별 batch size 설정 (in 쿼리)
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

 

 

효과

    적용 전

 

적용 후

성능을 더 올리고 싶다면 -> Redis

 

 

장점

  • 쿼리 호출 수가 1+N → 1+1 로 최적화 된다.
  • DB데이터 전송량이 최적화 , 각각 조회하므로 전송해야할 중복 데이터가 없다.
  • 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소
  • 페이징 가능
ToOne 관계는 페치 조인으로 쿼리 수를 줄이고
나머지는 "hibernate.default_batch_fetch_size" 로 최적화 

size는 100~1000사이 권장 (500)

SQL IN 절을 사용하기에, 1000개 이상일 때 오류를 일으니키는 DB가 있음.

 

item 또한 IN 쿼리를 날리면서

쿼리 호출 수가 1:N:N 쿼리가 1:1:1 로 최적화 된다.

order : orderItem: Item

 

 

+ 스프링부트 3.1 변경사항

스프링 부트 3.1 부터는 하이버네이트 6.2를 사용한다. 하이버네이트 6.2 부터는 where in 대신에 array_contains 를 사용한다

 

 

엔티티를 DTO로변환 - 페이징

댓글