본문 바로가기
Back-end/JPA 개념

API 조회 성능 최적화 - 컬렉션(Fetch Join + 페이징 + Batch Size)

by devraphy 2022. 6. 3.

0. 개요

- 이전 포스팅에서 Fetch Join을 사용하면 페이징을 사용할 수 없다는 것을 배웠다.

- 그러나 개발을 하다 보면 Fetch Join과 페이징이 함께 필요한 경우가 발생할 것이다.

- 이러한 상황에서 어떻게 페이징을 구현할 수 있는지 알아보자. 

 

 

1. Fetch Join + 페이징 사용 불가

a) 근본적인 이유

- Fetch Join은 일대다(= OneToMany) 조인에서 사용된다.

- 일대다(= OneToMany) Join의 목적은 일(= One)을 기준으로 Join 하는 것이다.

- 그러나 DB는 다(= Many)를 기준으로 Join 하므로 결과 데이터가 다(= Many)의 개수만큼 생성된다. 

- 이처럼 결과 데이터의 증가로 인해, 데이터의 순서가 뒤틀리게 되어 페이징을 사용할 수 없는 환경이 된다.

- Hibernate의 경우 내부 메모리를 통해 페이징을 수행하는데, 이는 장애로 이어질 가능성이 높다.

- 그렇다면 이 문제를 어떻게 해결할 수 있을까?

 

 

 

2. Fetch Join + 페이징 사용 방법

a) X To One 관계를 모두 Fetch Join 한다 

- 컬렉션 조회 시, 컬렉션 내부의 모든 X To One 관계에 Fetch Join을 사용한다.

- 애초에 X To One 관계는 Join 시, 결과 데이터의 수가 증가되지 않는다.

 

 

b) 컬렉션 내부의 컬렉션은 Lazy Loading을 이용한다.

- 컬렉션 내부에 존재하는 X To Many 관계는 컬렉션 필드로 존재한다.

- 해당 컬렉션 필드는 Lazy Loading을 통해 값을 불러오도록 한다. 

 

 

c) Lazy Loading 성능 최적화 - batch size

- Batch size 설정을 통해 가져올 데이터의 개수를 미리 설정해놓는다.

- 이는 다음 2가지 옵션을 이용하여 설정할 수 있다. 

   → @BatchSize (개별 설정)

   → hibernate.default_batch_fetch_size (hibernate 글로벌 설정)

 

- 이처럼 위에서 설명한 3가지 단계를 거쳐 fetch join과 페이징을 함께 사용할 수 있다.

- 직접 예시를 통해 어떻게 사용하는 것인지 알아보자. 

 

 

3. Fetch Join + 페이징 사용 예시

a) 조회할 컬렉션의 X To One 필드에 Fetch Join 적용

- 우선 조회할 컬렉션 내부의 필드 중 X To One 관계를 가진 필드에 Fetch Join을 적용한다.

- 조회할 컬렉션 내부에 존재하는 컬렉션 필드는 자연스럽게 Lazy Loading으로 사용하게 된다.

- 쿼리는 다음과 같다. 

 

public List<Order> findAllUsingFetchJoin() {
   return em.createQuery(
             "select o from Order o" +
             " join fetch o.member m " +
             " join fetch o.delivery d", Order.class).getResultList();
}

 

- 위의 쿼리를 실행하면 기대한 것처럼 중복 없는 결과가 반환된다.

- 그러나 내부 컬렉션 필드인 OrderItem이 Lazy Loading으로 조회되기 때문에 N + 1 문제가 발생한다.

- 즉, OrderItemr과 OrderItem 내부에 존재하는 Item 테이블 조회 쿼리가 중복적으로 발행된다.

   → Order 테이블에 2개의 주문이 존재하고 각 주문당 2가지 종류의 아이템이 존재한다. 

   → 총 2개의 주문, 4개의 아이템이 존재한다.

   → 그러므로 Order, Member, Delivery 조회(Fetch Join 사용으로 한방 쿼리 1회),

       OrderItem 조회(2회), Item 조회(4회)의 쿼리가 발행된다. 

 

- 이 문제의 해결 방법은 아래에 Batch Size 부분에서 설명하도록 하겠다. 

 

 

b) 페이징 적용

- 현재 X To One 관계만 Fetch Join을 적용했기에 문제없이 페이징을 적용할 수 있다. 

- 페이징을 다음과 같이 구현해보자. 

 

// Repository에 구현한 메서드

public List<Order> findAllUsingFetchJoin(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();
}

 

// API Controller에 구현된 메서드

@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.findAllUsingFetchJoin(offset, limit);
        List<OrderDto> result = orders.stream()
                .map(o -> new OrderDto(o)).collect(Collectors.toList());
        return result;
}

 

- 위의 코드를 실행하면 온전하게 페이징이 구현되는 것을 확인할 수 있다. 

 

 

c) Batch Size 설정

- Batch Size는 hibernate 옵션(= 글로벌) 또는 annotation(= 단일 적용)으로 설정할 수 있다.

- application.yml과 같은 hibernate 옵션을 관리하는 파일에 다음과 같이 설정할 수 있다. 

 

 

 

d) Batch Size의 역할

- Batch Size란, 한 번의 쿼리가 발행될 때마다 가져올 데이터의 양을 설정하는 것이다.

- 이를 설정해놓으면 동일한 테이블 조회에 대해 단일 쿼리가 발행되던 것이 하나의 쿼리로 나간다.

- 즉, Batch Size를 설정하면 다수의 단일 조회 쿼리가 발생하는 문제를 해결할 수 있다. 

 

- 이전에 발행됐던 쿼리는 다음과 같이 감소하게 된다. 

  → Order, Member, Delivery 조회(Fetch Join 사용으로 한방 쿼리 1회)

  → OrderItem 조회(1회), Item 조회(1회)

 

- 이처럼 단일 쿼리로 처리하던 작업이 하나의 쿼리로 변경될 수 있는 이유는 IN 키워드를 사용하기 때문이다.

- 다음 예시 쿼리를 보자. 

 

 

- 기존의 방식에서는 4개의 아이템을 조회하기 위하여 4개의 조회 쿼리가 발행되었다.

- 그러나 Batch Size를 설정하면 위의 사진처럼 IN 키워드를 사용하여 4개의 아이템을 한 번에 조회한다. 

- 이와 같은 용도로 Batch Size를 사용한다. 

 

- Batch Size는 100 ~ 1000 사이를 선택하는 것을 권장한다. 

- Batch Size가 너무 크다면 한 번의 DB 통신에 순간적인 부하가 발생한다.

- 반대로 Batch Size가 너무 작으면 다수의 DB 통신이 발생한다. 

- 그러므로 WAS와 DB가 버틸 수 있는 올바른 수치의 Batch Size를 설정하는 것이 매우 중요하다.   

- 일반적으로 100 ~ 500 사이의 수치를 권장한다. 

 

 

e) 결론

- Fetch Join과 페이징을 함께 사용하는 방법을 정리해보자.

  → X To One 관계의 필드만 Fetch Join을 사용한다.

  → 컬렉션 필드는 Lazy Loading을 사용한다. 

  → Batch Size를 설정하여 동일한 테이블 조회를 단일 조회가 아닌 다중 조회로 사용한다. 

댓글