JPA

컬렉션 fetch join 및 페이징 applying in memory 오류

MIN우 2023. 8. 2. 11:10
728x90

이번에 프로젝트를 하는 도중에

2023-08-01 23:49:22.440  WARN 2334 --- [nio-8080-exec-7] o.h.h.internal.ast.QueryTranslatorImpl   : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
2023-08-01 23:49:22.441 DEBUG 2334 --- [nio-8080-exec-7] org.hibernate.SQL

이런 경고로그가 발생했다

 

어 뭐지 ..? 코드를 다시보니

      JPAQuery<Board> contentQuery = selectFrom(board)
                .leftJoin(board.comments, comment).fetchJoin()
                .where(categoryEq(condition.getCategory()),recruitmentPeriodPredicate,activityFieldExpression)
                .orderBy(getOrderSpecifier(pageable.getSort()).stream().toArray(OrderSpecifier[]::new))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize());

        List<Board> content = contentQuery.fetch();

        JPAQuery<Long> countQuery = select(board.count())
                .from(board)
                .where(categoryEq(condition.getCategory()))
                .where(recruitmentPeriodPredicate);

        long totalCount = countQuery.fetchOne();
        return new PageImpl<>(content, pageable, totalCount);

board 1 : comment N 구조로 되어있으며 

해당하는 board에 대한 comment 를 가져오려고 전부 fetch join으로 가져오고 있었지만 

페이징하는과정에서 오류가 발생했다.

 

하이버네이트는 경고 로그를 남기고 모든 DB데이터를 읽어서 메모리에서 페이징을 처리한다.

즉 Limit를 사용하지않고 조회된 모든 결과를 Java Memory에 올려놓고 Pagination 을 처리한다.

 

지금은 데이터가 별로 없어서 정상적으로 작동하지만 나중에는 memory leak가 발생하여

성능의 악영향을 줄 수 있다.

 

 

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

  • 컬렉션을 페치 조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가한다.
  • 일다대에서 일(board 1)을 기준으로 페이징을 하는 것이 목적이다. 그런데 데이터는 다(Comment N)를 기준으로 row가 생성된다.
  • Board를 기준으로 페이징 하고 싶은데, 다(N) Comment기준이 되어버린다.

 

내가 정리해 둔 글... 

 

1. 엔티티 조회방식으로 우선 접근 
     A. 페치조인으로 쿼리수를 최적화 
     B. 컬렉션 최적화 
         i. 페이징필요 (hibernate.default_batch_fetch_size, @BatchSize 로 최적화 
         ii.페이징필요 x -> 페치조인사용 

2. 엔티티조회방식으로 해결이 안되면 DTO조회방식사용 
    i. ToOne 관계에서는 V4를 사용하고 
    ii. ToMany관계에서는 V5를 사용하여 IN절을 활용해서 메모리에 미리조회해서 최적화를 시킨다. 

3. DTO조회방식으로 해결이 안되면 NativeSQL or JdbcTemplate  


참고: 엔티티 조회 방식은 페치 조인이나, hibernate.default_batch_fetch_size, @BatchSize 같이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다. 반면 DTO로 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다. 

참고:Entity를 직접조회하면서 fetch 조인이나 batch를 넣어서 해결이 안된다 ? 이정도면  
서비스가 트래픽이 무진장 많을것이다. redis같은 캐시를 사용해서 해결을 해야지 DTO조회방식으로 
해결이 될까 ?라는 의문이있다.(대부분의 서비스는 fetch 조인만으로도 성능이 나온다) 

참고: Entity는 직접캐시하면안된다. Entity는 영속성컨텍스트에서 관리되기때문에 캐시에 잘못 올라갈경우 
안지워지니 꼬일수있다. Entity 를 Dto로 변환후 DTO는 캐시에 넣어야한다. 

문제 해결     

B. 컬렉션 최적화 
 i. 페이징필요 (hibernate.default_batch_fetch_size, @BatchSize 로 최적화 

이 방법으로 처리하였다.

toOne 관계는 전부 fetch join으로 가져온다.

ToOne관계 row수를 증가시키지않으므로 페이징 쿼리에 영향을 주지 않는다.

컬렉션은 지연로딩으로 가져온다.

 

보통은 컬렉션을 지연로딩으로 가져온다고 하면 N+1을 고려할 것이다. 하지만

 

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

한다면 ?

 

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

 

  • where 절을 보게 되면 한꺼번에 설정한 size 만큼 IN 쿼리로 조회한다.
  • default_batch_fetch_size 개수만큼 모일 때까지 쌓아두었다가, 해당 개수가 다 모이면 쿼리를 보낸다.

 

정리

1. 엔티티 조회방식으로 우선 접근 
     A. 페치조인으로 쿼리수를 최적화 
     B. 컬렉션 최적화 
         i. 페이징필요 (hibernate.default_batch_fetch_size, @BatchSize 로 최적화 
         ii.페이징필요 x -> 페치조인사용 

2. 엔티티조회방식으로 해결이 안되면 DTO조회방식사용 
    i. ToOne 관계에서는 V4를 사용하고 
    ii. ToMany관계에서는 V5를 사용하여 IN절을 활용해서 메모리에 미리조회해서 최적화를 시킨다. 

3. DTO조회방식으로 해결이 안되면 NativeSQL or JdbcTemplate  


참고: 엔티티 조회 방식은 페치 조인이나, hibernate.default_batch_fetch_size, @BatchSize 같이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다. 반면 DTO로 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다. 

참고:Entity를 직접조회하면서 fetch 조인이나 batch를 넣어서 해결이 안된다 ? 이정도면  
서비스가 트래픽이 무진장 많을것이다. redis같은 캐시를 사용해서 해결을 해야지 DTO조회방식으로 
해결이 될까 ?라는 의문이있다.(대부분의 서비스는 fetch 조인만으로도 성능이 나온다) 

참고: Entity는 직접캐시하면안된다. Entity는 영속성컨텍스트에서 관리되기때문에 캐시에 잘못 올라갈경우 
안지워지니 꼬일수있다. Entity 를 Dto로 변환후 DTO는 캐시에 넣어야한다. 

 

 

 

 

728x90