컬렉션 fetch join 및 페이징 applying in memory 오류
이번에 프로젝트를 하는 도중에
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는 캐시에 넣어야한다.