contents
QueryDSL은 타입 세이프(type-safe)한 SQL과 유사한 쿼리를 생성할 수 있게 해주는 자바 프레임워크입니다. 오류가 발생하기 쉬운 문자열(JPQL이나 네이티브 SQL 등)로 쿼리를 작성하는 대신, 직관적이고 플루언트(fluent)한 자바 API를 사용하여 쿼리를 작성할 수 있습니다.
마치 SQL처럼 읽기 쉽지만, 컴파일러와 IDE가 오류를 확인하고 자동 완성을 제공해주는 일반 자바 코드처럼 안전하게 데이터베이스 쿼리를 작성하는 방법이라고 생각할 수 있습니다.
QueryDSL이 해결하는 문제
QueryDSL은 JPA 환경에서 데이터베이스 쿼리를 작성하는 두 가지 주요 방식의 중대한 한계를 해결하기 위해 만들어졌습니다.
-
문자열 기반 쿼리 (JPQL/SQL):
-
문제점: 자바 컴파일러에게는 이들은 그냥
String변수일 뿐입니다. -
타입 안정성 부재: 엔티티 이름이나 필드 이름을 잘못 입력하면(
"SELECT c FROM Custome c WHERE c.fistName = ?"), 이 오류는 쿼리가 실행되는 런타임에만 발견되어 애플리케이션 충돌을 일으킵니다. -
IDE 지원 부재: 문자열 내에서는 엔티티나 필드 이름에 대한 자동 완성을 받을 수 없습니다.
-
어려운 동적 쿼리: 선택적인
WHERE절을 포함하는 쿼리를 만들려면 지저분하고 오류가 발생하기 쉬운 문자열 연결이 필요합니다(if (city != null) { query += " AND c.city = ..."; }).
-
-
JPA Criteria API:
- 문제점: 타입 세이프하기는 하지만, Criteria API는 악명 높을 정도로 장황하고, 읽기 어려우며, 작성하기에 직관적이지 않습니다. 간단한 쿼리 하나에 여러 줄의 복잡한 상용구 코드가 필요하여 유지보수가 어렵습니다.
QueryDSL은 완벽한 중간 지점을 제공합니다: Criteria API의 완전한 타입 안정성과 거의 JPQL만큼 간결하고 가독성 좋은 문법을 모두 제공합니다.
QueryDSL의 작동 원리: Q-타입의 마법 ⚙️
QueryDSL의 핵심 메커니즘은 코드 생성입니다.
-
애노테이션 처리: 프로젝트 빌드 과정(예: Maven이나 Gradle로 컴파일할 때)에서 QueryDSL의 애노테이션 프로세서가 JPA 엔티티 클래스(예:
@Entity애노테이션이 붙은Customer클래스)를 스캔합니다. -
Q-타입 생성: 각 엔티티에 대해, 해당하는 "메타모델" 클래스인 Q-타입을 생성합니다.
Customer.java엔티티가 있다면,QCustomer.java클래스가 생성됩니다. -
플루언트 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();
-
selectFrom(customer):Customer엔티티를 대상으로 쿼리를 시작합니다. -
.where(...): 필터링 절입니다. -
customer.city.eq("Seoul"): 이것이 타입 세이프한 조건입니다.customer.city는StringPath이고,.eq()는 동등 비교를 위한 메서드입니다..ne()(같지 않음),.gt()(보다 큼),.like()등 다른 메서드들도 있습니다. -
.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의 장점과 단점 ⚖️
장점 ✅
-
타입 안정성: 오타나 오류를 런타임이 아닌 컴파일 타임에 잡아냅니다.
-
IDE 자동 완성: 개발자 생산성을 극적으로 향상시키고 실수를 줄여줍니다.
-
가독성: 쿼리가 구조화된 플루언트 스타일로 작성되어 읽고 유지보수하기 쉽습니다.
-
쉬운 동적 쿼리: 조건부 쿼리를 간단하고, 안전하며, 우아하게 작성할 수 있습니다.
-
다용성: JPA와 가장 많이 사용되지만, QueryDSL은 SQL, JDO, Lucene, 심지어 MongoDB를 위한 모듈도 가지고 있습니다.
단점 ❌
-
설정 오버헤드: 코드 생성을 위한 애노테이션 프로세서를 설정하기 위해 빌드 도구(Maven 또는 Gradle)에 초기 구성이 필요합니다.
-
생성된 코드: 프로젝트에 생성된 Q-타입 소스 파일 세트가 추가되어, 일부 개발자에게는 거추장스럽게 느껴질 수 있습니다.
-
학습 곡선: 문법이 직관적이긴 하지만, 프로젝션, 서브쿼리 등 모든 기능을 마스터하는 데는 약간의 시간이 걸립니다.
references