QueryDSL
- JPA에서 제공하는 객체지향쿼리인 JPQL(Java Persistence Query Language)을 통해 동적 쿼리를 구성하면 코드가 굉장히 난잡해진다는 것을 느낄 수 있다.
- JPQL은 문자열을 사용한다. 문자열을 조건에 따라 이어붙이는 형식으로 구성하기 때문에 생기는 문제가 있다. 문자열이기에 오타가 발생해도 컴파일 단계에서 에러를 잡아주지 못한다.(다만, NamedQuery를 사용하면 가능하다) 또한 동적쿼리를 구성할 때, 중간중간 if문에 의해 문자열이 추가되기 때문에 가독성이 떨어진다. 따라서 쿼리를 체계적으로 관리하기 어렵다.
- QueryDSL은 위와 같은 문제를 해결하기 위해 만들어졌다. Type-Safe한 쿼리를 사용하기 위해 엔티티와 매핑되는 정적 타입 QClass를 생성해 쿼리를 생성할 수 있게 만들었다. 컴파일 단계에서 오류를 잡아낼 수 있고 메소드 체이닝을 통해 조건을 보다 쉽게 추가할 수 있다. 즉, 동적 쿼리를 작성할 때 용이해진다는 말이다.
- QueryDSL도 내부적으로는 JPQL를 구성해 쿼리를 만들어낸다. 다만 사용자들이 편하게 사용할 수 있게 추상화해놓은 JPQL 빌더라고 생각하면 된다.
Gradle 설정
- Spring Boot 3.x 버전 기준
- QueryDSL Implementation 부분과 QueryDSL Build Options 부분을 추가
build.gradle (groovy)
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
}
group = 'com.pythonstrup'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
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'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// QueryDSL Implementation
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
test {
useJUnitPlatform()
}
/**
* QueryDSL Build Options
*/
def querydslDir = "src/main/generated"
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}
clean.doLast {
file(querydslDir).deleteDir()
}
build.gradle.kts (kotlin)
- 코틀린의 경우, QeuryDSL Version Setting 주석 부분도 추가
plugins {
java
id("org.springframework.boot") version "3.1.2"
id("io.spring.dependency-management") version "1.1.2"
id("org.asciidoctor.jvm.convert") version "3.3.2"
}
group = "com.pythonstrup"
version = "0.0.1-SNAPSHOT"
val queryDslVersion = "5.0.0" // QueryDSL Version Setting
java {
sourceCompatibility = JavaVersion.VERSION_17
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
extra["snippetsDir"] = file("build/generated-snippets")
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok")
runtimeOnly("com.mysql:mysql-connector-j")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
// QueryDSL Implementation
implementation ("com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta")
annotationProcessor("com.querydsl:querydsl-apt:${queryDslVersion}:jakarta")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
}
tasks.withType<Test> {
useJUnitPlatform()
}
/**
* QueryDSL Build Options
*/
val querydslDir = "src/main/generated"
sourceSets {
getByName("main").java.srcDirs(querydslDir)
}
tasks.withType<JavaCompile> {
options.generatedSourceOutputDirectory = file(querydslDir)
// 위의 설정이 안되면 아래 설정 사용
// options.generatedSourceOutputDirectory.set(file(querydslDir))
}
tasks.named("clean") {
doLast {
file(querydslDir).deleteRecursively()
}
}
Gradle 동작 원리
의존성 설정
- Querydsl JPA Support 의존성을 추가해준다.
- maven repository: https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa
Annotation Processor
- 먼저, Annotation Processor는 컴파일 단계에서 Annotation에 정의된 일렬의 프로세스를 동작하게 하는 것이다. 컴파일 단계에서 실행되기 때문에 빌드 단계에서 에러를 출력하게 하거나 소스코드 및 바이트 코드를 생성할 수 있다.
- querydsl-apt는 소스 코드 레벨에서 Querydsl 관련 어노테이션들을 처리하고 쿼리 타입 클래스들을 생성해주는 역할을 한다. 이를 통해 쿼리를 직접 작성하는 대신 Java 코드를 활용하여 컴파일러가 검사하는 안전한 방법으로 쿼리를 작성할 수 있다. sourceSets에 QClass를 저장할 경로를 설정해주면 해당 경로에 쿼리 타입 클래스를 생성해준다.
- annotation-api를 추가하지 않으면 아래와 같은 에러가 발생한다. 해당 api에는 @Generated, @Deprecated, @SuppressWarnings, @Override와 같은 자바 표준 어노테이션이 해당 API에 포함되어 있다.
Unable to load class 'jakarta.annotation.Generated'
- persistence-api를 추가하지 않으면 아래와 같은 에러가 발생한다. persistence-api는 @Entity, @Id, @GeneratedValue, @Column 등의 어노테이션을 지원한다.
Unable to load class 'jakarta.persistence.Entity'.
빌드옵션
querydslDir
- querydslDir은 변수다. QClass를 저장하길 원하는 경로를 설정해줬다. 대체로 "src/main/generated" 경로로 설정한다.
sourceSets
- Gradle 빌드 스크립트에서 정의한 소스코드와 리소스 디렉토리를 구성하고 관리할 때 사용한다.
- java source set에 querydsl QClass 위치를 추가했다.
JavaCompile 명령어
- JavaCompile 작업이 일어날 때, querydsl QClass 파일 생성 위치를 지정할 때 사용한다.
- Groovy => tasks.withType(JavaCompile) {} 로 추가
- Kotlin => tasks.withType<JavaCompile> {} 로 추가
Clean 명령어
- gradle clean 명령어를 실행할 때, 동작시킬 작업을 추가할 수 있다.
- clean 명령어가 실행되면 QClass Directory 삭제하도록 설정했다.
실행 방법
Qclass 생성 및 삭제
생성
- Gradle - Task - other - compileJava를 실행하면 된다.
- 아래와 같이 src/main/generated 경로에 QClass가 생성된다.
삭제
- Gradle - Task - build - clean 을 실행하면 된다.
- src/main/generated 디렉토리와 파일이 전부 삭제된다.
주의사항
- src/main/generated 디렉토리는 개발할 때 편의를 위해 사용하는 것이다. 실제 배포를 위해 build를 명령어를 실행할 때, 코드 실행을 위해 필요한 QClass들이 build 디렉토리의 entity와 같은 경로에 전부 포함되기 때문에 Docker와 같은 컨테이너에 generated 디렉토리를 COPY해서 넣을 이유가 없다.
- Git과 같은 버전관리시스템을 사용하고 있다면, generated를 굳이 저장할 필요가 없기 때문에 ignore 파일에 src/main/generated 디렉토리를 추가하도록 하자.
참고자료
- QueryDSL: https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81
- sourceSets: https://kkang-joo.tistory.com/3
- AnnotationProcessor와 QueryDSL: http://honeymon.io/tech/2020/07/09/gradle-annotation-processor-with-querydsl.html