페치 조인과 페이지네이션
페치 조인을 단일 값 연관 필드(일대일, 다대일) 사용하면 페이징 API를 사용할 수 있지만 컬렉션(일대다)에 사용할 시 정상적으로 작동한다고 해도 applying in memory 즉, 모든 데이터를 메모리로 가져와 페이징 처리를 하기에 경고를 남긴다. 데이터가 많을 경우 성능 이슈와 메모리 초과 예외가 발생할 수 있다. 그렇다면 어떻게 해야할까? 한 가지 방법은 페치 조인을 적용하지 않고 엔티티의 아이디 목록을 가져온다. 아이디 목록을 In 절에 페치 조인을 적용한다. 즉, 같은 요청에 대해 같은 쿼리를 두 번 적용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public Page<PostEntity> findAll(Pageable pageable){ JPAQuery<Long> countQuery; QPostEntity postEntity; List<PostEntity> postEntities; List<Long> postEntityIds; postEntity = QPostEntity.postEntity; postEntityIds = jpaQueryFactory .select(postEntity.id) .from(postEntity) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .distinct() .fetch(); postEntities = jpaQueryFactory .selectFrom(postEntity) .leftJoin(postEntity.comments) .fetchJoin() .where(postEntity.id.in(postEntityIds)) .distinct() .fetch(); countQuery = jpaQueryFactory .select(postEntity.countDistinct()) .from(postEntity); return PageableExecutionUtils.getPage(postEntities, pageable, countQuery::fetchOne); } | cs |
2개 이상의 컬렉션 페치 조인
페이지네이션과 더불어 페치 조인은 2개 이상의 컬렉션에 사용할 수 없다. 사용할 경우 MultipleBagFetchException 예외가 발생한다. 여기서 Bag는 Hibernate에서 사용하는 용어로 순서와 키가 없으며 중복을 허용한다. Java Collection은 Bag을 구현하지 않아 List를 사용한다. 따라서 1개의 Bag에 대한 페치 조인만 허락한다. 포스트는 카테고리, 댓글 그리고 파일을 가지는데 카테고리는 다대일 관계를 가지고 나머지는 일대다 관계이며 파일은 임베디드 타입이지만 조인문으로 쿼리를 수행한다. 카테고리, 댓글 그리고 파일을 동시에 페치 조인을 실행하면 위의 오류가 발생한다. 오류는 일대다 관계(컬렉션)인 댓글과 파일 때문에 발생한다. 어떻게 해야할까?
대략적으로 3가지의 해결책이 있다. 1번 해결책은 @OrderColumn 어노테이션을 추가하는 것으로 List는 순서가 없으므로 Hibernate는 List를 Bag로 처리하는데 이를 List로 처리하도록 해주어야 한다. @OrderColumn 어노테이션이 이 역할을 수행한다. 2번 해결책은 페치 조인을 각각의 컬렉션에 수행하는 것이다. 즉, 하나의 쿼리를 여러 쿼리로 나누는 방법으로 가독성도 향상되고 쿼리를 개선할 수 있는 여지도 있다. 3번 해결책은 List를 Set으로 변경하는 것인데 엔티티가 영속성 처리가 되지 않은 상태에서 Set에 넣으면 동일한 필드 데이터 건의 경우 사라질 수 있는 문제가 있으며 카테시안 곱이 발생할 수 있다는 문제가 있다.
2번 해결책이 이상적이라는 글을 보아서 2번 해결책을 구현하기로 결정했다. 일단 댓글과 페치 조인을 실행한 후 나온 결과에 파일과 페치 조인을 적용한다. 마지막으로 여기서 카테고리와 페치 조인을 적용한다. 코드는 위의 코드에서 수정되어 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | @Override public Page<PostEntity> findAll(Pageable pageable) { JPAQuery<Long> countQuery; QPostEntity postEntity; List<PostEntity> postEntities; List<Long> postEntityIds; postEntity = QPostEntity.postEntity; orders = getOrderSpecifier(pageable); postEntityIds = jpaQueryFactory .select(postEntity.id) .from(postEntity) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .distinct() .fetch(); postEntities = jpaQueryFactory .selectFrom(postEntity) .leftJoin(postEntity.comments) .fetchJoin() .where(postEntity.id.in(postEntityIds)) .distinct() .fetch(); postEntities = jpaQueryFactory .selectFrom(postEntity) .leftJoin(postEntity.files) .fetchJoin() .where(postEntity.id.in(postEntityIds)) .distinct() .fetch(); postEntities = jpaQueryFactory .selectFrom(postEntity) .leftJoin(postEntity.category) .fetchJoin() .where(postEntity.id.in(postEntityIds)) .distinct() .fetch(); countQuery = jpaQueryFactory .select(postEntity.countDistinct()) .from(postEntity); return PageableExecutionUtils.getPage(postEntities, pageable, countQuery::fetchOne); } | cs |
update: 2024.10.16
댓글 없음:
댓글 쓰기