Spring Boot에서 Querydsl 활용하기: 동적 쿼리의 모든 것

SUUUI·2025년 3월 21일
0

Querydsl이란?

Querydsl은 안전한 방식으로 JPA 쿼리를 작성할 수 있게 해주는 프레임워크다. JPQL을 문자열로 작성하는 대신, 자바 코드로 쿼리를 작성할 수 있어 다음과 같은 장점이 있다:

컴파일 타임에 오류 검출: 문자열 기반 쿼리는 런타임에 오류가 발견되지만, Querydsl은 컴파일 타임에 오류를 잡아준다.

리팩토링 용이성: 엔티티 필드명이 변경되면 관련 쿼리도 자동으로 반영된다.

동적 쿼리 작성 용이: 조건에 따라 쿼리를 동적으로 구성하기 쉽다.(동적 쿼리(Dynamic Query)란 실행 시점에 조건에 따라 쿼리의 구조가 변하는 것을 의미)

내가 Querydsl을 처음 접했을 때 느낀 것은 마치 SQL 쿼리문을 Java로 옮겨놓은 듯한 직관적인 문법이었다. 코드를 보는 것만으로도 어떤 쿼리가 실행될지 쉽게 예측할 수 있어서 좋았다.

▶️프로젝트 구현 사례: 헬스장 검색 기능
요구사항
헬스장 정보 플랫폼에서 다음과 같은 검색 기능이 필요했다:

지역명으로 검색
헬스장 이름, 주소, 설명에 포함된 키워드로 검색
일일 이용권 가격 범위로 검색
월 이용권 가격 범위로 검색
인기 헬스장 필터링
페이지네이션 지원

구현 방법

먼저 레포지토리 인터페이스를 정의했다:

public interface GymRepositoryCustom {
    List<Gym> findByGym(GymSearchRequestDTO gymSearchRequestDTO, Pageable pageable);
}

그리고 이 인터페이스를 구현하는 클래스를 만들었다:

@RequiredArgsConstructor
@Slf4j
public class GymRepositoryCustomImpl implements GymRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<Gym> findByGym(GymSearchRequestDTO gymSearchRequestDTO, Pageable pageable) {
        return jpaQueryFactory
                .selectFrom(gym)
                .where(
                        eqLocal(gymSearchRequestDTO.getLocalName()),
                        containsSearchKeyword(gymSearchRequestDTO.getSearchKeyword()),
                        betweenDailyPrice(gymSearchRequestDTO.getMinDailyPrice(),
                                gymSearchRequestDTO.getMaxDailyPrice()),
                        betweenMonthlyPrice(gymSearchRequestDTO.getMinMonthlyPrice(),
                                gymSearchRequestDTO.getMaxMonthlyPrice()),
                        filterByPopular(gymSearchRequestDTO.getPopular())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(gym.id.desc())
                .fetch();
    }

}

여기서 핵심은 where 절 안에 여러 조건 메서드를 나열하는 방식이다. 각 메서드는 BooleanExpression을 반환하며, null이 반환되면 해당 조건은 무시된다. 이렇게 하면 조건이 있을 때만 쿼리에 적용되는 동적 쿼리를 구현할 수 있다.

조건 메서드 구현

각 검색 조건에 대한 메서드를 어떻게 구현했는지 살펴보자:

  1. 지역별 검색
private BooleanExpression eqLocal(String localName) {
    if (localName == null) {
        return null;
    }
    return gym.local.localName.contains(localName).or(gym.address.contains(localName));
}

지역명이 입력되면 헬스장의 지역명에 포함되거나 주소에 포함되는 경우를 찾는다. 지역명이 없으면 조건을 무시한다.

  1. 키워드 검색
private BooleanExpression containsSearchKeyword(String searchKeyword) {
    if (searchKeyword == null || searchKeyword.trim().isEmpty()) {
        return null;
    }
    return gym.gymName.contains(searchKeyword)
            .or(gym.address.contains(searchKeyword))
            .or(gym.description.contains(searchKeyword));
}

검색 키워드가 입력되면 헬스장 이름, 주소, 설명 중 어디에든 포함되는 경우를 찾는다. 키워드가 없거나 공백만 있으면 조건을 무시한다.

  1. 월 이용권 가격 범위 검색
private BooleanExpression betweenMonthlyPrice(Long minPrice, Long maxPrice) {
    if (minPrice == null && maxPrice == null) {
        return null;
    }
    if (minPrice == null) {
        return gym.monthlyPrice.loe(maxPrice);
    }
    if (maxPrice == null) {
        return gym.monthlyPrice.goe(minPrice);
    }
    return gym.monthlyPrice.between(minPrice, maxPrice);
}

최소가격과 최대가격 조건에 따라 다른 쿼리를 생성한다. 둘 다 있으면 between, 하나만 있으면 이상/이하 조건을 사용한다.

  1. 일일 이용권 가격 범위 검색
private BooleanExpression betweenDailyPrice(Long minPrice, Long maxPrice) {
    if (minPrice == null && maxPrice == null) {
        return null;
    }
    if (minPrice == null) {
        return gym.dailyPrice.loe(maxPrice);
    }
    if (maxPrice == null) {
        return gym.dailyPrice.goe(minPrice);
    }
    return gym.dailyPrice.between(minPrice, maxPrice);
}

월 이용권과 동일한 로직으로 일일 이용권 가격 범위 검색을 구현했다.

  1. 인기 헬스장 필터링
private BooleanExpression filterByPopular(Popular isPopular) {
    return isPopular != null ? gym.popular.eq(isPopular) : null;
}

인기 헬스장 필터가 선택되면 해당 조건을 추가한다.

profile
간단한 개발 기록

0개의 댓글