contents

QueryDSL타입 세이프(type-safe)한 SQL과 유사한 쿼리를 생성할 수 있게 해주는 자바 프레임워크입니다. 오류가 발생하기 쉬운 문자열(JPQL이나 네이티브 SQL 등)로 쿼리를 작성하는 대신, 직관적이고 플루언트(fluent)한 자바 API를 사용하여 쿼리를 작성할 수 있습니다.

마치 SQL처럼 읽기 쉽지만, 컴파일러와 IDE가 오류를 확인하고 자동 완성을 제공해주는 일반 자바 코드처럼 안전하게 데이터베이스 쿼리를 작성하는 방법이라고 생각할 수 있습니다.


QueryDSL이 해결하는 문제

QueryDSL은 JPA 환경에서 데이터베이스 쿼리를 작성하는 두 가지 주요 방식의 중대한 한계를 해결하기 위해 만들어졌습니다.

  1. 문자열 기반 쿼리 (JPQL/SQL):

    • 문제점: 자바 컴파일러에게는 이들은 그냥 String 변수일 뿐입니다.

    • 타입 안정성 부재: 엔티티 이름이나 필드 이름을 잘못 입력하면("SELECT c FROM Custome c WHERE c.fistName = ?"), 이 오류는 쿼리가 실행되는 런타임에만 발견되어 애플리케이션 충돌을 일으킵니다.

    • IDE 지원 부재: 문자열 내에서는 엔티티나 필드 이름에 대한 자동 완성을 받을 수 없습니다.

    • 어려운 동적 쿼리: 선택적인 WHERE 절을 포함하는 쿼리를 만들려면 지저분하고 오류가 발생하기 쉬운 문자열 연결이 필요합니다(if (city != null) { query += " AND c.city = ..."; }).

  2. JPA Criteria API:

    • 문제점: 타입 세이프하기는 하지만, Criteria API는 악명 높을 정도로 장황하고, 읽기 어려우며, 작성하기에 직관적이지 않습니다. 간단한 쿼리 하나에 여러 줄의 복잡한 상용구 코드가 필요하여 유지보수가 어렵습니다.

QueryDSL은 완벽한 중간 지점을 제공합니다: Criteria API의 완전한 타입 안정성과 거의 JPQL만큼 간결하고 가독성 좋은 문법을 모두 제공합니다.


QueryDSL의 작동 원리: Q-타입의 마법 ⚙️

QueryDSL의 핵심 메커니즘은 코드 생성입니다.

  1. 애노테이션 처리: 프로젝트 빌드 과정(예: Maven이나 Gradle로 컴파일할 때)에서 QueryDSL의 애노테이션 프로세서가 JPA 엔티티 클래스(예: @Entity 애노테이션이 붙은 Customer 클래스)를 스캔합니다.

  2. Q-타입 생성: 각 엔티티에 대해, 해당하는 "메타모델" 클래스인 Q-타입을 생성합니다. Customer.java 엔티티가 있다면, QCustomer.java 클래스가 생성됩니다.

  3. 플루언트 API: 이렇게 생성된 Q-타입 클래스는 엔티티와 그 속성을 나타내는 정적 필드를 포함합니다. 이 필드들은 깔끔한 플루언트 API를 사용하여 쿼리를 작성하기 위한 빌딩 블록 역할을 합니다.

예를 들어, Customer 엔티티에 firstName 필드가 있다면, 생성된 QCustomer 클래스는 QCustomer.customer.firstName을 통해 접근할 수 있게 해줍니다. 이것은 실제 자바 객체이므로, IDE가 자동 완성해주고 컴파일러가 그 존재를 확인할 수 있습니다.


핵심 문법 및 사용법 🔎

일반적인 JPA 애플리케이션에서 QueryDSL을 어떻게 사용하는지 살펴보겠습니다.

기본 설정

먼저, 쿼리 작성을 위한 주 진입점인 JPAQueryFactory를 인스턴스화합니다.

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QCustomer customer = QCustomer.customer; // 생성된 Q-타입 인스턴스를 가져옵니다.

간단한 SELECT 쿼리

서울에 사는 모든 고객을 찾는 방법은 다음과 같습니다.

List customersFromSeoul = queryFactory
    .selectFrom(customer)
    .where(customer.city.eq("Seoul"))
    .fetch();

동적 쿼리 (가장 큰 강점)

QueryDSL은 동적 쿼리를 매우 우아하게 만들어 줍니다. 선택적 조건에 따라 사용자를 검색하고 싶다고 가정해 봅시다.

public List search(String name, Integer minAge) {
    QCustomer customer = QCustomer.customer;
    
    BooleanBuilder builder = new BooleanBuilder();
    
    if (name != null) {
        builder.and(customer.name.contains(name));
    }
    if (minAge != null) {
        builder.and(customer.age.goe(minAge)); // goe = 크거나 같음
    }
    
    return queryFactory
        .selectFrom(customer)
        .where(builder)
        .fetch();
}

이는 JPQL 문자열을 연결하는 것보다 훨씬 깔끔하고 안전합니다.

조인과 프로젝션 (DTO)

조인은 엔티티의 관계를 따라가므로 매우 직관적입니다. 또한 결과를 DTO(Data Transfer Object)로 직접 프로젝션하는 것도 쉽습니다.

// 특정 제품을 주문한 고객의 이름을 찾기
QOrder order = QOrder.order;
QProduct product = Q.product;

List customerNames = queryFactory
    .select(customer.name)
    .from(customer)
    .join(customer.orders, order) // Customer.orders 컬렉션 조인
    .join(order.product, product)
    .where(product.name.eq("Laptop"))
    .fetch();

QueryDSL의 장점과 단점 ⚖️

장점 ✅

단점 ❌

references