[JPA] 페이징과 한계돌파

개발의 숩
|2023. 3. 20. 13:26

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

  • 컬렉션을 페치조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
  • 일대다에서 일(1)을 기준으로 페이징을 하는 것이 목적이다. 그렇데 데이터는 다(N)를 기준으로 row가 생성된다.
  • Order를 기준으로 페이징하고 싶은데 다(N)인 OrderItem을 조인하면 OrderItem이 기준이 되어버린다.
  • 더 자세한 내용은 자바 ORM 표준 JPA 프로그래밍 - 페치 조인 한계 참고

이 경우 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도한다. 최악의 경우 장애로 이어질 수 있다.

 

 

한계 돌파

그러면 페이징과 컬렉션 엔티티를 함께 조회하려면 어떻게 해야할까 ?

지금부터 코드도 단순하고, 성능 최적화도 보장하는 매우 강력한 방법을 소개하겠다.

대부분의 페이징 + 컬렉션 엔티티 조회 문제는 이 방법으로 해결할 수 있다.

 

  • 먼저 ToOne 관계를 모두 페치조인한다. ToOne관계는 row 수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.
  • 컬렉션은 지연 로딩으로 조회한다.
  • 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size @BatchSize 를 적용한다.
    • **hibernate.default_batch_fetch_size : 글로벌 설정**
    • **@BatchSize : 개별 최적화 - 디테일하게 적용하고 싶을 때**
    • 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회한다.
    jpa:
        hibernate:
          ddl-auto: create
        properties:
          hibernate:
            show_sql: true
            format_sql: true
            default_batch_fetch_size: 1000 #최적화 옵션
    
    • 개별로 설정하려면 @BatchSize를 적용하면 된다 (컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에 적용)
    장점
    • 쿼리 호출 수 가 1+N → 1+1로 최적화된다
    • 조인보다 DB 데이터 전송량이 최적화된다.이 방법은 각각 조회하므로 전송해야할 중복 데이터가 없다
    • Order와 OrderItem을 조인하면 Order가 OrderItem만큼 중복해서 조회된다.
    • 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소한다.
    • 컬렉션 페치 조인은 페이징이 불가능하지만 이 방법은 페이징이 가능하다
    결론
    • ToOne 관계는 페치 조인해도 페이징에 영향을 주지 않는다. 따라서 ToOne관계는 페치조인으로 쿼리 수를 줄이고 해결하고, 나머지는 hibernate.default_batch_fetch_size 로 최적화하자
    참고: default_batch_fetch_size 의 크기는 적당한 사이즈를 골라야 하는데, 100 ~ 1000 사이를 선택하는 것을 권장한다. 이 전략은 SQL IN절 사용하는데, 데이터베이스에 따라 IN절 파라미터를 1000으로 제한하기도 한다. 1000으로 잡으면 한번에 1000개를 DB에서 애플리케이션에 불러오므로 DB에 순간 부하가 증가할 수 있다. 하지만 애플리케이션은 100이든 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다. 1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정하면 된다.
    • 한번에 조회
    • 데이터가 최종 갯수만큼 나오는데 중복인 된 상태에서 조회가 된다
    • DB에서 애플리케이션으로 전부 전송하게 되어, 전송량 자체가 많아지게 된다
    • 용량이 많은 이슈
    public List<Order> findAllByMemberDelivery(int offeset, int limit) {
            return em.createQuery(
                            "select o from Order o"+
                                    " join fetch o.member m "+
                                    " join fetch o.delivery d ",Order.class)
                    .setFirstResult(offeset)
                    .setMaxResults(limit)
                    .getResultList();
        }
    
    • 데이터 전송량이 쿼리가 최적화되어서 전송되기때문에 용량이슈가 발생하지 않는다
    • 데이터는 중복없이 조회가 가능하다
    • 정확하게 필요한 데이터를 조회하게 된다.
    public List<Order> findAllByMemberDelivery(int offeset, int limit) {
            return em.createQuery(
                            "select o from Order o",Order.class)
                    .setFirstResult(offeset)
                    .setMaxResults(limit)
                    .getResultList();
        }
    
    • batch size 전략때문에 성능 최적화가 된 상태에서 데이터를 가져올 수 있다.
    • 아무래도 네트워크를 많이 타서 가져오게 되므로 fetch join을 미리 잡아놓고 가져오는게 효율적이다

 

 

'TIL' 카테고리의 다른 글

[JPA] OSIV와 성능 최적화  (0) 2023.03.22
[JPA] API 개발 고급 정리  (1) 2023.03.21
[JPA] N+1 문제  (0) 2023.03.19
[JPA] respository에 DTO 사용 ?  (0) 2023.03.18
[JPA] DTO 설계 이유  (0) 2023.03.18