QueryDSL은 JPA 기반 프로젝트에서 쿼리를 타입-세이프하게 작성할 수 있는 강력한 도구다.
금융 IT처럼 데이터 무결성이 중요한 환경에서는 오타나 런타임 오류를 줄이는 게 생명인데, QueryDSL은 이걸 깔끔하게 해결해준다.
✅ QueryDSL이란?
- QueryDSL은 SQL 비슷한 문법을 자바 코드로 작성해서 데이터베이스 쿼리를 만드는 프레임워크다.
- JPA, MongoDB, SQL 등 다양한 백엔드를 지원하고, 타입-세이프 덕분에 컴파일 때 오류를 잡아준다.
- 문자열 기반 JPQL이나 SQL 쓰다가 오타로 런타임에 터지는 일을 줄이고, IDE 자동완성으로 쿼리 짜는 속도도 빨라진다.
💡 타입-세이프란?
코드 작성할 때 타입을 엄격히 체크해서 런타임 오류를 미리 막아준다. 예를 들어, 엔티티 필드 이름 틀리면 컴파일러가 바로 알려준다
✅ QueryDSL의 강점
- 타입-세이프 쿼리: 컴파일 때 오타나 문법 오류를 잡아서 런타임 사고 방지.
- 동적 쿼리: 조건에 따라 쿼리를 유연하게 변경 가능.
- 다양한 백엔드 지원: MySQL, MongoDB, Hibernate 등 뭐든 가능.
- IDE 자동완성: 쿼리 쓰면서 필드 이름이나 메서드 자동완성으로 생산성 올라감.
📌 타입-세이프 쿼리 생성
- QueryDSL은 자바 코드로 쿼리를 작성하니 컴파일 시점에 오류를 잡는다.
- JPQL은 문자열 기반이라 오타나 문법 오류를 런타임에야 발견하지만, QueryDSL은 .메서드() 형태로 쿼리를 구성해 IDE가 바로 문제점을 알려준다.
QTodo todo = QTodo.todo;
jpaQueryFactory
.selectFrom(todo)
.where(todo.titel.eq("계좌 이체"))
.fetch();
위 코드에서 todo.title을 todo.titel로 오타 내면 컴파일러가 바로 에러를 띄운다
💡 JPQL은 "SELECT t FROM Todo t WHERE t.title = :title"처럼 문자열로 쿼리를 작성한다.
오타나 잘못된 필드명을 런타임에야 알게 될 수 있다.
📌 다양한 데이터베이스 백엔드 지원
- QueryDSL은 JPA, Hibernate, MongoDB, SQL, JDO 등 다양한 백엔드를 지원한다.
- 같은 문법으로 RDBMS나 NoSQL 쿼리를 작성할 수 있어 특정 DB에 종속되지 않는다.
📌 동적 쿼리 지원
- QueryDSL은 조건에 따라 쿼리를 동적으로 구성할 수 있다.
- BooleanExpression을 사용하면 파라미터가 null일 때 조건을 제외해 간결하게 처리가능하다.
private BooleanExpression weatherEq(String weather) {
QTodo todo = QTodo.todo;
return weather != null && !weather.isEmpty() ? todo.weather.eq(weather) : null;
}
♐JpaRepository와 비교
- 조건이 많아질수록 query 문이 길어질 수 있다.
@Query("SELECT t FROM Todo t WHERE (:weather IS NULL OR t.weather = :weather)")
List<Todo> findByWeather(@Param("weather") String weather);
📌 4. IDE 자동완성 지원
- QueryDSL은 자바 코드 기반이라 IDE의 자동완성 기능을 활용할 수 있다.
- todo.user 처럼 필드 이름을 입력하면 IDE가 자동완성해주고, 메서드 호출도 실시간으로 확인 가능하다.
✅ QueryDSL vs 다른 기술
- QueryDSL을 제대로 이해하려면 SQL, JPQL, Criteria API와 비교해보는 게 좋다.
QueryDSL을 제대로 이해하려면 SQL, JPQL, Criteria API와 비교해보는 게 좋다. 아래는 간단한 비교표다.
기술타입-세이프동적 쿼리코드 복잡성객체 지향 쿼리
기술 | 타입-세이프동적 | 동적 쿼리 | 코드 복잡성 | 객체 지향 쿼리 |
SQL | ❌ | 제한적 | 간단 | ❌ |
JPQL | ❌ | 제한적 | 간단 | ❌ |
Criteria API | ✅ | 가능 | 복잡 | ✅ |
QueryDSL | ✅ | 가능 | 간단 | ✅ |
✅ QueryDSL 구성 요소와 흐름
- QueryDSL은 몇 가지 핵심 구성 요소로 동작한다.
📌 메타 모델: Q-Class
- Q-Class는 엔티티 클래스를 기반으로 생성된 메타 모델로, 쿼리 작성의 핵심이다.
- 예를 들어, Todo 엔티티가 있으면 QTodo 클래스가 생성된다.
생성 과정
- @Entity가 붙은 엔티티 클래스를 build.gradle의 APT(Annotation Processing Tool)가 분석.
- 컴파일 시 Q-Class 생성 (예: QTodo.java).
- Q-Class를 사용해 타입-세이프 쿼리 작성.
의존성 설정방법
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.1.0'
implementation 'com.querydsl:querydsl-apt:5.1.0'
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
💡 엔티티가 변경되면 Q-Class도 재생성해야 한다
📌 JPAQueryFactory
- QueryDSL 쿼리를 생성하고 실행하는 핵심 클래스다.
- EntityManager를 주입받아 설정한다.
♐ JPAConfiguration
@Configuration
public class JPAConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
JPAQueryFactory 생성자
생성자 | 설명 |
---|---|
JPAQueryFactory(JPQLTemplates templates, Supplier<javax.persistence.EntityManager> entityManager) |
JPQL 템플릿과 엔티티 매니저 공급자를 이용해 쿼리 팩토리를 생성 |
JPAQueryFactory(JPQLTemplates templates, javax.persistence.EntityManager entityManager) |
JPQL 템플릿과 엔티티 매니저를 이용해 쿼리 팩토리를 생성 |
JPAQueryFactory(Supplier<javax.persistence.EntityManager> entityManager) |
엔티티 매니저 공급자를 이용해 쿼리 팩토리를 생성 |
JPAQueryFactory(javax.persistence.EntityManager entityManager) |
엔티티 매니저를 이용해 쿼리 팩토리를 생성 |
JPAQueryFactory 메서드 (QueryDSL 5.1.0 기준)
메서드 | 리턴 값 | 설명 |
---|---|---|
delete(EntityPath<?> path) |
JPADeleteClause |
새로운 DELETE 절을 생성 |
from(EntityPath<?> from) |
JPAQuery<?> |
주어진 소스로 새 쿼리 생성 |
from(EntityPath<?>... from) |
JPAQuery<?> |
주어진 소스로 새 쿼리 생성 |
insert(EntityPath<?> path) |
JPAInsertClause |
새로운 INSERT 절을 생성 |
query() |
JPAQuery<?> |
새로운 쿼리 생성 |
select(Expression<?>... exprs) |
JPAQuery<Tuple> |
주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
select(Expression<T> expr) |
<T> JPAQuery<T> |
주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectDistinct(Expression<?>... exprs) |
JPAQuery<Tuple> |
주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectDistinct(Expression<T> expr) |
<T> JPAQuery<T> |
주어진 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectFrom(EntityPath<T> from) |
<T> JPAQuery<T> |
주어진 소스 및 프로젝션으로 새 JPQLQuery 인스턴스 생성 |
selectOne() |
JPAQuery<Integer> |
프로젝션을 이용하여 하나의 새 JPQLQuery 인스턴스 생성 |
selectZero() |
JPAQuery<Integer> |
프로젝션을 이용하여 0개의 새 JPQLQuery 인스턴스 생성 |
update(EntityPath<?> path) |
JPAUpdateClause |
새로운 UPDATE 절을 생성 |
https://javadoc.io/doc/com.querydsl/querydsl-jpa/latest/index.html
querydsl-jpa 5.1.0 javadoc (com.querydsl)
Latest version of com.querydsl:querydsl-jpa https://javadoc.io/doc/com.querydsl/querydsl-jpa Current version 5.1.0 https://javadoc.io/doc/com.querydsl/querydsl-jpa/5.1.0 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/co
javadoc.io
📌 JPAQuery
- JPAQuery는 실제 쿼리를 구성하는 클래스다.
- select, where, orderBy, groupBy 같은 메서드로 쿼리를 만든다.
ex)
QTodo todo = QTodo.todo;
jpaQueryFactory
.selectFrom(todo)
.where(todo.id.eq(1L))
.fetchOne();
주요 메서드
메서드 | 패키지 클래스 | 설명 |
---|---|---|
select | JPQLQueryFactory | 조회할 컬럼을 지정 |
from | JPQLQueryFactory | 조회할 테이블을 지정 |
selectFrom | JPQLQueryFactory | 조회할 테이블과 컬럼을 한번에 지정 |
selectDistinct | JPQLQueryFactory | 중복을 제거한 결과를 조회 |
insert | JPQLQueryFactory | 새로운 레코드 삽입 |
delete | JPQLQueryFactory | 레코드 삭제 |
distinct | QueryBase | 중복 제거 |
where | QueryBase | 특정 조건으로 데이터를 필터 |
orderBy | QueryBase | 결과를 특정 기준으로 정렬 |
groupBy | QueryBase | 특정 컬럼으로 그룹화 |
having | QueryBase | 그룹화된 결과에 추가 필터 적용 |
limit | QueryBase | 결과의 최대 개수를 제한 |
offset | QueryBase | 시작점을 지정하여 결과의 일부분만 반환 |
innerJoin | JPAQueryBase | 두 테이블의 교집합을 반환 |
fetchJoin | JPAQueryBase | 연관된 엔티티를 함께 조회 |
leftJoin | JPAQueryBase | 왼쪽 테이블의 모든 데이터와 오른쪽 테이블의 일치하는 데이터를 반환 |
참고자료
https://javadoc.io/doc/com.querydsl/querydsl-jpa/latest/com/querydsl/jpa/impl/JPAQuery.html
JPAQuery - querydsl-jpa 5.1.0 javadoc
Latest version of com.querydsl:querydsl-jpa https://javadoc.io/doc/com.querydsl/querydsl-jpa Current version 5.1.0 https://javadoc.io/doc/com.querydsl/querydsl-jpa/5.1.0 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/co
javadoc.io
📌Projection: 특정 필드 조회
- 엔티티의 특정 필드만 조회할 때 사용한다. 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있다.
- Tuple, Projections.fields, Projections.beans, Projections.constructor, @QueryProjection 등이 있다.
타입/메서드/어노테이션 | 설명 |
---|---|
tuple | 여러 컬럼의 값을 한 번에 가져올 수 있는 구조입니다. 각 컬럼의 값들을 한번에 담을 수 있는 역할을 합니다. |
Projections.field | 주어진 표현에 대한 프로젝션을 생성하는 데 사용됩니다. 이 메서드는 특정 필드만 선택하여 조회하는 방법으로 사용되며, 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다. |
Projections.beans | 주어진 표현식에 대해 Bean을 채우는 프로젝션을 생성하는 데 사용됩니다. 이 메소드는 원하는 필드만을 선택하여 특정 Bean에 담아 반환하는 방법으로 사용되며, 이를 통해 필요한 정보만 효율적으로 가지고 올 수 있습니다. |
Projections.constructor | 주어진 유형과 표현식을 이용해 생성자 호출 프로젝션을 생성하는 데 사용됩니다. 이 메소드는 원하는 필드를 선택하여 특정 클래스의 생성자에 매핑하여 객체를 생성하는 방법으로 사용됩니다. 이를 통해 필요한 정보만을 선택적으로 가지고 와서 특정 객체를 효율적으로 생성할 수 있습니다. |
@QueryProjection | Querydsl이 제공하는 어노테이션으로, 이를 생성자에 붙여주면 해당 생성자를 이용한 조회를 간편하게 할 수 있습니다. 이를 통해 필요한 정보만을 선택적으로 가지고 와서 특정 객체를 효율적으로 생성할 수 있습니다. |
♐Tuple
- 엔티티의 특정한 값만 가져올 수 있다.
QTodo todo = QTodo.todo;
List<Tuple> result = jpaQueryFactory
.select(todo.title, todo.weather)
.from(todo)
.fetch();
♐Projections.fields
특정 필드만 선택하여 조회하는 방법으로 사용된다.
public class TodoResponseDto {
private Long id;
private String title;
// getters, setters
}
QTodo todo = QTodo.todo;
List<TodoResponseDto> result = jpaQueryFactory
.select(Projections.fields(TodoResponseDto.class, todo.id, todo.title))
.from(todo)
.fetch();
♐ @QueryProjection
- Querydsl이 제공하는 어노테이션이다.
public class TodoDto {
private Long id;
private String title;
@QueryProjection
public TodoDto(Long id, String title) {
this.id = id;
this.title = title;
}
}
QTodo todo = QTodo.todo;
TodoDto result = jpaQueryFactory
.select(new QTodoDto(todo.id, todo.title))
.from(todo)
.where(todo.id.eq(1L))
.fetchOne();
📌 동적 조건절: BooleanExpression
- BooleanExpression은 동적 쿼리의 조건을 구성한다.
- null 체크를 통해 유연한 조건 처리가 가능하다.
private BooleanExpression weatherEq(String weather) {
QTodo todo = QTodo.todo;
return weather != null && !weather.isEmpty() ? todo.weather.eq(weather) : null;
}
@Override
public Page<Todo> findByWeatherAndModifiedAtQuery(String weather, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {
QTodo todo = QTodo.todo;
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
builder.and(weatherEq(weather));
builder.and(startDateExp(startDate));
builder.and(endDateExp(endDate));
List<Todo> content = jpaQueryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(builder)
.orderBy(todo.modifiedAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<Long> totalCountQuery = jpaQueryFactory
.select(todo.count())
.from(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(builder);
return PageableExecutionUtils.getPage(content, pageable, ()->totalCountQuery.fetch().size());
}
📌 결과 처리: Fetchable
- fetch(), fetchOne(), fetchResults() 등으로 쿼리 결과를 가져온다.
List<Todo> todos = jpaQueryFactory
.selectFrom(QTodo.todo)
.fetch();
Todo todo = jpaQueryFactory
.selectFrom(QTodo.todo)
.where(QTodo.todo.id.eq(1L))
.fetchOne();
QueryResults<Todo> results = jpaQueryFactory
.selectFrom(QTodo.todo)
.fetchResults();
주요 메소드
메서드 | 리턴 값 | 결과 | 설명 |
---|---|---|---|
fetch() | List<T> | 여러개 반환 값 | 쿼리를 실행하여 결과를 리스트로 가져옵니다. 결과가 없는 경우에는 빈 리스트를 반환합니다. |
fetchCount() | long | 결과의 수 | count 쿼리를 실행하여 결과 수를 가져옵니다. |
fetchOne() | T | 단일 반환 값 | 쿼리를 실행하여 단일 결과를 가져옵니다. 결과가 없는 경우에는 null을 반환합니다. 결과가 둘 이상인 경우에는 NonUniqueResultException을 발생시킵니다. |
fetchFirst() | T | 단일 반환 값(첫번째) | 쿼리를 실행하여 첫 번째 결과를 가져옵니다. 결과가 없는 경우에는 null을 반환합니다. |
fetchResults() | QueryResults<T> | 전체 결과 수, 페이지 정보 등 | 쿼리를 실행하여 결과와 함께 전체 결과 수를 포함하는 QueryResults 객체를 가져옵니다. |
✅ QueryDSL의 동작 원리
- QueryDSL이 이렇게 잘 동작하는 이유는 몇 가지 핵심 메커니즘 덕분이다.
📌 DSL(Domain-Specific Language)
- QueryDSL은 특정 도메인(데이터베이스 쿼리)에 최적화된 언어다.
- SQL이나 JPQL을 자바 코드로 대체해 타입-세이프와 IDE 지원을 제공한다.
📌 Q-Class와 메타 모델
- Q-Class는 엔티티의 구조를 반영한 메타 모델로, 쿼리 작성의 기반이다.
- 컴파일 시 APT가 엔티티를 분석해 Q-Class를 생성한다. Q-Class가 없으면 타입-세이프 쿼리는 불가능하다.
📌 JPA와의 보완 관계
- JPA는 데이터베이스 상호작용을 추상화하고, QueryDSL은 안전하고 효율적인 쿼리 작성을 돕는다.
- Hibernate와 함께 쓰면 엔티티 매핑과 쿼리 처리가 완벽해진다.
📌 JDO 지원
- QueryDSL은 JDO(Java Data Objects)도 지원한다. JDO는 객체 지향 방식으로 데이터베이스에 접근한다.
✅ QueryDSL를 쓸때 주의해야 할 점
📌 Q-Class 동기화
- 엔티티 구조가 바뀌면 Q-Class를 재생성해야 한다.
- build.gradle 설정을 꼼꼼히 하고, 빌드마다 확인하자. 동기화 안 맞으면 쿼리 오류로 데이터 불일치 생긴다.
📌 성능 최적화
- fetchJoin은 강력하지만, 데이터량 많을 때 메모리 부담 크다. 금융 시스템에서는 적절히 사용.
- Projections로 필요한 필드만 조회해 네트워크 부하 줄이기.
📌 복잡한 쿼리 가독성
- 쿼리가 길어지면 BooleanBuilder나 메서드 분리로 가독성을 높이자.
- 예를 들어, 위 코드의 weatherEq, startDateExp는 조건 로직을 깔끔히 분리한 사례다.
'SQL' 카테고리의 다른 글
[DB]Lock 이란? (0) | 2025.06.05 |
---|---|
[DB] 트랜잭션 (3) | 2025.05.01 |
[DB] H2 란? (2) | 2025.04.30 |
[SQL]UPPER/LOWER 대소문자 구분없이 Like 사용 (1) | 2025.03.05 |
[SQL]CSV 파일을 이용해 데이터 가져오기(MY SQL) (0) | 2025.01.20 |