[Spring Study] QueryDSL 세팅하기 JOOQ와 비교

안녕하세요 오늘은 프로젝트에 QueryDSL을 세팅하는 방법에 대해서 기록하려고 합니다. 그 전에 QueryDSL 이 무엇인지 그리고 비슷한 라이브러리인 JOOQ와 차이점에 대해서 정리해보려고 합니다.

QueryDSL

QueryDSL은 자바 기반의 타입 안전한 쿼리 빌더 라이브러리로, SQL, JPA, MongoDB 등의 다양한 데이터베이스 쿼리를 작성할 수 있도록 도와줍니다. 장점으로는 쿼리를 문자열로 작성하지 않고 자바 코드로 작성할 수 있도록 하기 때문에 컴파일 타임에 오류를 잡아낼 수 있고IDE의 자동 완성 기능을 사용할 수 있습니다.

QueryDSL 주요 특징

  • 타입 안전성: 컴파일 타임에 쿼리 오류를 잡아낼 수 있습니다.
  • 코드 자동 완성: IDE에서 쿼리 작성 시 자동 완성 기능을 사용할 수 있습니다.
  • 통합성: JPA, SQL, MongoDB 등 다양한 데이터베이스와 통합할 수 있습니다.
  • 간결함: 복잡한 쿼리를 간결하고 가독성 있게 작성할 수 있습니다.

JOOQ

JOOQ( Java Object Oriented Querying) 또한 SQL을 자바 코드로 작성할 수 있게 도와주는 라이브러리입니다. 자바 코드로 SQL 쿼리를 작성하고, 데이터베이스 메타데이터를 기반으로 자동 생성된 클래스를 통해 타입 안전성을 제공합니다.

JOOQ 주요 특징

  • 타입 안전성: 데이터베이스 스키마 기반으로 생성된 클래스를 통해 타입 안전성을 제공합니다.
  • 자동 코드 생성: 데이터베이스 스키마를 기반으로 쿼리 빌더 클래스를 자동으로 생성합니다.
  • 직접 SQL 사용: SQL을 직접 자바 코드로 작성할 수 있어, 복잡한 쿼리를 자유롭게 작성할 수 있습니다.
  • 범용성: 다양한 데이터베이스를 지원하며, SQL 표준을 따릅니다.

QueryDSL vs JOOQ

장점

  • QueryDSL
    • 타입 안전성: JPA와 같은 ORM과 잘 통합되며, 컴파일 타임에 쿼리 오류를 잡을 수 있습니다.
    • 유연성: JPA, SQL, MongoDB 등 다양한 데이터베이스와 쉽게 통합할 수 있습니다.
    • 가독성: 쿼리를 객체지향적으로 작성할 수 있어 코드 가독성이 높습니다.
  • JOOQ
    • SQL 중심: SQL을 직접적으로 자바 코드로 작성할 수 있어, SQL을 잘 이해하는 개발자에게 유리합니다.
    • 자동 코드 생성: 데이터베이스 스키마를 기반으로 자동 생성된 클래스를 통해 쿼리를 작성할 수 있습니다.
    • 강력한 기능: 복잡한 SQL 쿼리와 연산을 자바 코드로 쉽게 작성할 수 있습니다.

단점

  • QueryDSL
    • 초기 설정: 설정과 사용법을 익히는 데 다소 시간이 걸릴 수 있습니다.
    • 복잡한 쿼리: 매우 복잡한 SQL 쿼리의 경우 가독성이 떨어질 수 있습니다.
  • JOOQ
    • 러닝 커브: SQL에 대한 이해도가 부족한 경우 사용이 어려울 수 있습니다.
    • 초기 설정: 데이터베이스 스키마를 기반으로 코드를 생성하는 과정이 필요합니다.
    • ORM 통합: JPA와 같은 ORM과의 통합이 다소 어렵습니다.

차이점

  • 사용 방식
    • QueryDSL: 객체지향적인 쿼리 작성 방식을 선호하는 경우 유리합니다.
    • JOOQ: SQL을 직접적으로 작성하고자 하는 경우 유리합니다.
  • 타입 안전성 제공 방식
    • QueryDSL: 쿼리를 객체로 작성하여 타입 안전성을 제공합니다.
    • JOOQ: 데이터베이스 메타데이터를 기반으로 생성된 클래스를 통해 타입 안전성을 제공합니다.
  • 데이터베이스 지원
    • QueryDSL: JPA, SQL, MongoDB 등 다양한 데이터베이스를 지원합니다.
    • JOOQ: 주로 SQL 데이터베이스를 지원하며, SQL 표준을 따릅니다.

프로젝트의 요구사항에 따라 적합한 라이브러리를 선택하는 것이 중요하겠지만, 국내 기업 특성 상 QueryDSL 사용률이 높기 때문에 이번 실습 프로젝트에서 QueryDSL 을 사용하기로 했습니다.

QueryDSL 설정하기 (Spring Boot 3.x 버전)

SpringBoot 버전 2.6 이상, 3.x 버전 기준으로 설정 방법에 대해서 정리해보도록 하겠습니다.

Gradle 설정

build.gradle 파일에 QueryDSL 의존성 및 스크립트를 추가합니다.

dependencies 부분 입니다.

이제 사용방법에 대해서 소개해보도록 하겠습니다.

엔티티 클래스 작성

QueryDSL이 작동하려면 JPA 엔티티가 필요하기 때문에 먼저 엔티티 클래스를 작성해주셔야 합니다. 이미 엔티티가 존재하는 경우 생략해도 좋습니다. 저는 Todo 라는 클래스에 엔티티를 생성했습니다.

Q 클래스 생성

Maven 또는 Gradle을 사용하여 프로젝트를 빌드하면, QueryDSL은 Q 클래스(QUser 등)를 자동으로 생성합니다. 이 클래스들은 QueryDSL 쿼리 작성에 사용됩니다. Q클래스는 QueryDSL이 자동으로 생성하는 메타모델 클래스 입니다. 이렇게 생성된 Q 클래스는 JPA 엔티티와 동일한 속성을 가지게 됩니다. Q클래스를 사용하면 쿼리를 자바 코드로 작성할 때 컴파일 타임에 오류를 잡아낼 수 있습니다. 저 같은 경우 빌드 이후 build 폴더에 QTodo 라는 클래스가 자동으로 생성된 것을 확인할 수 있었습니다.

레포지토리 인터페이스 작성

이제 Spring Data JPA의 JpaRepository를 상속받는 인터페이스를 작성합니다.

Spring Data JPA의 JpaRepository를 상속받는 과정은 기본적인 CRUD(Create, Read, Update, Delete) 작업을 쉽게 수행할 수 있도록 해주는 인터페이스를 사용함으로써 데이터베이스 접근을 단순화하는 것입니다. 해당 클래스를 상속받음으로써 별도의 구현 없이 데이터베이스 작업을 수행할 수 있게됩니다.

JpaRepository 주요 메서드

JpaRepository 인터페이스는 여러 유용한 메서드를 제공합니다. 몇 가지 주요 메서드는 다음과 같습니다.

  • save(S entity): 엔티티를 저장합니다. 엔티티가 존재하지 않으면 새로 생성하고, 존재하면 업데이트합니다.
  • findById(ID id): ID를 기준으로 엔티티를 조회합니다.
  • findAll(): 모든 엔티티를 조회합니다.
  • deleteById(ID id): ID를 기준으로 엔티티를 삭제합니다.
  • count(): 총 엔티티 수를 반환합니다.

사용자 정의 메서드 추가

기본 CRUD 메서드 외에도, 리포지토리 인터페이스에 사용자 정의 쿼리 메서드를 추가할 수 있습니다. Spring Data JPA는 메서드 이름을 기반으로 쿼리를 생성합니다.

package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
    List<User> findByEmail(String email);
}

서비스에서 QueryDSL 사용하기

저는 QueryDSL 을 사용해서 페이징을 구현해보았습니다. 코드는 아래와 같습니다.

우선 QuerydslRepositorySupport를 상속받는 TodoSearchImpl 커스텀 리포지토리 구현 클래스를 선언했습니다.

QuerydslRepositorySupport는 Spring Data JPA에서 QueryDSL을 사용하여 복잡한 쿼리를 작성하고 실행하는 것을 도와주는 추상 클래스입니다. 이 클래스는 QueryDSL과의 통합을 쉽게 하고, 엔티티와의 쿼리 작업을 위한 여러 유틸리티 메서드를 제공합니다.

유틸리티 메서드에 대해서 간략하게 설명하자면 아래와 같은 메서드들이 있습니다.

  • from: 지정된 엔티티에 대한 쿼리 객체를 생성합니다.
  • getQuerydsl: QueryDSL의 JPAQueryFactory 객체를 반환하여 쿼리 빌더 메서드를 사용할 수 있게 합니다.
  • applyPagination: 페이징 처리를 쿼리에 적용합니다.
  • getEntityManager: EntityManager 객체를 반환하여 JPA 관련 작업을 수행할 수 있습니다.

그 다음 QueryDSL을 사용하기 위해 QTodo 인스턴스를 생성했습니다. 그 다음 QuerydslRepositorySupport 클래스의 from 메서드를 사용하여 JPQLQuery 객체를 생성합니다. where 메서드를 사용하여 조건을 설정하는 것도 가능합니다. 여기서는 title 속성에 “1”이 포함된 Todo 엔티티를 조회해보도록 했습니다.

본격적으로 페이지네이션 및 정렬을 설정하는 코드에 대해서 살펴보겠습니다.

PageRequest pageable = PageRequest.of(1, 10, Sort.by("id").descending());
this.getQuerydsl().applyPagination(pageable, query);

1페이지부터 시작하여 페이지당 10개의 결과를 반환하며, id 속성을 기준으로 내림차순으로 정렬하도록 설정했습니다. 그 다음 applyPagination 메서드를 사용하여 페이지네이션과 정렬을 쿼리에 적용하였습니다.

다음으로는 쿼리 실행 관련 메서드들입니다. fetch 메서드와 fetchCount 메서드의 차이점을 확인하기 위해 두가지 메서드 모두 호출해보았습니다.

query.fetch();
query.fetchCount();

fetch() 메서드는 쿼리를 실행하여 결과를 가져옵니다. 그 후 조회된 엔티티 리스트를 반환합니다.

fetchCount() 메서드는 쿼리를 실행하여 결과의 개수를 가져옵니다. 예를들어 페이지네이션 시 전체 결과의 개수를 알기 위해 사용됩니다.