QueryDSL 사용


QueryDsl 이란?

EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다.
이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있다.
둘은 가장 단순한 검색 방법이다.


현실적으로는 위 기능만으로 개발하기 어렵기 때문에 SQL로 필요한 내용을 최대한 걸러서 조회해야 하는데
ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로
검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다.
JPQL이 이런 문제를 해결하기 위해 만들어졌는데 다음과 같은 특징이 있다.


SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리다.
JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다.
그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.


JPA가 공식 지원하는 기능은 다음과 같다.


QueryDSL은 JPQL의 빌더(Criteria) 클래스이다.
코드 기반이며 단순하고 사용하기 쉽다는 장점이 있다.

public void queryDSL() {         
    EntityManager em = emf.createEntityManager();          
    
    JPAQuery query = new JPAQuery(em);
    QMember qMember = new QMember("m"); // JPQL 병칭 = m         
    List<Member> members = 
        query.from(qMember)
            .where(qMember.name.eq("회원"))
            .orderBy(qMember.name.desc())
            .list(qMember);
}          

QueryDSL을 사용하려면 우선 com.mysema.query.jpa.impl.JPAQuery 객체를 생성해야 한다.
이때 엔티티 매니저를 생성자에 넘겨준다.
그리고 사용할 쿼리 타입(Q)을 생성하는데 생성자에는 별칭을 주면 된다.
이 별칭은 JPQL에서 별칭으로 사용한다.




QueryDsl 사용하기

Spring Boot 2.3부터 Gradle 6.3 이상을 요구한다.
예전에 작성한 코드를 가지고 Spring Boot와 Gradle을 버전업했다.
인텔리제이에서 Querydsl을 이용해서 생성하는 Q클래스를 불러올 수 없다는 경고가 노출된다.

gradle은 프로젝트의 의존성을 관리하고 작성된 코드를 배포 가능한 형태로 가공하는 개발도구다.
그리고 그 버전이 매우 빠른 속도로 업데이트 된다. 기능의 변동도 많다.
때문에 기존에 작성한 스크립트가 쓸모가 없어지거나 동작하지 않는 등의 상황이 발생한다.
gradle 플러그인(https://plugins.gradle.org/)을 사용하다보면 그런 상황을 많이 마주하게 된다.

그 중에 JPA 엔티티 클래스(javax.persistence.Entity 선언클래스)를 가공하여
Query와 유사한 작성법으로 사용할 수 있는 Q클래스를 생성하는 Queyrdsl JPA 플러그인이 대표적인 경우다.


Querydsl JPA는 프로젝트 내에서 @Entity 어노테이션을 선언한 클래스를 탐색하고
JPAAnnotationProcessor를 이용하여 Q클래스를 생성한다.
생성된 Q클래스는 자바 언어가 가지는 정적코드의 장점을 활용하여 안전한 쿼리문을 작성할 수 있다.

Annotation processor가 등장하기 이전 그레이들 버전(4.6 이전)에서는
JPAAnnotationProcessor의 작동을 정의하는 스크립트를 정의하는 것이 쉽지 않았다.


gradle에서 qureydsl를 사용하는 방법은 두가지다.




플러그인 com.ewerk.gradle.plugins.querydsl 사용

apply plugin: "com.ewerk.gradle.plugins.querydsl"

def queryDslDir = "src/main/generated"
querydsl {
    library = "com.querydsl:querydsl-apt:4.2.2" // 사용할 AnnotationProcesoor 정의
    jpa = true
    querydslSourcesDir = queryDslDir
}
sourceSets {
    main {
        java {
            srcDir queryDslDir
        }
    }
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

configurations {
     // 아래를 지정하지 않으면, compile로 걸린 JPA 의존성에 접근하지 못한다.
    querydsl.extendsFrom compileClasspath
}

이 gradle 플러그인은 2018.7 에 출시된 1.0.10 를 마지막으로 더이상 업데이트가 없다.
gradle 4.6 에서 Annotation Processor 가 소개되고,
이를 반영한 gradle 5.X 가 출시됐을 때는 정상적으로 작동하지 않았다.
그래서 위 스크립트 부분 중 다음 부분이 추가되었다.

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}


querydsl-apt 에 있는 AnnotationProcessor 의 경로를 설정해준다.
그리고 gradle 6.x 에서는 다음 코드를 추가해주면 정상작동한다고 한다.

configurations {
     // 아래를 지정하지 않으면, compile로 걸린 JPA 의존성에 접근하지 못한다.
    querydsl.extendsFrom compileClasspath
}

최신버전에서 정상 동작시키려고 뭔가 하나씩 설정을 추가하는 상황이 발생한다.
또한 gradle 플러그인은 IntelliJ IDEA 연동에 문제 일으키기 때문에
Annotation Processor을 권장한다.




Cannot find symbol 오류

QueryDSL과 Hibernate/Eclipse Metamodel Generator를 함께 사용할 때
아직 생성되지 않은 메타 모델 클래스를 사용하는 코드들 때문에 cannot find symbol 에러가 발생할 수 있는데,
이는 이 둘을 서로 따로 생성했을 때 발생하는 현상이다.

Lombok을 함께 사용할 경우 각 AP가 실행된 뒤에 다시 lombok AP가 돌면서 발생하는 것으로 알고 있는데
이 둘을 함께 지정해서 APT를 수행해야 에러가 나지 않는다.




Annotation Processor 설정 사용

gradle 4.6(https://docs.gradle.org/4.6/release-notes.html)에서 소개된
Annotation processor는 어노테이션이 선언된 클래스처리를 별도의 프로세서에서 처리하여 성능향상을 꽤했다.

def querydslVersion = '4.3.1'

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // Querydsl
    implementation group: 'com.querydsl', name: 'querydsl-jpa', version: querydslVersion
    implementation group: 'com.querydsl', name: 'querydsl-apt', version: querydslVersion
    implementation group: 'com.querydsl', name: 'querydsl-core', version: querydslVersion

    annotationProcessor group: 'com.querydsl', name: 'querydsl-apt', version: querydslVersion
    annotationProcessor group: 'com.querydsl', name: 'querydsl-apt', version: querydslVersion, classifier: 'jpa'
    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
    annotationProcessor("jakarta.annotation:jakarta.annotation-api")

    runtimeOnly 'com.h2database:h2'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.junit.jupiter:junit-jupiter:5.6.0'
    testImplementation 'org.assertj:assertj-core:3.15.0'
}

def generated='src/main/generated'
sourceSets {
    main.java.srcDirs += [ generated ]
}

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

clean.doLast {
    file(generated).deleteDir()
}




참고