Spring Boot 3.x 환경에서 Querydsl 설정 및 동적 쿼리 최적화 가이드
Spring Data JPA는 간단한 CRUD 작업을 처리할 때 매우 강력하지만, 복잡한 검색 조건이나 동적 쿼리를 작성해야 할 때는 한계에 부딪히기 쉽습니다. @Query 어노테이션을 사용하여 직접 JPQL을 작성할 수는 있지만, 문자열 기반의 쿼리는 오타 발생 시 런타임 에러를 유발하며 가독성이 떨어지는 단점이 있습니다.
이러한 문제를 해결해 주는 도구가 바로 Querydsl입니다. Querydsl은 자바 코드로 쿼리를 작성할 수 있게 해주어 컴파일 시점에 오류를 잡아낼 수 있고, 메서드 체이닝 방식을 통해 직관적인 동적 쿼리 작성을 지원합니다. 이번 포스팅에서는 최신 Spring Boot 3.x 환경에서의 설정 방법과 실무 최적화 팁을 알아보겠습니다.
1. Spring Boot 3.x Querydsl 설정
Spring Boot 3.x(Jakarta EE) 환경에서는 이전 버전(Java EE)과 라이브러리 의존성 및 설정 방식이 약간 변경되었습니다. build.gradle 설정을 정확히 구성하는 것이 첫 번째 단계입니다.
build.gradle 설정 예제
dependencies {
// 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 QClass 생성 위치 설정
def querydslDir = "$buildDir/generated/querydsl"
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}
clean {
delete file(querydslDir)
}
핵심은 :jakarta 분류자를 사용하여 Jakarta 패키지를 지원하도록 명시하는 것입니다. 설정 후 gradle build를 실행하면 generated 폴더 하위에 엔티티 기반의 QClass들이 생성됩니다.
2. JPAQueryFactory 빈(Bean) 등록
Querydsl을 편리하게 사용하기 위해 JPAQueryFactory를 스프링 빈으로 등록하여 주입받아 사용하는 방식을 권장합니다.
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
3. 실무 지향적 동적 쿼리 작성 기법
Querydsl의 가장 큰 강점은 BooleanExpression을 활용한 동적 쿼리 작성입니다. 이를 통해 쿼리 조건을 작은 단위의 메서드로 분리하여 재사용성과 가독성을 높일 수 있습니다.
BooleanExpression 활용 예제
public List<Member> searchMember(MemberSearchCondition condition) {
return queryFactory
.selectFrom(member)
.where(
usernameEq(condition.getUsername()),
ageGoe(condition.getAgeGoe())
)
.fetch();
}
private BooleanExpression usernameEq(String username) {
return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
where 절에 null이 전달되면 Querydsl이 이를 무시하므로, 자연스럽게 동적 쿼리가 완성됩니다. 또한 각 메서드는 다른 쿼리에서도 재사용될 수 있습니다.
4. Querydsl 성능 최적화 팁
1) Projection 활용 (DTO 조회)
전체 엔티티가 아닌 특정 필드만 필요한 경우, 엔티티 대신 DTO로 바로 조회하여 성능을 최적화할 수 있습니다. Projections.constructor나 Projections.fields를 사용합니다.
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
2) Fetch Join 사용
N+1 문제를 방지하기 위해 연관된 엔티티를 한 번의 쿼리로 가져오도록 fetchJoin()을 명시적으로 사용해야 합니다.
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("user1"))
.fetchOne();
3) Exist 쿼리 최적화
JPA의 exists는 내부적으로 count 쿼리를 실행하므로 대용량 데이터에서 성능이 저하될 수 있습니다. Querydsl에서는 fetchFirst()를 사용하여 직접 구현하는 것이 성능상 유리합니다.
public Boolean exists(Long id) {
Integer fetchOne = queryFactory
.selectOne()
.from(member)
.where(member.id.eq(id))
.fetchFirst(); // limit 1과 동일
return fetchOne != null;
}
결론
Querydsl은 Spring Data JPA와 함께 사용했을 때 시너지가 가장 큰 도구입니다. 타입 안정성 확보, 동적 쿼리의 유연성, 코드 재사용성 등 개발자가 얻을 수 있는 이점이 매우 많습니다.
Spring Boot 3.x 환경으로 전환하면서 설정 방식이 다소 까다로워졌지만, 위에서 설명한 jakarta 설정과 BooleanExpression 패턴을 적용한다면 유지보수가 용이하고 고성능의 데이터 접근 계층을 구축할 수 있을 것입니다. 복잡한 쿼리 로직으로 고민 중이라면 지금 바로 Querydsl 도입을 검토해 보시기 바랍니다.