Spring

QueryDSL 의존성 설정 및 활용

윤승 2025. 5. 13. 14:43

QueryDSL을 사용하기 전에 먼저 QueryDSL이란 무엇인지 알아보자.

 

QueryDSL이란?
QueryDSL은 도메인 모델을 기반으로 한 타입 안전(type-safe)한 쿼리 작성 라이브러리

 

JPA를 쓰다 보면 이런 경험 한 번쯤 해봤을 거다.
복잡한 조건을 가진 쿼리문들을 사용하게 되면 JPQL로 하자니 불편하고, 메서드 이름도 길어지고...
이럴 때 사용하는 게 바로 QueryDSL이다.
QueryDSL은 자바 코드로 SQL을 짤 수 있게 해주는 안전한 쿼리 빌더 타입이다!

 

✅  QueryDSL 관련 의존성 및 설정

QueryDSL을 사용하기 위해서는 다음과 같은 의존성들과 설정이 필요하다.

스프링부트3 버전을 사용 중이다.

 

build.gradle 파일

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.3'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // 🔽 QueryDSL 관련 의존성 추가
    // QueryDSL JPA 연동 (jakarta API 기반)
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    
    // Q타입 클래스 생성을 위한 APT 프로세서
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

    // bcrypt
    implementation 'at.favre.lib:bcrypt:0.10.2'

    // jwt
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
}

// QueryDSL의 Q타입 클래스가 생성될 디렉토리 지정
def querydslSrcDir = 'src/main/generated'

// clean 명령 시 생성된 Q타입 디렉토리 삭제
clean {
    delete file(querydslSrcDir)
}


tasks.named('test') {
    useJUnitPlatform()
}

 

 

 

관련 의존성들과 설정을 해주었다면,

Gradle에 들어가서 compile.java를 실행해 주면 된다.

 

실행하였다면, 

 

이런 식으로 Q클래스들이 생성되어 있는 것을 알 수 있다.

 

QueryDSL을 사용하려면, Q 클래스가 정상적으로 생성된 후 JPAQueryFactory를 Bean으로 등록해야 한다.
이를 위해 별도의 @Configuration 파일을 작성해 JPAQueryFactory를 스프링 컨테이너에 등록해 주는 작업이 필요하다.
@Configuration
public class JPAConfiguration {
    
    @PersistenceContext
    private EntityManager em;

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

 

 

  • @Configuration: 이 클래스가 스프링 컨테이너에 Bean으로 등록된다.
  • @PersistenceContext는 스프링이 해당 필드에 적절한 EntityManager를 주입해 준다.
    • EntityManager는 JPA의 핵심 인터페이스로, 쿼리 실행, 엔티티 저장/조회/수정 등을 담당한다.
  • JPAQueryFactory는 QueryDSL의 핵심 객체로, 이걸 통해 타입 안전한 쿼리(QueryDSL 문법)를 실행할 수 있다.

 

모든 설정들이 완료되었다면, 이제 QueryDSL을 사용해 보자.

 

기존 JPQL(@Query) 방식

@Query("SELECT t FROM Todo t " +              // Todo 엔티티를 선택
       "LEFT JOIN t.user " +               
       "WHERE t.id = :todoId")                // 조건: Todo의 id가 todoId인 경우
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId); //NULL방지

 

 

  • 문자열 기반 JPQL을 @Query 어노테이션 안에 작성
  • 조건은 :파라미터명 형식으로 바인딩
  • fetch join을 쓰지 않으면 지연 로딩 발생 가능

QueryDSL 방식

@RequiredArgsConstructor // final 필드를 자동 생성자 주입해주는 롬복 어노테이션
public class TodoRepositoryQuerydslImpl implements TodoRepositoryQuerydsl {

    private final JPAQueryFactory jpaQueryFactory; // QueryDSL 쿼리를 실행할 수 있는 핵심 객체

    @Override
    public Optional<Todo> findByIdWithUser(Long todoId) {
        return Optional.ofNullable(
            jpaQueryFactory.select(QTodo.todo)        // Todo 엔티티를 선택
                .from(QTodo.todo)                     // Todo 테이블로부터
                .leftJoin(QTodo.todo.user)            // Todo와 연결된 User를 left join
                .fetchJoin()                          // 즉시 로딩(fetch join)으로 User 정보까지 한 번에 가져옴
                .where(QTodo.todo.id.eq(todoId))      // 조건: id가 todoId인 데이터
                .fetchOne()                           // 단일 결과 조회 (없으면 null 반환)
        );
    }
}

 

 

  • 컴파일 타임에 오류 확인 가능 → 타입 안정성 확보
  • 가독성이 높고 유지보수에 유리
  • 복잡한 조건, 동적 쿼리 구성에 매우 강력

Repository 로직 흐름