[QueryDSL] QueryDSL 쓸려면 제대로 알고 쓰자

2025. 5. 8. 20:41·SQL

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 클래스가 생성된다.

 

생성 과정

  1. @Entity가 붙은 엔티티 클래스를 build.gradle의 APT(Annotation Processing Tool)가 분석.
  2. 컴파일 시 Q-Class 생성 (예: QTodo.java).
  3. 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
'SQL' 카테고리의 다른 글
  • [DB]Lock 이란?
  • [DB] 트랜잭션
  • [DB] H2 란?
  • [SQL]UPPER/LOWER 대소문자 구분없이 Like 사용
코딩로봇
코딩로봇
금융 IT 개발자
  • 코딩로봇
    쟈니의 일지
    코딩로봇
  • 전체
    오늘
    어제
    • 분류 전체보기 (152)
      • JavaScript (8)
      • SQL (11)
      • 코딩테스트 (30)
        • Java (15)
        • SQL (13)
      • Java (10)
      • 프로젝트 (30)
        • 트러블슈팅 (10)
        • 프로젝트 회고 (18)
      • git,Github (2)
      • TIL (38)
      • Spring (20)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스파르타 코딩 #부트캠프 #첫ot
    java #arraylist #list #배열
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
코딩로봇
[QueryDSL] QueryDSL 쓸려면 제대로 알고 쓰자
상단으로

티스토리툴바