왜 존재하는가 — JPQL 이든 SQL 이든 쿼리를 문자열로 쓰면 두 가지가 깨진다.
- 오타·타입 오류가 런타임까지 미뤄진다.
"selet m from Member m"은 컴파일이 통과하고, 그 코드를 실제로 실행하는 순간에야 터진다. 컴파일러는 문자열 안을 들여다보지 않기 때문이다. - 조건이 선택적으로 붙는 검색을 표현할 수 없다. 이름·나이·도시가 각각 있을 수도 없을 수도 있으면, 문자열을 이어 붙이며
WHERE 1=1로 시작해AND와 띄어쓰기를 손으로 맞추게 된다. 조건이 n 개면 조합은 2ⁿ 이라 조건 4개에 16가지 경로가 생기고, 그 전부를 사람이 검증해야 한다.
어떻게 푸는가 — 메타모델 QueryDSL 은 빌드 시점에 애너테이션 프로세서(컴파일 과정에 끼어들어 소스를 생성하는 컴파일러 플러그인)를 돌려, @Entity 가 붙은 Member 로부터 QMember 라는 클래스를 생성한다. 이 생성물이 메타모델 — 엔티티의 구조를 타입이 있는 필드로 다시 표현한 거울이다. 쿼리를 그 필드로 쓰기 때문에
member.age.goe(20) // age 가 숫자가 아니면 컴파일 에러
member.email.eq(3) // 컴파일 에러
문자열이었다면 런타임까지 살아남던 오류가 컴파일 단계에서 걸린다. goe 는 greater-or-equal(>=), lt 는 less-than(<) 처럼 SQL 비교 연산자를 메서드 이름으로 옮긴 것이다.
동적 쿼리 — 실무에서 이걸 쓰는 진짜 이유 where() 에 넘긴 인자가 null 이면 QueryDSL 은 그 조건을 조립에서 빼 버린다. 그래서 "값이 있으면 조건, 없으면 null" 을 반환하는 메서드를 나열하면 분기문이 사라진다.
query.selectFrom(member)
.where(emailEq(email), ageGoe(age)) // 입력 없는 조건은 AND 에서 빠진다
.fetch();
여기서 주의할 함정 — 조건이 전부 null 이면 WHERE 절 자체가 없어져 테이블 전체 조회가 된다. 최소 조건을 강제하거나 조회 상한을 두지 않으면 인덱스를 못 타는 풀 스캔이 그대로 나간다.
2026년 현재의 실무 사실 원조 com.querydsl 은 사실상 유지보수가 멈춰 Jakarta 이행(javax.persistence → jakarta.persistence) 대응이 없다. Spring Boot 3 로 올릴 때 QueryDSL 이 깨지는 원인이 이것이고, 지금은 커뮤니티가 이어받은 OpenFeign 포크 io.github.openfeign.querydsl 를 jakarta classifier(같은 라이브러리의 변종 산출물을 구분하는 좌표 조각)로 쓰는 것이 표준이다.
한계 ① 재귀 CTE·JSON 연산처럼 특정 DB 에만 있는 문법은 표현 한계가 있어 그 쿼리만 네이티브 SQL 로 빠진다. ② JPA 위에서 도는 도구라서, 영속성 컨텍스트 와 페치 전략을 모르면 QueryDSL 로 써도 지연 로딩 이 만드는 추가 쿼리는 그대로 발생한다. 도구가 아니라 로딩 전략이 원인이기 때문이다.