개발지식 먹는 하마 님의 블로그

[Spring] QueryDSL 기초 + 페이징 적용 본문

Web

[Spring] QueryDSL 기초 + 페이징 적용

devhippo 2025. 5. 7. 23:23

QueryDSL

데이터 조회 시, 간단한 조회는 JPA를 통한 메서드명 설정만으로 조회를 할 수 있지만,
복잡한 조회를 위해서는 JPQL로 쿼리문을 문자열로 작성해야 한다.
이는 가독성이 떨어지고 오타와 같은 휴먼 에러가 발생하기 쉽다.

QueryDSL은 JPQL을 Java 코드로 안전하게 작성할 수 있도록 도와주는 프레임워크로 JPQL의 단점을 보완한다.

  • 가독성이 좋다.
  • 문법 오류 없이 안전하게 작성할 수 있다.
  • IDE 자동완성이 지원된다.

QueryDSL 설정 방법

1. 의존성 추가

✅ 의존성

dependencies {
    implementation 'com.querydsl:querydsl-jpa'
    annotationProcessor 'com.querydsl:querydsl-apt'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
}
항목 설명
implementation 'com.querydsl:querydsl-jpa' QueryDSL의 JPA 기능을 사용하기 위한 런타임 라이브러리
annotationProcessor 'com.querydsl:querydsl-apt' Q타입 클래스를 생성하는 APT
annotationProcessor 'jakarta.persistence:jakarta.persistence-api' QueryDSL APT가 JPA 어노테이션을 해석하기 위해 필요

 

✅ Q타입 생성 디렉터리 설정

def querydslDir = "$buildDir/generated/querydsl"

sourceSets {
    main.java.srcDirs += [querydslDir]
}

tasks.withType(JavaCompile) {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

Q타입 클래스가 생성될 디렉터리 경로를 설정한다.

💡Q타입 클래스란?

QueryDSL의 쿼리 전용 클래스
User라는 Entity가 있으면 QUser를 컴파일 시점에 자동 생성한다!

 

Kotlin을 사용하면 의존성 설정이 이렇게 변해요!

Kotlin 기반으로 설정한다면 위의 Java 기반 설정을 아래의 의존성으로 대체할 수 있다.

dependencies {
    implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"
    kapt "com.querydsl:querydsl-apt:5.0.0:jakarta"
    kapt "jakarta.persistence:jakarta.persistence-api"
    kapt "jakarta.annotation:jakarta.annotation-api"
}

annotationProcessor 'com.querydsl:querydsl-apt' ➡️ kapt "com.querydsl:querydsl-apt:5.0.0:jakarta"

annotationProcessor 'jakarta.persistence:jakarta.persistence-api' ➡️ kapt "jakarta.persistence:jakarta.persistence-api"

: Kotlin에서 어노테이션 프로세서는 kapt로 대체된다.

디렉터리 경로도 kapt가 자동으로 build/generated/source/kapt/main를 사용하기 때문에 
직접 지정해줄 필요가 없어진다 👍


2. JPAQueryFactory 빈 등록

 

https://lordofkangs.tistory.com/464

QueryDSL은 JPA와 같이 EntityManager가 자동으로 주입되지 않기 때문에 이를 위한 설정을 해주어야 한다.

@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}

기본 메서드  
select() / selectFrom()  
from()  
where()  
groupBy()  
having()  
orderBy()  
limit()  
fetch() 리스트 결과 조회
fetchOne() 단일 결과 조회

 

조건절

조건 설명
.eq() 같다
.ne() 같지 않다
.gt(), .lt() 초과, 미만
.between() 범위
.contatins(), .startswith() 문자열 포함, 시작

페이징 with QueryDSL

페이징 할 때는 조회용 쿼리 외에 카운트 쿼리도 사용해주어야 한다.
그러다 보니 쿼리가 매우 길어질 수 있다.

그렇다면 유사한 부분을 공통으로 뺄 수 없을까?

반복되는 Query 처리

공통으로 처리할 수 있다!

특히 아래의 예시와 같은 Where문의 경우 조건이 늘어날수록 매우 길어지기 때문에 공통으로 처리하면 코드 길이가 줄어든다.

private BooleanExpression[] buildTodoSearchConditions(String title, String nickname, LocalDate startAt, LocalDate endAt) {
    return new BooleanExpression[] {
        (title != null && !title.isBlank()) ? todo.title.contains(title) : null,
        (nickname != null && !nickname.isBlank()) ? writer.nickname.contains(nickname) : null,
        (startAt != null) ? todo.createdAt.goe(startAt.atStartOfDay()) : null,
        (endAt != null) ? todo.createdAt.loe(endAt.atTime(LocalTime.MAX)) : null
    };
}

//실제 사용
.where(buildTodoSearchConditions(title, nickname, startAt, endAt))


그러나 공통된 내용을 무조건 묶어서 처리하는 것은 주의해야 한다.
특히 아래와 같은 예시가 그러하다.

private JPAQuery<?> baseTodoQuery() {
return jpaQueryFactory
.from(todo)
.leftJoin(todo.managers, manager)
.leftJoin(manager.user, managerUser)
.join(todo.user, writer);
}

//실제 사용
List<TodoSearchResponse> results = baseTodoQuery()
    .select(...)...

예시는 from과 Join 쿼리가 공통되기 때문에 이를 묶어서 처리한 것이다.
그러나 이를 사용할 경우 실제 사용부분의 코드와 같이 사용하게 되면 select절 앞에 from과 join절이 사용되게 된다.

QueryDSL은 Builder 패턴 기반이기 때문에 유연한 메서드 체이닝이 가능하므로 순서가 뒤바뀌는 것은 문제가 되지 않는다.

그러나, 기존에 쓰던 형식과 다르기 때문에 협업이나 컨벤션 관점에서 좋지 않다.

따라서, 반복된다고 무작정 공통으로 묶는 것이 아니라
메서드명으로 "이 쿼리는 이런 부분을 공통으로 묶었어!" 라고 명확하게 표현할 수 있는 부분
공통으로 묶어서 처리하는 것이 권장된다!