Query DSL 5.0.0 적용하기

1. 개요

DB연동 시 native query를 사용하지 않는다면 JPA를 많이 사용할텐데, 사실 JPA만으로는 복잡한 SQL을 작성하기가 어려울 때가 많다.

결국 jpql과 natvie query를 사용해 쿼리를 작성하다보면, 이럴거면 차라리 mybatis를 쓰지(…)하는 생각이 들 때가 있다. 이럴 때 타입세이프한 SQL을 사용할 수 있는 프레임워크가 몇가지 있는데, 가장 인기있는 게 Query dsl과 jooq인 듯 하다.


1.1. Query dsl과 Jooq

그런데 jooq은 오라클 연동 시 상용 버전을 사용해야 한다. mysql이라던지 h2같은 경우엔 무료로 사용 가능하나, 난 그렇지 않으므로 Querydsl을 사용하고 있다. Query dsl은 일반적인 RDBMS외에도 Mongo db나, 엘라스틱 서치에 사용되는 lucene과도 사용할 수 있는데, 일반적으로 spring data jpa와 많이들 사용하는 듯 하다.


1.2. Query dsl과 Spring data jdbc

Spring data jdbc공식문서에는 query dsl연동이 되는 것처럼 설명되어 있으나, 실제로 적용해보면 에러가 떨어진다…(삽질한 자의 분노)

이 중 query dsl 5.0.0은 2021.7.22에 정식 릴리즈되었다. 이번에 적용할 기회가 생겨 해당 내용을 정리해 포스팅한다.


2. build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.querydsl:querydsl-apt:5.0.0'
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    implementation 'com.querydsl:querydsl-core:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:general'
}

Q클래스 생성을 위해 별도의 플러그인을 사용했던 예전 버전과는 달리, 최근엔 어노테이션 프로세서를 활용해 Q클래스를 생성한다. 따라서 별도의 플러그인을 적용할 필요없이, 간단하게 querydsl-aptannotationProcessor로 달아주면 된다.

querydsl-jpa만 디펜던시를 등록해도 querydsl-core를 알아서 들고오긴 하지만, 2021.08.22 현재 core 라이브러리를 4.4.0으로 들고오는 버그가 있다. 따라서 해당 버전은 5.0.0으로 디펜던시를 등록해주었다.


3. Query dsl사용하기

3.1. 엔티티 등록

@Entity
@QueryEntity
public class Person {
  private @Id String id;

  private String firstName, lastName;
}

엔티티는 Spring data jpa를 사용하던 기존과 동일하게 엔티티를 만들되, @QueryEntity 어노테이션을 달아준다.

그리고 컴파일을 하게 되면, gradle javaCompile시 마다 자동으로 해당 엔티티를 읽어 Q클래스를 생성한다. 생성된 Q클래스는 build\generated\sources\annotationProcessor 하위에 위치한다.

빌드 경로를 임의로 변경했다면 상기 위치와 다를 수 있다.


3.2. 레파지토리 등록

3.2.1. QuerydslPredicateExecutor적용
@Repository
public interface PersonRepository extends PagingAndSortingRepository<Person, String>,
    QuerydslPredicateExecutor<Person> { }

QueryEntity를 조회할 레파지토리가 QuerydslPredicateExecutor를 상속받도록 한다. 해당 인터페이스만 상속받더라도 간단하게 query dsl의 기본 메소드를 사용할 수 있다. EX. findAll(Predicate predicate, Pageable pageable)


3.2.2. QuerydslBinderCustomizer추가
@Repository
public interface PersonRepository extends PagingAndSortingRepository<Person, String>,
    QuerydslPredicateExecutor<Person>,
    QuerydslBinderCustomizer<QPerson> {

  @Override
  default void customize(QuerydslBindings bindings, QPerson person) {
    bindings.bind(person.firstName).first(StringExpression::containsIgnoreCase);
    bindings.bind(person.lastName).first(StringExpression::containsIgnoreCase);
  }
}

QuerydslPredicateExecutor만 적용해도 query dsl을 사용하는 데는 문제가 없다. 그러나 api요청 시 request parameter로 predicate를 받고, 기본 바인딩 규칙을 사용하고 싶지 않다면 별도의 커스텀이 필요하다.

기본 바인딩 규칙은 공식문서를 참조한다.


먼저 QuerydslBinderCustomizer를 상속한다. Q클래스가 생성된 상태여야 함에 유의하자. 그리고 customize메소드를 구현하게되면, api요청 시 자동적으로 생성되는 predicate 바인딩방식을 커스텀할 수 있다.

기본적으로 String타입인 firstName과 lastName은 eq로 바인딩되는데, 상기와 같이 선언했다면 이 두 조건은 containsIgnoreCase, 즉 대소문자 구분없이 포함하는지 체크하도록 바인딩된다. request parameter에 해당 파라미터가 있는 경우에만 바인딩되어 편리하다.


3.3. Api생성

@GetMapping("/person")
public ResponseEntity<Page<QPerson>> person(
  @QuerydslPredicate(root = QPerson.class) Predicate predicate, Pageable pageable) {
  return ResponseEntity.ok(service.person(predicate, pageable));
}

생성할 api는 Person entity를 루트로 한 predicate를 받는다. 그리고 하기와 같이 요청하게 되면, where firstName like ‘%hk%’로 자동 매핑된다.

curl -X GET "http://localhost:8080/api/person?firstName=hk" -H "accept: */*"


4. 결론

그간 한국에서는 Java+Spring+Mybatis조합이 인기가 많았다. 그래서 sql기반 개발에 익숙한 시니어 개발자분들은 orm에 부정적인 분들이 많다.

그래도 기존 SQL기반 개발의 단점을 생각해보면 점점 ORM사용 비중이 늘어나지 않을까 싶다. 다른 괜찮은 프레임워크를 발견하기 전까지는 JPA+Query dsl조합을 많이 사용하게될 것 같다.