QueryDSL — 타입세이프 동적 쿼리
JPQL이든 SQL이든 문자열로 쿼리를 짜면 두 가지가 괴롭다 — (1) 컴파일 타임에 오타·타입 오류를 못 잡고("selet"도 런타임에야 터짐), (2) 조건이 동적으로 바뀌는 검색(이름 있으면 이름으로, 가격대 있으면 가격으로…)을 StringBuilder로 잇다 보면 띄어쓰기·AND 지옥이 된다. QueryDSL은 *엔티티에서 생성한 메타모델(Q 클래스)*로 쿼리를 자바 코드처럼 타입 안전하게 조립해 이 둘을 푼다. (출처: OpenFeign/querydsl / Spring Data JPA — QueryDSL Extension.)
⚠️ 2026년 현재 — 어떤 QueryDSL을 쓰나
가장 먼저 알아야 할 실무 사실 — 원조 com.querydsl(querydsl/querydsl)은 사실상 유지보수가 멈췄다(마지막 릴리스가 수년 전, Spring Boot 3·Jakarta 대응이 끊김). 그래서 커뮤니티가 이어받은 **OpenFeign 포크 io.github.openfeign.querydsl**가 Spring Boot 3.x/4.x·Jakarta(jakarta.persistence) 호환으로 활발히 릴리스되는 사실상의 표준이다. 신규 프로젝트는 groupId를 io.github.openfeign.querydsl로 쓴다.
// build.gradle — OpenFeign 포크 (Jakarta / Spring Boot 3+)
implementation 'io.github.openfeign.querydsl:querydsl-jpa:<버전>:jakarta'
annotationProcessor 'io.github.openfeign.querydsl:querydsl-apt:<버전>:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
옛
com.querydsl+javax.persistence조합은 Spring Boot 3에서 깨진다(javax→jakarta 이행). 마이그레이션의 핵심이 이 groupId·classifier 교체다.
Q 클래스 — 엔티티의 타입세이프 거울
QueryDSL은 애너테이션 프로세서가 빌드 시 @Entity Member → QMember 같은 메타모델 클래스를 생성한다. 이 Q 클래스의 필드(member.email, member.age)로 조건을 타입 안전하게 쓴다 — 오타·타입 불일치는 컴파일 에러가 된다.
JPAQueryFactory query = new JPAQueryFactory(entityManager);
QMember m = QMember.member;
List<Member> result = query
.selectFrom(m)
.where(m.age.goe(20) // age >= 20 (타입 안전)
.and(m.email.endsWith("@corp.com")))
.orderBy(m.createdAt.desc())
.limit(20)
.fetch();
동적 쿼리 — QueryDSL이 가장 빛나는 곳
문자열 쿼리의 최대 약점인 조건 조립을 QueryDSL은 우아하게 푼다. BooleanBuilder로 조건을 모으거나, null을 반환하는 조건 메서드를 where()에 나열하면 null인 조건은 자동으로 무시된다.
// null 반환 패턴 — 입력이 없으면 그 조건은 그냥 빠진다
private BooleanExpression emailEq(String email) {
return email != null ? member.email.eq(email) : null;
}
private BooleanExpression ageGoe(Integer age) {
return age != null ? member.age.goe(age) : null;
}
List<Member> search(String email, Integer age) {
return query.selectFrom(member)
.where(emailEq(email), ageGoe(age)) // null 조건은 AND에서 제외
.fetch();
}
StringBuilder로 WHERE 1=1 AND ...를 잇던 코드가 조건 메서드 나열로 깔끔해진다 — 동적 검색 화면의 정석 패턴이다.
조인·프로젝션·서브쿼리
// 페치 조인 + DTO 프로젝션 + 서브쿼리
List<MemberDto> dtos = query
.select(Projections.constructor(MemberDto.class,
member.email, member.age)) // 필요한 칼럼만 DTO로
.from(member)
.join(member.team, team).fetchJoin() // N+1 방지 페치 조인
.where(member.age.gt(
JPAExpressions.select(member.age.avg()).from(member))) // 서브쿼리
.fetch();
- 프로젝션 —
Projections.constructor/fields/bean으로 필요한 칼럼만 DTO로(=SELECT *회피, [07] 정신과 동일). - 페치 조인 — 연관을 한 방에 가져와 N+1을 막는다.
- 집계·그룹화 —
groupBy()·having()·Expressions로 윈도우/함수 일부도 표현.
QueryDSL vs JPQL vs 네이티브 SQL — 언제 무엇
| 타입 안전 | 동적 쿼리 | DB 특화 기능 | |
|---|---|---|---|
| QueryDSL | ✅ 컴파일 검증 | ✅ 최고 | △ 엔티티/표준 범위 |
| 문자열 JPQL | ❌ 런타임 | △ 번거로움 | △ JPA 추상 |
| 네이티브 SQL | ❌ 런타임 | △ | ✅ JSONB·CTE·윈도우 등 전부 |
원칙 — 동적 조건·복잡한 검색·타입 안정성이 중요하면 QueryDSL, DB 고유 기능(PostgreSQL jsonb @>·재귀 CTE·BRIN 힌트 등)이 절실하면 네이티브 SQL을 그 쿼리만 쓴다. 한 프로젝트에서 섞어 쓰는 게 보통이다.
한계와 비용
- Q 클래스 생성 빌드 설정(annotationProcessor)이 필요하고, 엔티티가 바뀌면 재생성해야 한다.
- DB 특화 문법은 표현에 한계가 있어, 그런 쿼리는 네이티브로 빠진다.
- 결국 JPA/하이버네이트 위에서 도는 도구라 — 영속성 컨텍스트·페치 전략·N+1 이해가 선행돼야 효과를 본다.
한 줄 요약 — QueryDSL은 Q 클래스 메타모델로 타입세이프·동적 쿼리를 짠다. 2026년엔 OpenFeign 포크(
io.github.openfeign.querydsl)·Jakarta가 사실상 표준(원조는 중단). 동적 검색엔 QueryDSL(null조건 무시 패턴), DB 특화엔 네이티브 SQL을 섞어 쓴다.
(출처: OpenFeign/querydsl(GitHub) / Spring Data JPA — QueryDSL Extension / Spring Boot #48573 — io.github.openfeign.querydsl 이행.)