
개요
현재 진행중인 프로젝트에서 검색 기능을 구현하면서 QueryDsl 구현 하게 되었습니다.
queryDSL의 spirng boot 프로젝트 적용 부터, 주요 클래스, 유스케이스, 검색 구현 방법등을 조사해보고자 합니다.
본론
1. QueryDSL이란?
QueryDSL 이란 HQL쿼리를 타입 안전한 방식으로 유지해야한다는 필요성에서 탄생한 쿼리 빌더 프레임워크 입니다.
도메인 모델이 변화함에 따라 타입 안전성은 소프트웨어 개발에 큰 이점을 제공합니다. 도메인 변경 사항은 쿼리에 직접 반영되며, 쿼리 생성 시 자동 완성 기능을 통해 쿼리 생성이 더욱 빠르고 안전해집니다.
초기에는 Hibernate 만을 위한 프레임워크였으나, 이후에는 다른 jpa 구현체, jdo, jdbc, 등을 지원하고 있습니다.
QueryDSL의 원칙
1. 타입 안정성
컴파일 시점에 오류를 잡을 수 있는 안전한 코드를 말합니다.
JPQL이나, SQL의 경우 쿼리 문법 오류가 런타임, 즉 서버 실행중에 발생하게 됩니다.
반면, QueryDsl은 자바 코드처럼 쿼리를 작성하게 되기 때문에, 타입에 관한 컴파일타임 문법 오류 확인이 가능해집니다.
예시
- 예: 일반 JPQL (타입 안전 X)
String name = "Alice";
List<User> users = entityManager
.createQuery("SELECT u FROM User u WHERE u.name = :name")
.setParameter("name", name)
.getResultList(); // -> 쿼리 오타는 런타임에야 알 수 있음
- 예: Querydsl (타입 안전 O)
QUser user = QUser.user; // QUser는 User 클래스 기반으로 생성된 Querydsl 타입
List<User> users = queryFactory
.selectFrom(user)
.where(user.name.eq("Alice")) // name이 String이 아닌 타입이면 컴파일 에러 발생
.fetch();
2. 일관성
QueryDsl은 jpa, sql, mongoDB 등 다양한 백엔드에서 동일한 방식으로 쿼리를 작성할 수 있도록 설계 되어있습니다.
예시
// JPA 기반 Querydsl
queryFactory.select(user.name).from(user).where(user.age.gt(18)).fetch();
// SQL 기반 Querydsl
SQLQuery<?> sqlQuery = new SQLQuery<Void>(connection, templates);
sqlQuery.select(user.name).from(user).where(user.age.gt(18)).fetch();
같은 .select().from().where() 패턴으로 유지된다는 게 일관성의 핵심입니다.
QueryDSL 의 핵심 인터페이스 3가지
Query | 쿼리 구성자 인터페이스 |
---|---|
Fetchable | 쿼리 실행자 인터페이스 |
Expression | 조건문, 필드, 연산 표현에 사용되는 타입 기반 표현 객체 |
- Query
- select(), from(), where() 같은 쿼리 구성 메서드를 제공하는 핵심 인터페이스입니다.
- JPAQuery, SQLQuery 등이 이걸 구현합니다.
- Fetchable
.fetch()
,.fetchOne()
,.fetchFirst()
같은 쿼리 실행 메서드를 제공합니다.List<User> result = queryFactory.selectFrom(user) .where(user.age.gt(18)) .fetch(); // 이게 Fetchable의 기능
- Expression
- 쿼리에서 사용되는 모든 조건, 필드, 연산은 Expression의 구현체입니다.
user.name.eq("Alice")
에서user.name
도,eq("Alice")
도 모두 Expression입니다.BooleanExpression condition = user.age.gt(18).and(user.name.startsWith("A"));
- 이런 연산들이 다 타입 안전하게 구성되고, 체이닝이 가능한 이유가 Expression 덕분이라고 합니다.
Criteria API
Hibernate의 HQL 은 문자열 기반 쿼리입니다. (ex.from User where name = :name
)
이를 해결하기 위해서 Hibernate에서 Criteria API 라는 타입 안전 동적 쿼리 언어를 제공하는데요.
이 유사한 목적으로 인해 QueryDSL 과 많이 비교되는 주제라고 합니다.
차이점은 아래와 같습니다.
항목 | Criteria API | QueryDSL |
---|---|---|
학습 곡선 | 상대적으로 높음 (코드 장황함) | 낮음 (직관적, 간결함) |
가독성 | 낮음 (복잡한 구조) | 높음 |
표현력 | 제한적 (join, 서브쿼리 번거로움) | 강력함 (복잡한 join/subquery도 쉽게) |
지원 상태 | JPA 표준, 하지만 실무에서는 잘 안 씀 | 비표준, 그러나 Spring 생태계에서 사실상 표준 |
빌드 설정 | Static Metamodel 생성 필요 | QClass 생성 필요 (APT 사용) |
라이브러리 의존성 | 기본 JPA만으로 가능 | 추가 라이브러리 필요 (querydsl-jpa, apt) |
Criteria API
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get(User_.name), "Tom"));
predicates.add(cb.ge(root.get(User_.age), 20));
cq.select(root).where(cb.and(predicates.toArray(new Predicate[0])));
QueryDSL
QUser user = QUser.user;
List<User> result = queryFactory
.selectFrom(user)
.where(
user.name.eq("Tom"),
user.age.goe(20)
)
.fetch();
2.0 프로젝트 도입 - 의존성
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'org.gdsc-cau.team5'
version = '1.0-SNAPSHOT'
sourceCompatibility = '21'
repositories {
mavenCentral()
}
ext {
queryDslVersion = "6.10.1"
}
dependencies {
// spring web
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'com.ibm.icu:icu4j:74.2'
// database
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
// query-dsl
implementation "io.github.openfeign.querydsl:querydsl-core:${queryDslVersion}"
implementation "io.github.openfeign.querydsl:querydsl-jpa:${queryDslVersion}"
annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:${queryDslVersion}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
먼저 dpendencies 에서 queryDslVersion
을 6.10.1
버전을 사용했습니다.
- springboot의 3.x 버전부터 Hibernate 6.x 버전을 사용하며, queryDsl 5.x 버전은 Hibernate 6 버전을 완전히 지원하지 않는다는 점
jakarta.persistence.*
의존성을 사용한다는 점 등 다양한 개선점이 있기 때문에 선택했습니다.
implementation "io.github.openfeign.querydsl:querydsl-core:${queryDslVersion}"
implementation "io.github.openfeign.querydsl:querydsl-jpa:${queryDslVersion}"
annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:${queryDslVersion}:jpa"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
항목 | QueryDSL 5.x | QueryDSL 6.x |
---|---|---|
Java 호환성 | Java 8 이상 | Java 11 이상 권장 (최신 기능 적극 활용) |
JPA 의존성 | javax.persistence.* | jakarta.persistence.* (Jakarta EE 9 이상 지원) |
모듈 구조 | 모노리틱, 비교적 덜 나뉘어 있음 | 모듈화 개선 (querydsl-core, querydsl-jpa, querydsl-sql 등) |
빌드/생성 도구 | Gradle/Maven용 APT 설정 필요 | Java 17+ 기준 Gradle Plugin or querydsl-apt 설정 개선됨 |
Spring Data 연동 | Spring Data JPA 연동시 일부 수동 설정 필요 | Spring Boot 3.x + Jakarta 호환성 보장 |
기타 개선 | - | Kotlin 지원 강화, 타입 시스템 정리, 내부 리팩토링 |
그 다음 dependency 아래 부분들은 jpa entity들에 대한 Q모델 생성 및 관리를 담당하는 코드들입니다.
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile;
// Gradle의 main 소스셋에 src/main/generated 디렉토리를 추가함
// 즉, 생성된 Q클래스들도 자바 소스처럼 인식됨 → IDE와 컴파일러가 읽을 수 있음
sourceSets {
main {
java {
srcDirs += querydslDir
}
}
}
// Java 컴파일 작업 중에 annotation processor가 생성하는 파일들을 querydslDir 위치에 생성하도록 설정
// 즉, QUser, QPost 같은 Q타입 클래스들이 여기 경로에 생기도록 유도
tasks.withType(JavaCompile) {
options.generatedSourceOutputDirectory = file(querydslDir)
}
// ./gradlew clean 실행 시, 이전에 생성된 Q파일들이 있는 src/main/generated 폴더도 같이 삭제
// 자동 생성 폴더는 항상 새로 만들어지기 때문에 삭제해도 됨
tasks.named("clean") {
doLast {
delete file(querydslDir)
}
}
test {
useJUnitPlatform()
}
intellij 사용시 build 이후 main코드 혹은 테스트 코드 실행시 Qmodel 재 생성 되는 이슈

Gradle
이 아닌intellij
로 build 하고 있다면,enable annotation processing
을 켜놓았을 때, 메인코드나 테스트코드를 실행할경우 인텔리제이 자체적으로 어노테이션 관련 처리를 한번 더 실행하는 것으로 보입니다.enable annotation processing
을 끄거나, build를intellij
가 아닌,gradle
이 담당하게 할 경우 작성된build.gradle
과 충돌하지 않습니다.
2.1 프로젝트 도입
이제, 도입된 querydsl이 제대로 동작하는지 한번 코드를 작성해보겠습니다.
정상적으로 진행되는지 테스트 코드를 통해 확인해보겠습니다.
저는 현재 아래와 같이 프로젝트 상에서 CQRS 분리를 통해 application 계층에서 jpaRepository에 의존하는 reader
계층을 분리해주었습니다.
@Service
@RequiredArgsConstructor
public class PostReaderImpl implements PostReader {
private final PostRepository postRepository;
@Override
public Post getPostOrThrow(final Long postId) {
return postRepository.findById(postId)
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_POST));
}
@Override
public Post getPostWithCommentOrThrow(final Long postId) {
return postRepository.findWithCommentsUsersById(postId)
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_POST));
}
@Override
public List<Post> getPosts() {
return postRepository.findAllByOrderByCreatedAtDesc();
}
}
여기서 전체 게시물을 가져오는 postReader.getPosts()
코드가 queryDsl을 사용하도록 로직을 변경해보겠습니다.
@Service
@RequiredArgsConstructor
public class PostReaderImpl implements PostReader {
private final PostRepository postRepository;
private final JPAQueryFactory queryFactory;
@Override
public Post getPostOrThrow(final Long postId) {
return postRepository.findById(postId)
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_POST));
}
@Override
public Post getPostWithCommentOrThrow(final Long postId) {
return postRepository.findWithCommentsUsersById(postId)
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND_POST));
}
@Override
public List<Post> getPosts() {
QPost post = QPost.post;
return queryFactory.selectFrom(post)
.fetch();
}
간단한 Query
와 fetch
를 통해서 쿼리를 실행하도록 변경했습니다.
또한 querydsl의 JpaQueryFactory
를 사용하여 실행하도록 했는데요.
과연 실행이 될까요?

안됩니다. No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' available
라는 오류가 발생하는데, JpaQueryFactory
에 대한 bean을 등록해주지 않아서 발생하는 오류입니다. 이를 위해 아래와 같이 config를 등록하여 JpaQueryFactory를 별도의 bean으로 등록해줍니다.
package org.sopt.global.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
❓ 왜 Spring이 자동으로 주입해주지 않나요?
- JPAQueryFactory는 Spring Data JPA의 일부가 아니고, QueryDSL 라이브러리의 구성 요소입니다.
- Spring은 기본적으로 이 Bean을 만들지 않으므로 직접 등록이 필요합니다.
설정이 다 되었다면, 아래와 같이 게시물을 생성하고 읽기 테스트를 하는 테스트 코드를 작성해줍니다.
저는 application-test.yml
파일을 별도로 작성해주어, 테스트 DB 환경을 분리해주었습니다.
package org.sopt.integration.post.application.reader;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.sopt.post.application.reader.PostReader;
import org.sopt.post.domain.Post;
import org.sopt.post.domain.PostRepository;
import org.sopt.user.domain.User;
import org.sopt.user.domain.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@ActiveProfiles("test")
@Transactional
@DisplayName("postReader 통합 테스트")
class PostReaderImplTest {
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private PostReader postReader;
@BeforeEach
void setUp() {
final User user = User.of("name", "email@.com");
userRepository.save(user);
postRepository.saveAll(List.of(
Post.of("test1", "content", user),
Post.of("test2", "content2", user)
));
}
@Test
@DisplayName("queryDsl을 통해 전체 게시글이 조회된다.")
void 전체_게시글을_조회한다() {
// when
List<Post> posts = postReader.getPosts();
// then
assertThat(posts).hasSize(2);
assertThat(posts).extracting("title")
.containsExactlyInAnyOrder("test1", "test2");
}
}
이후 테스트를 실행해주게 되면, 성공하는 것을 확인할수 있습니다.

레퍼런스
자바의 데이터베이스 접근 방법 - queryDSL 공식문서에서 레퍼런스됨
[QueryDSL] annotationProcessorGeneratedSourcesDirectory deprecated
'개발 > 자바,스프링' 카테고리의 다른 글
커넥션 풀이란? (1) | 2025.06.19 |
---|---|
Spring의 Dependency Injection(의존성 주입)과 방법론 (0) | 2025.04.24 |
JAVA 오류 : Modifier static is only allowed in constant variable declarations (0) | 2023.10.17 |
window에 자바 11 설치기 (1) | 2023.09.24 |