
QueryDSL은 JPA에서 문자열 기반 JPQL을 길게 작성하는 대신,
Q타입을 이용해 자바 코드로 쿼리를 구성할 수 있도록 도와주는 라이브러리다.
즉, 쿼리를 문자열로 직접 조립하는 대신 코드로 안전하게 조건식을 만들고 조합할 수 있다.
기존 JPQL은 레퍼지토리에 이런식으로 문자열 쿼리를 작성한다.
@Query("""
select ap
from ArtistProfile ap
where (:applyKeyword = false or ...)
and (:applyInstIds = false or ...)
""")
반면 QueryDSL은 조건을 코드로 조립한다.
.where(
keywordCondition(req.keyword(), user, artistProfile),
instCategoryCondition(req.instCategory(), artistInstrument, artistProfile),
instIdsCondition(req.instIds(), artistInstrument, artistProfile),
styleTagIdsCondition(req.styleTagIds(), artistStyleMap, artistProfile)
)
이렇게 하면 어떤 조건이 붙는지가 메서드 이름만으로도 드러난다.
검색 자체가 동적 검색이라는 점이 핵심이엇다.
모든 사용자가 같은 조건으로 검색하는 것이 아니라, 어떤 사용자는 키워드만 넣고, 어떤 사용자는 조건1, 조건2까지 함께 선택할 수 있었다.
즉, 조건이 고정된 조회가 아니라 있을 수도 있고 없을 수도 있는 조건이 많았다.
이런 경우 JPQL은 결국 문자열 안에 여러 분기 조건과 boolean 플래그가 들어가게 된다.
반면 QueryDSL은 조건을 메서드로 나눠서, 있으면 적용하고 없으면 제외하는 방식으로 구성할 수 있다.
그래서 검색 조건이 계속 늘어날 가능성이 있는 구조에서 유지보수가 가능한 형태를 만드는것에 더 가까운 선택을 한것이다.
QueryDSL은 먼저 몇 가지 준비가 필요하다.
Gradle 기준으로 QueryDSL 관련 의존성을 추가했다.
implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
QueryDSL은 엔티티를 기반으로 QUser , QArtistProfile 같은 Q타입을 생성해서 사용한다.
즉, 단순 라이브러리 추가만으로 끝나는 게 아니라 빌드 시 annotation processor가 Q클래스를 생성할 수 있도록 설정해야 했다.
이후 빌드를 수행하면 build/generated/... 아래에 Q타입이 생성되고,
코드에서는 이런 식으로 사용할 수 있다.
QArtistProfile artistProfile = QArtistProfile.artistProfile;
QUser user = QUser.user;
QueryDSL 쿼리를 작성하려면 JPAQueryFactory 가 필요해서 설정 클래스를 통해 Bean으로 등록했다.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
기본 JpaRepository 만으로는 QueryDSL 커스텀 구현을 담기 어려워서,
검색 메서드는 별도의 Custom Repository 인터페이스와 구현 글래스로 분리했다.
예를 들어 구조는 다음처럼 잡았다.
ArtistProfileRepositoryArtistProfileRepositoryCustomArtistProfileRepositoryImpl이번 구현에서 체감한 장점은 다음과 같았다.
1. 동적 조건 분기가 자연스럽다
조건이 없으면 null 반환, 있으면 BooleanExpression 반환방식으로 작성할 수 있다.
*BooleanExpression이란, QueryDSL 참/거짓 조건식이다.
user.username.containsIgnoreCase(keyword) 해당 식을 SQL으로 보면 이렇다.
lower(username) like '%keyword%'
즉, where 절에 들어갈 수 있는 조건식 잭체이다.
2. 가독성이 좋다
검색 조건별 메서드를 분리할 수 있어서, 어떤 조건이 적용되는지 구조적으로 읽힌다.
3. 확장성이 좋다
나중에 다른 필터 조건을 추가할 때도 where(...)에 조건 메서드 하나 더 넣으면 된다.
이번 구현에서는 최종 응답 DTO 하나만 사용하지 않고,
조회 과정에서 사용하는 중간 DTO를 따로 두었다.
예를 들어,
ArtistSearchRow : 검색 결과의 기본 정보ArtistInstrumentRow : 카드에 붙일 악기 목록 정보ArtistSeachResponse : 최종 응답 DTO 해당 방식으로 나눈 이유는,
조회 시점의 데이터 구조와 최종 응답 구조가 완전히 같지 않았기 대문이다.
검색 1차 결과는 기본 정보 중심이었고,
악기 정보는 별도로 조회해 artistProfileId 기준으로 묶어야 했다.
(검색 조건에 매칭된 정보만이 아니라, 아티스트가 보유한 악기 목록을 카드에 함께 보여줘야 했기 때문)
즉, 레퍼지토리에서는 DB 조회에 적합한 row 단위 DTO를 사용하고,
서비스에서는 그것들을 조립해서 최종 응답 DTO로 변환하는 식으로 역할을 분리했다.
물론 QueryDSL이 만능은 아니다.
1. 처음에는 구조가 더 복잡해 보인다
JPQL은 Repository 메서드 하나에 바로 쓸 수 있지만,
QueryDSL은 custom repository, impl 클래스, Q타입 생성 등 준비할 것이 더 많다.
즉, 단순 조회에서는 오히려 과한 선택이 될 수 있다.
2. 작은 조회까지 전부 QueryDSL로 갈 필요는 없다
조건이 거의 없고 고정된 조회라면 JPQL이나 Spring Data JPA 메서드 쿼리로도 충분하다.
이번 검색 구현은 QueryDSL을 처음 직접 적용한 사례였다.
단순히 새로운 기술을 써봤다 보다, 검색 조건이 계속 늘어날 수 있는 구조에서 왜 이 도구가 필요한지를 실제로 체감한 작업이었다.
특히 JPQL로도 구현 가능한 상황이었지만,
이번에는 현재 기능뿐 아니라 추후 확장 가능성까지 고려해 선택한 설계라는 점에서 의미가 있었다.