JPQL과 QueryDSL 사이에서 동적 검색 쿼리 고르기

검색 조건의 수와 조인·프로젝션 복잡도에 따라 JPQL, Specification, QueryDSL을 고르는 흐름

검색 화면의 조건이 하나일 때는 Repository 메서드 이름으로 충분했다. 조건이 선택 사항으로 늘어나자 @Query 안에서 null 여부를 확인하는 JPQL을 사용했고, 주문 유형과 메뉴 ID, 활성 상태, 상세 항목 조인, 응답 DTO 변환까지 한 번에 다루는 조회에는 QueryDSL을 적용했다.

당시 글의 요지는 간단했다. 정적이고 짧은 쿼리는 @Query, 조건이 많고 동적으로 조합되는 쿼리는 QueryDSL로 나눈다. 지금 다시 정리하면 그 사이에 “작은 조건 조합은 Specification으로 충분한가”도 함께 비교하겠다.

고정된 조건은 메서드 이름이나 JPQL로

예를 들어 이름과 카테고리를 선택적으로 검색하는 쿼리는 다음처럼 쓸 수 있다.

@Query("""
    select p from Product p
    where (:name is null or p.name like concat('%', :name, '%'))
      and (:category is null or p.category = :category)
    """)
List<Product> search(String name, String category);

조건이 늘지 않고 의도가 명확하다면 이 방식은 쿼리를 한곳에서 읽기 쉽다. 더 단순한 동등 조건은 메서드 이름 기반 쿼리로도 표현할 수 있다. 다만 선택 조건마다 :value is null or ...를 붙이는 패턴을 모든 검색에 일반화하지는 않는다. 데이터베이스와 통계에 따라 실행 계획에 영향을 줄 수 있으므로, 데이터가 커지면 실제 SQL과 실행 계획을 확인해야 한다.

조건을 재사용하고 조합할 때 Specification

필터가 몇 개 있고 각 조건을 다른 화면에서도 재사용하고 싶다면 Spring Data JPA의 Specification이 중간 선택지가 된다. Criteria API로 조건을 조합하고 JpaSpecificationExecutor를 통해 실행할 수 있다. “활성 주문”, “특정 주문 유형”, “특정 메뉴 포함” 같은 단순한 술어를 기능별로 나눠 조합할 수 있다.

검색 조건과 무관한 DTO 프로젝션, 복잡한 조인, 그룹화까지 크게 늘어나면 Specification만으로 쿼리 전체를 읽기 좋게 유지하기 어려울 수 있다. 이때는 쿼리 작성 방식보다 검색 요구와 응답 모양을 먼저 쪼개는 편이 낫다.

조인과 DTO가 늘어날 때 QueryDSL

원문에서 다룬 주문 검색은 deletionStatus = ACTIVE 조건을 기본으로 두고, 주문 유형과 메뉴 ID를 선택적으로 더한 뒤 주문 상세를 조인하고 응답 DTO로 조회하는 형태였다. 이런 경우 QueryDSL은 조건을 Java 코드로 단계별 조립할 수 있어 동적 조건이 어디서 추가되는지 드러난다.

BooleanBuilder where = new BooleanBuilder();
where.and(order.deletionStatus.eq(ACTIVE));

if (orderType != null) {
    where.and(order.type.eq(orderType));
}
if (menuId != null) {
    where.and(orderDetail.menu.id.eq(menuId));
}

이 조각만으로 전체 조회가 완성되는 것은 아니다. 실제 구현에는 조인 경로, DTO 프로젝션, 정렬, Pageable, count 쿼리가 맞게 연결되어야 한다. 특히 일대다 상세 테이블을 조인해 페이지를 만들면 한 주문이 여러 행으로 늘어날 수 있다. 페이지 내용과 전체 개수를 함께 검증하고, 필요하면 중복 제거 또는 두 단계 조회를 선택해야 한다.

QueryDSL은 Q 타입을 빌드 시 생성하도록 annotation processing과 의존성을 설정해야 한다. 프로젝트가 이미 사용 중이라면 복잡한 조회에서 이 비용을 회수하기 쉽지만, 단순한 조회 하나 때문에 도입하면 빌드 설정만 늘 수도 있다. Spring Data 문서는 Querydsl 확장 설정과 Q 클래스 생성을 별도로 설명하므로, 사용 중인 Spring Data 버전에 맞는 지원 상태와 빌드 플러그인을 확인하는 편이 좋다.

도구보다 검색 요구를 기준으로

선택 기준은 “QueryDSL이 더 고급인가”가 아니다. 고정 쿼리의 가독성, 조건 재사용, 동적 조인과 프로젝션, 페이지 count 비용, 빌드 복잡도를 함께 본다. 그 기준으로 메서드 이름 → @Query → Specification → QueryDSL을 필요한 수준까지만 선택하면 불필요한 추상화를 줄일 수 있다.

그리고 어떤 도구를 썼는지보다 실제 데이터에서 어떤 SQL이 나갔는지가 성능을 결정한다. 정렬과 검색 조건에 맞는 인덱스, 페이지 크기, count 쿼리를 확인해야 한다. 생성 시각 조건에 인덱스를 추가했던 경험은 createdAt 인덱스 적용 전후를 확인한 기록에 적었다.

참고 문서: Spring Data JPA 쿼리 메서드와 @Query, Spring Data JPA Specifications, Spring Data JPA Querydsl 설정

당시 기록: Spring의 동적 쿼리: JPQL, QueryDSL