JPA Specification, Fetch join 문제 해결 과정 - LikeLionTeam/BootHouse GitHub Wiki

JPA Specification과 Fetch Join 적용 시 발생한 필터링 및 페이징 문제 해결

✔상황

특정 필터링 조건을 기반으로 코스 데이터를 페이징 처리하여 조회하는 API를 개발 중, JPA Specification을 활용하여 동적 쿼리를 작성했습니다. 이 과정에서 여러 연관 엔티티를 fetch join으로 불러오고, 페이징 처리를 함께 적용하려고 했습니다.

✔문제 발생

1. 동적 필터링 조건이 무시됨

처음 작성한 코드에서 @Query를 사용해 fetch join을 적용하고 count 쿼리까지 처리하려 했으나, Specification에서 정의한 필터링 조건이 무시되었습니다.

@Query(value = "SELECT DISTINCT c FROM CourseEntity c " +
        "JOIN FETCH c.bootcampEntity " +
        "JOIN FETCH c.categoryEntity " +
        "JOIN FETCH c.subCourseEntity",
        countQuery = "SELECT COUNT(c) FROM CourseEntity c")
Page<CourseEntity> findAll(Specification<CourseEntity> specification, Pageable pageable);

결과적으로 @QuerySpecification이 충돌하여 동적 필터링이 전혀 동작하지 않았습니다.

2. 페이징 처리 시 Fetch Join과 Count 쿼리의 충돌

Fetch join과 페이징 처리를 동시에 사용하면, count 쿼리에서 여러 테이블 간의 불필요한 join이 발생하여 성능 문제가 생기거나, 에러가 발생할 수 있습니다.

그 이유는 count 쿼리에서는 단순히 개수를 반환하는 것이 목적이므로, fetch join이 필요하지 않기 때문입니다.

✔해결 과정

1. @QuerySpecification의 충돌 해결

@Query를 제거하고, Specification 내에서 fetch join과 필터링 조건을 직접 작성했습니다. 이를 통해, @Query에 의한 동적 필터링 무시 문제를 해결할 수 있었습니다.

2. Fetch Join과 Count 쿼리의 충돌 해결

count 쿼리에서 fetch join을 사용하지 않도록 설정했습니다. 일반 조회 쿼리에서는 fetch join을 적용해 연관 엔티티를 불러오고, count 쿼리에서는 이를 제외하여 성능 문제와 충돌을 방지했습니다.

주요 해결 코드:

1. Specification에서 fetch join 적용

public class CourseSpecification {
    public static Specification<CourseEntity> filterCourses(
            Long categoryId, CourseFilter courseFilter, String sort, String search) {
        return (root, query, criteriaBuilder) -> {

            if (CourseEntity.class.equals(query.getResultType())) {
                // 일반 조회 쿼리일 때만 fetch join 적용
                root.fetch("bootcampEntity", JoinType.LEFT);
                root.fetch("categoryEntity", JoinType.LEFT);
                root.fetch("subCourseEntity", JoinType.LEFT);
                query.distinct(true);
            }

            // 기타 필터링 조건 추가...
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}

2. 페이징 처리를 위한 findAll 메서드 정의

// @Query 제거
public interface CourseJpaRepository extends JpaRepository<CourseEntity, Long>,
        JpaSpecificationExecutor<CourseEntity> {
    
    Page<CourseEntity> findAll(Specification<CourseEntity> specification, Pageable pageable);
}

✔정리

  • @QuerySpecification은 함께 사용할 수 없다.

    • 동적 필터링이 필요하다면 @Query 대신 Specification에서 조건을 정의해야 한다.
  • 페이징 처리 시 fetch joincount 쿼리에서 사용하지 않는다.

    • 일반 조회 쿼리에서는 fetch join을 사용하고, countQuery에서는 성능 문제와 에러를 방지하기 위해 제외해야 한다.

✔핵심 트러블슈팅 요약

@Query를 사용하면 Specification의 동적 조건이 무시되므로, 동적 필터링을 위해서는 @Query 대신 Specification에서 모든 조건을 처리해야 한다. 페이징 처리 시 fetch join과 count 쿼리 간의 충돌을 방지하기 위해, count 쿼리에서는 fetch join을 적용하지 말아야 한다.