데이터베이스 용어 사전
SQLQ 클래스 · BooleanBuilder · 동적 쿼리

QueryDSL

자바 코드로 타입 안전한 쿼리를 조립하는 라이브러리. 동적 조건이 많은 화면에서 가장 빛난다.

자바 코드로 쿼리를 조립하는 라이브러리. 문자열이 아니라 코드라서 오타·타입 오류가 컴파일 단계에서 걸린다.

2026년 현재 — 어떤 것을 쓰나

원본 querydsl/querydsl 은 오랫동안 정체돼 있었다

  • OpenFeign 포크(io.github.openfeign.querydsl)가 사실상 유지보수 주체
  • Jakarta EE · 최신 JDK 대응이 여기서 이뤄진다

새 프로젝트라면 포크 쪽 좌표를 확인하고 시작한다

Q 클래스 — 엔티티의 타입세이프 거울

빌드 시점에 애노테이션 프로세서가 엔티티를 읽어

  • Member — → QMember (member.name · member.age · member.team ...) 생성한다
QMember m = QMember.member;

List<Member> result = queryFactory
    .selectFrom(m)
    .where(m.age.gt(20)                    // age 가 없거나 숫자가 아니면 컴파일 에러
           .and(m.name.startsWith("김")))
    .orderBy(m.age.desc())
    .fetch();

가장 빛나는 곳 — 동적 쿼리

검색 화면처럼 조건이 있을 수도 없을 수도 있는 경우, 문자열 SQL은 금방 지저분해진다.

// null 인 조건은 자동으로 무시된다 — where 에 null 을 넘기면 그 조건이 빠진다
private BooleanExpression nameLike(String name) {
    return name != null ? m.name.contains(name) : null;
}
private BooleanExpression ageGoe(Integer age) {
    return age != null ? m.age.goe(age) : null;
}

queryFactory.selectFrom(m)
    .where(nameLike(cond.getName()), ageGoe(cond.getAge()))   // 둘 다 null 이면 조건 없음
    .fetch();

이 패턴은 조건 메서드를 재사용할 수 있다는 것이 진짜 이득이다. BooleanBuilder보다 조합성이 좋다.

JPQL·네이티브 SQL과의 자리

  • QueryDSL — 동적 조건이 많다 · 컴파일 검증이 필요하다 · 조건을 재사용한다
  • JPQL — 정적이고 단순한 쿼리 (@Query 한 줄이 더 읽기 쉽다)
  • 네이티브 SQL — DB 고유 문법 · 윈도우 함수 · 복잡한 튜닝

한계와 비용

  • 빌드에 애노테이션 프로세싱 단계가 붙는다 (Q 클래스 미생성 오류가 흔한 첫 장벽)
  • 엔티티가 바뀌면 재생성해야 한다
  • 결국 JPQL 로 번역되므로 JPA 의 제약을 그대로 받는다
    • (컬렉션 페치 조인 + 페이징 불가 등)
  • 아주 복잡한 통계 쿼리는 네이티브가 낫다

면접 함정

  • "QueryDSL은 SQL을 직접 만든다" → JPQL로 번역되고, 그다음 JPA 구현체가 SQL을 만든다.
  • "QueryDSL을 쓰면 N+1이 없다" → 무관하다. 페치 조인을 명시해야 하는 건 똑같다.

프로젝션 — 필요한 칼럼만 가져온다

// 엔티티 전체가 아니라 DTO 로 바로 받는다 (SELECT * 를 피한다)
List<MemberDto> dtos = queryFactory
    .select(Projections.constructor(MemberDto.class, m.id, m.name))
    .from(m)
    .fetch();

페이징에서 자주 하는 실수

// ❌ 컬렉션 페치 조인 + 페이징 → 메모리에서 페이징한다 (전체를 다 읽는다)
queryFactory.selectFrom(m).join(m.orders).fetchJoin()
    .offset(0).limit(10).fetch();

// ✅ id 만 페이징으로 먼저 뽑고, 그 id 들로 페치 조인한다
List<Long> ids = queryFactory.select(m.id).from(m).offset(0).limit(10).fetch();
List<Member> result = queryFactory.selectFrom(m)
    .join(m.orders).fetchJoin().where(m.id.in(ids)).fetch();

JPA의 제약이 그대로 적용된다는 점을 보여주는 대표 사례다. QueryDSL은 문법을 안전하게 해 줄 뿐 실행 모델을 바꾸지 않는다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — QueryDSL — 타입세이프 동적 쿼리(OpenFeign 포크)