✅ QueryDSL이란 무엇인가?
DSL는 Domain Specific Language의 약자이다. 말 그대로 특정 영역의 언어이다.
앞에 Query라는 단어가 붙었으니 쿼리 영역에 집중하는 언어라고 생각할 수 있다.
QueryDSL은 Java 기반의 타입 안전한 SQL/JPQL 쿼리 생성 라이브러리다.
보통 JPA를 쓸 때 JPQL을 문자열로 작성한다. 예를 들어,
String jpql = "select a from Article a where a.title like :title";
하지만 이렇게 문자열로 쿼리를 쓰면
- 오타가 있어도 컴파일러가 못 잡아준다.
- 복잡한 동적 쿼리를 만들기 어렵다.
- 리팩토링이 힘들다.
여기서 QueryDSL은 이 문제를 해결해 준다.
- 컴파일 시점에 쿼리 오류를 잡을 수 있다.
- 동적 쿼리를 간결하게 작성할 수 있다.
- IDE 자동완성이 된다.
예를 들어 QueryDSL로 같은 쿼리를 작성하면 아래처럼 된다:
QArticle article = QArticle.article;
List<Article> articles = queryFactory
.selectFrom(article)
.where(article.title.contains("QueryDSL"))
.fetch();
위 코드를 보면 모든 속성과 테이블이 타입으로 관리되며
오타나 잘못된 조인 등을 컴파일 단계에서 체크할 수 있다.
복잡한 조건, 동적 정렬, 서브쿼리까지
마치 자바 코드를 짜듯이 쿼리를 만들 수 있다.
정리하자면 QueryDSL은
→ JPQL도 SQL도 더 안전하고 편리하게 쓸 수 있게 해주는 도구이며 JPA와 상호보완적인 관계이다.
✅ QueryDSL의 장점
- 타입 안전성 (Type-Safe)
- JPQL을 문자열로 작성하면 오타나 잘못된 필드명을 컴파일러가 잡아주지 못한다.
- QueryDSL은 Q타입 클래스를 이용해 쿼리를 작성하므로
컴파일 단계에서 오류를 검출할 수 있다. - ex) article.title.eq("Hello")
→ title이 존재하지 않으면 컴파일 에러
- IDE 자동완성 지원
- Q타입 클래스 덕분에 자동완성 기능이 매우 강력하다.
- 덕분에 쿼리 작성이 훨씬 빠르고 실수가 줄어든다.
- 동적 쿼리 작성이 쉽다
- BooleanBuilder 나 where 절에 여러 조건을 체이닝하며
가변적으로 쿼리를 조립할 수 있다. - ex)
BooleanBuilder builder = new BooleanBuilder();
if (title != null) {
builder.and(article.title.contains(title));
}
- BooleanBuilder 나 where 절에 여러 조건을 체이닝하며
- 복잡한 쿼리 작성에 유리
- 서브쿼리, 조인, 그룹핑 등 복잡한 SQL도
마치 자바 코드처럼 깔끔하게 작성할 수 있다.
- 서브쿼리, 조인, 그룹핑 등 복잡한 SQL도
- JPQL, SQL 모두 지원
- JPA 기반 JPQL뿐 아니라 Native SQL도 작성 가능하다.
- 복잡한 Native Query가 필요할 때도 QueryDSL로 깔끔하게 처리 가능.
❌ QueryDSL의 단점
- 빌드 과정이 복잡해질 수 있다
- Q타입 파일 생성 과정(annotation processor)을
빌드 툴(Gradle, Maven)에 설정해야 한다. - 처음 세팅할 때 약간 복잡하다.
- Q타입 파일 생성 과정(annotation processor)을
- 학습 비용
- JPQL만 쓰던 개발자라면 처음에 문법을 익히는 데 시간이 필요하다.
- 의존성 추가가 필요
- QueryDSL 라이브러리와 APT(annotation processor)를 추가해야 한다.
✅ Q타입 클래스란?
Q타입클래스가 없으면 QueryDSL은 사용이 불가능하다.
Q타입클래스는 QueryDSL이 컴파일 할때 자동으로 생성해주는 쿼리 전용 클래스이다.
JPA 엔티티를 자바 코드에서 타입 안전하게 다룰 수 있게 해주는 일종의 쿼리 전용 DTO 같은 존재다.
예) Article 엔티티:
@Entity
public class Article {
@Id
private Long id;
private String title;
}
빌드할 때 만들어지는 QArticle (Q타입 클래스):
public class QArticle extends EntityPathBase<Article> {
public static final QArticle article = new QArticle("article");
public final StringPath title = createString("title");
public final NumberPath<Long> id = createNumber("id", Long.class);
...
}
이렇게 Q타입클래스를 만들어 놓으면,
QArticle article = QArticle.article;
List<Article> list = queryFactory
.selectFrom(article)
.where(article.title.eq("Hello"))
.fetch();
이렇게 코드를 작성했을 때
- article.title → title 컬럼
- article.id → id 컬럼
으로 매칭 시켜주어서 모든 필드를 타입 세이프하게 접근 가능하다.
또한 Q타입클래스는 따로 설정해서 생성 빌드 경로도 정해주어야 한다.
🚀 QueryDSL 활용
✅ 동적쿼리
예) 조건이 있을 때만 where 절을 추가하고 싶을 때 ↓
BooleanBuilder builder = new BooleanBuilder();
if (title != null) {
builder.and(article.title.contains(title));
}
if (authorName != null) {
builder.and(article.author.name.contains(authorName));
}
List<Article> list = queryFactory
.selectFrom(article)
.where(builder)
.fetch();
- title과 authorName이 null이 아니라면 where 조건을 추가하는 코드이다.
- 만약 조건이 입력된다면,
String title = "hello";
builder.and(article.title.contains(title));
이 Java 코드는,
WHERE title LIKE '%hello%'
이 SQL문으로 변역이 되는 식이다.
✅ Fetch Join
예) Article → Member(author) join을 fetch join 하려면 ↓
QArticle article = QArticle.article;
QMember member = QMember.member;
List<Article> articles = queryFactory
.selectFrom(article)
.leftJoin(article.author, member).fetchJoin()
.fetch();
- fetchJoin() 메서드만 붙여주면 된다.
- 연관 엔티티를 한 번에 조회해서 N+1 문제를 방지한다.
✅ 서브쿼리
예) 조회수(hit)가 전체 평균 이상인 게시글 찾기 ↓
QArticle article = QArticle.article;
QArticle subArticle = new QArticle("subArticle");
List<Article> list = queryFactory
.selectFrom(article)
.where(
article.hit.goe( // goe = greater or equal
JPAExpressions
.select(subArticle.hit.avg())
.from(subArticle)
)
)
.fetch();
//SQL로 바꾸면 아래처럼
SELECT *
FROM article
WHERE hit >= (SELECT AVG(hit) FROM article)
→ JPAExpressions 를 사용해 서브쿼리를 작성 (서브쿼리 생성 도우미 클래스)
- select, where, having 절에 서브쿼리 가능
- 타입 안전하게 작성 가능
✅ QueryDSL이 서브쿼리를 쉽게 만들어주는 게 큰 장점이다.
✅ QueryDSL 설정
💡 build.gradle.kts 설정
plugins {
java
id("org.springframework.boot") version "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "study"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
// QueryDSL
val querydslVersion = "5.0.0"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.postgresql:postgresql")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("com.h2database:h2")
implementation("org.springframework.boot:spring-boot-starter-validation")
// QueryDSL
implementation("com.querydsl:querydsl-jpa:${querydslVersion}:jakarta")
annotationProcessor("com.querydsl:querydsl-apt:${querydslVersion}:jakarta")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<JavaExec> {
systemProperty("file.encoding", "UTF-8")
}
// QueryDSL
val generatedDir = layout.buildDirectory.dir("generated/sources/annotationProcessor/java/main")
// QueryDSL
sourceSets["main"].java {
srcDir(generatedDir)
}
// QueryDSL
tasks.named<Delete>("clean") {
delete(generatedDir)
}
이렇게 설정하고 Gradle bulid를 하고
💡 JPAQueryFactory 빈 등록
@Configuration
@RequiredArgsConstructor
public class QuerydslConfig {
private final EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
이대로 실행하면

위 두 개의 Entity가 있다면,
bulid/generated/sources/annotationProcessor/java/main
아까 build.gradle.kts에서 설정해둔 위 경로로

QArticle, QMember 두 클래스가 생성된다.
생성이 완료되었다면,
@Repository
@RequiredArgsConstructor
public class ArticleRepositoryImpl implements ArticleRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<Article> findByTitle(String title, Pageable pageable) {
QArticle article = QArticle.article;
BooleanExpression titleCondition =
(title != null && !title.isBlank()) ? article.title.contains(title) : null;
List<Article> content = queryFactory
.selectFrom(article)
.where(titleCondition)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = queryFactory
.select(article.count())
.from(article)
.where(titleCondition)
.fetchOne();
return new PageImpl<>(content, pageable, total != null ? total : 0L);
}
}
이런 식으로 DSL을 활용할 수 있다.
'JPA' 카테고리의 다른 글
| [JPA] 다중성이란? (0) | 2025.06.26 |
|---|---|
| [JPA] Fetch Join이란? (0) | 2025.06.25 |
| [JPA] N+1 문제 원인 & 해결 (0) | 2025.06.24 |