catsridingCATSRIDING|OCEANWAVES
Dev

Querydsl 동적 쿼리 작성하기

jynn@catsriding.com
Dec 18, 2023
Published byJynn
999
Querydsl 동적 쿼리 작성하기

Write Dynamic Query with Querydsl

애플리케이션 개발 과정에선 다양한 요구사항을 충족시키기 위해서는 동적 쿼리(Dynamic Query) 생성이 필수적입니다. 하지만, JPA가 제공하는 기능만으로는 다소 복잡한 쿼리를 조작하는 데 한계가 존재합니다. 이러한 문제를 해결하고자 Querydsl 라이브러리를 도입하게 되면, Java 코드 안에서 더욱 간결하고 명료한 동적 쿼리를 작성하는 것이 가능해집니다. Querydsl의 이런 강력한 추상화 과정은 데이터베이스 쿼리 작성의 복잡성을 크게 줄이며, 애플리케이션의 효율성과 유지보수성을 크게 향상시킬 수 있습니다.

Understanding Dynamic Queries

동적 쿼리는 사용자의 요구 사항에 따라 SQL 쿼리의 구조가 변경되어야 할 때 필요합니다. 예를 들어, 사용자가 웹 인터페이스를 통해 다양한 검색 조건을 선택할 수 있고, 이러한 조건들을 기반으로 데이터베이스 쿼리를 구성해야 하는 경우가 이에 해당합니다.

Limitations of JPQL

JPQL(Java Persistence Query Language)을 사용하면 일정 수준의 동적 쿼리를 구현할 수 있지만, 복잡성이 증가할수록 그 한계를 드러내게 됩니다. 예를 들어, 사용자가 선택한 조건에 따라 여러 필드를 기준으로 검색을 수행해야 하는 경우, JPQL만으로는 이를 깔끔하게 처리하기 어려울 수 있습니다.

다음은 JPQL을 사용하여 동적 쿼리를 구현하려 할 때 발생할 수 있는 문제를 보여주는 예제입니다:

JpqlRepository.java
String query = "SELECT u FROM User u WHERE u.status = :status";

if (username != null) {
    query += " AND u.username = :username";
}

if (email != null) {
    query += " AND u.email = :email";
}

Query q = entityManager.createQuery(query);
q.setParameter("status", status);

if (username != null) {
    q.setParameter("username", username);
}

if (email != null) {
    q.setParameter("email", email);
}

List<User> users = q.getResultList();

위 코드에서 볼 수 있듯이, 각 조건의 존재 여부에 따라 쿼리 문자열을 수동으로 조합해야 하고, 매개변수를 설정하는 과정 또한 동적으로 처리해야 합니다. 이 방식은 쿼리가 길어지고 복잡해질수록 오류 발생 가능성을 높이고 코드의 가독성을 저하시킵니다.

Approach of Querydsl

Querydsl은 타입 안전성을 제공하며 쿼리 구성을 돕는 유연한 API를 통해 이러한 문제를 해결합니다. 이는 JPQL을 사용하여 JPA 쿼리를 작성할 때 발생할 수 있는 실수를 최소화하고, 쿼리 실행 전에 문제를 발견할 수 있도록 컴파일 타임 검증을 지원합니다.

이를 사용하면, 위의 JPQL 예제를 더 간결하고 명확한 Java 스타일로 표현할 수 있습니다:

QuerydslRepository.java
public List<User> fetchUsersBy(String username, String email) {
    return jpaQueryFactory
            .select(user)
            .from(user)
            .where(
                    dynamicEqualUsername(usernmae),
                    dynamicEqualEmail(email))
            .fetch();
}

private BooleanExpression dynamicEqualsUsername(String username) {
    return username == null ? null : user.username.eq(username);
}

private BooleanExpression dynamicEqualsEmail(String email) {
    return email == null ? null : user.email.eq(email);
}

위 코드는 Querydsl의 BooleanExpression을 사용하여 조건을 동적으로 구성하고 있습니다. 각 조건은 메소드 체인을 통해 추가되며, 타입 안전성이 보장됩니다. 이로써 개발자는 복잡한 동적 쿼리를 더욱 안정적으로 구성할 수 있으며, 런타임 오류의 위험을 줄일 수 있습니다.

Querydsl의 이러한 접근 방식은 데이터베이스 쿼리를 작성하는 작업을 단순화하고 코드의 유지보수를 용이하게 합니다. 따라서 복잡한 비즈니스 로직이 요구되는 동적 쿼리를 구현해야 하는 상황에서 매우 유용하게 활용될 수 있습니다.

Dynamic Query Mechanism

Querydsl의 가장 강력한 기능 중 하나는 바로 동적 쿼리 생성 메커니즘입니다. 이 기능은 개발자가 유연하고 확장 가능한 데이터 접근을 경험할 수 있게 하며, 전반적인 성능 향상을 이끌어낼 수 있는 잠재력을 제공합니다. 이 메커니즘은 BooleanExpression과 Querydsl 엔진의 null 처리 방법, 두 가지 주요 요소를 활용합니다.

BooleanExpression은 Querydsl에서 논리적 조건을 나타내는 인터페이스입니다. 이 인터페이스의 유연성은 Querydsl 쿼리 생성 엔진이 null 값을 처리하는 방식과 밀접하게 연결되어 있습니다. 만약 BooleanExpressionnull을 반환하거나 사용자 입력이 null인 경우, 쿼리 엔진은 해당 조건을 무시합니다. 이는 쿼리에서 해당 조건을 완전히 제외시켜, 필요하지 않은 조건에 의한 처리 부담을 줄여줍니다. 이런 처리 방식 덕분에 Querydsl은 유연하고 효과적인 SQL 쿼리 구성을 가능하게 합니다.

이러한 메커니즘을 활용하여 다음과 같은 동적 쿼리 작성이 가능합니다:

QuerydslRepository.java
public void fetchUsersBy(Integer age) {
    List<User> users = jpaQueryFactory
            .select(user)
            .from(user)
            .where(dynamicGreaterThanAge(age))
            .orderBy(user.name.asc())
            .fetch();
}

private BooleanExpression dynamicGreaterThanAge(Integer age) {
    return age == null ? null : user.age.gt(age);
}

매개변수 agenull이 아닌 경우, dynamicGreaterThanAge() 함수는 user.age.gt(age) 조건을 반환합니다. 따라서 해당 나이 조건이 SQL 쿼리에 포함됩니다:

console
select
    *
from
    USERS users
where
    users.age > 18;
order by
    users.name asc

반면, agenull인 경우, dynamicGreaterThanAge() 함수는 null을 반환하므로, 나이에 관한 조건은 SQL 쿼리에서 배제됩니다:

console
select
    *
from
    USERS users
order by
    users.name asc

Core Components for Dynamic Queries

Querydsl은 쿼리 작성을 위한 여러 핵심 컴포넌트를 제공합니다. 이 컴포넌트들은 쿼리 구성을 유연하고 효율적으로 만들어 주어, 복잡한 데이터베이스 작업을 간소화하며, 쿼리 로직을 재사용 가능하고 유지보수하기 쉬운 코드로 변환하는 데 중요한 역할을 합니다.

JPAQueryFactory

JPAQueryFactory는 쿼리 객체를 생성하는 팩토리 클래스로, EntityManager와 연동하여 데이터베이스 쿼리를 실행합니다.

실무 환경에서는 JPAQueryFactory를 프로젝트의 모든 리포지토리에서 쉽게 접근하고 사용할 수 있도록 다음과 같이 Spring Bean으로 등록하는 것이 일반적입니다:

QuerydslConfig.java
@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

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

이렇게 등록된 JPAQueryFactory의 Spring Bean은 리포지토리 구현체에서 주입받아 사용됩니다:

QuerydslRepository.java
@RequiredArgsConstructor
public class QuerydslRepository {

    private final JPAQueryFactory queryFactory;

    public User fetchUser(UserId userId) {
        return queryFactory
                .select(user)
                .from(user)
                .where(user.id.eq(userId.getId()))
                .fetchOne();
    }
}

JPAQueryFactory를 사용하면 SQL 쿼리를 Java 언어로 작성할 수 있으므로 타입 세이프(Type-Safe)하게 동작합니다. 이는 SQL 쿼리의 실행 시점에 타입 오류를 방지하고, IDE의 자동완성, 리펙토링, 디버깅 등의 도구와 기능을 지원을 받을 수 있게 합니다.

또한, SQL 쿼리를 문자열로 직접 작성하는 대신 Java의 변수와 메서드를 사용하여 쿼리를 구성하면, 쿼리를 마치 일반적인 프로그래밍 로직처럼 다룰 수 있습니다. 쿼리를 구성하는 로직을 재사용하면 코드 중복을 줄일 수 있고, 쿼리의 변경이 필요한 경우 관련된 모든 코드를 검사하고 수정할 필요 없이 변수 또는 메서드 정의만 변경하면 됩니다. 이를 통해 유지 보수성이 크게 향상됩니다.

BooleanExpression

BooleanExpression은 Querydsl에서 조건을 생성하고 관리하기 위한 핵심 컴포넌트입니다. 이 객체는 SQL 쿼리의 여러 부분인 WHERE, JOIN, HAVING 절 등에서 활용됩니다. 이를 통해 개발자는 여러 조건을 논리적으로 접근하고 복잡한 조건식을 구성할 수 있습니다.

다양한 타입의 데이터와 메서드에 대해 타입 안전성을 제공하는 BooleanExpression은 개발자가 어떤 타입의 데이터를 사용하고 있는지 명확하게 인지할 수 있도록 도와줍니다. 또한 메서드 체이닝(Method Chaining) 방식을 활용해 순차적이고 직관적인 방식으로 데이터에 접근하고 조작하는 코드를 작성할 수 있게 합니다. 이는 훨씬 보다 쉽게 코드를 작성하고 이해하며, 유지 관리를 더욱 용이하게 만들어줍니다.

BooleanExpression은 SQL의 다양한 조건 연산자에 대응하는 다양한 메서드를 제공하며, 이를 통해 어떤 조건도 유연하게 표현할 수 있습니다:

  • eq(T value): 값의 일치여부를 확인하며, 두 값이 같으면 참인 표현식을 반환합니다.
  • ne(T value): 두 값이 다를 경우 참이 되는 표현식을 반환합니다.
  • gt(T value): 값이 주어진 값보다 클 경우 참인 표현식을 반환합니다.
  • goe(T value): 값이 주어진 값보다 크거나 같을 경우 참인 표현식을 반환합니다.
  • lt(T value): 값이 주어진 값보다 작을 경우 참인 표현식을 반환합니다.
  • loe(T value): 값이 주어진 값보다 작거나 같을 경우 참인 표현식을 반환합니다.
  • between(T1 value1, T2 value2): 값이 두 주어진 값 사이에 있으면 참인 표현식을 반환합니다.
  • isNull(): 값이 null일 경우 참인 표현식을 반환합니다.
  • isNotNull(): 값이 null이 아닐 경우 참인 표현식을 반환합니다.
  • in(Collection<T> values): 주어진 컬렉션에 값이 포함되어 있으면 참인 표현식을 반환합니다.
  • notIn(Collection<T> values): 컬렉션에 값이 포함되어 있지 않으면 참인 표현식을 반환합니다.
  • like(): 값이 주어진 패턴과 일치하면 참인 표현식을 반환합니다.
  • notLike(): 값이 주어진 패턴과 일치하지 않으면 참인 표현식을 반환합니다.
  • contains(): 값에 주어진 문자열이 포함되어 있으면 참인 표현식을 반환합니다.

또한, 여러 조건을 논리적으로 결합할 수 있도록 and(), or(), not() 메서드도 제공하고 있습니다. 각 메서드는 새로운 BooleanExpression을 반환하므로, 메서드 체이닝 방식으로 조건을 연쇄적으로 결합할 수 있습니다.

  • and(): 이 메서드는 주어진 표현식과 현재 표현식을 논리적 AND 연산으로 결합한 새로운 BooleanExpression을 반환합니다. 이는 두 조건이 모두 참일 경우에만 참을 결과로 반환하는 논리 연산입니다.
QuerydslRepository.java
List<User> users = queryFactory
        .select(user)
        .from(user)
        .where(user.age.gt(21).and(user.status.eq("active")))
        .fetch()
  • or(): 이 메서드는 주어진 표현식과 현재 표현식을 논리적 OR 연산으로 결합한 새로운 BooleanExpression을 반환합니다.이는 두 조건 중 하나라도 참이면 참을 결과로 반환하는 논리 연산입니다.
QuerydslRepository.java
List<User> users = queryFactory
        .select(user)
        .from(user)
        .where(user.age.lt(30).or(user.status.eq("VIP")))
        .fetch()
  • not(): 이 메서드는 현재 표현식의 논리적 부정인 새로운 BooleanExpression을 반환합니다. 이는 현재 조건이 거짓일 경우 참을 결과로 반환하는 논리 연산입니다.
QuerydslRepository.java
List<User> users = queryFactory
        .select(user)
        .from(user)
        .where(user.status.eq("inactive").not())
        .fetch()

BooleanExpression과 관련 API를 활용함으로써, 아래와 같이 복잡한 조건식을 간결하고 명확하게 작성할 수 있습니다:

QuerydslRepository.java
BooleanExpression isAdult = user.age.gt(18);
BooleanExpression hasValidEmail = user.email.contains("@");
BooleanExpression isVerified = user.isVerified.isTrue();

List<User> users = queryFactory
    .select(user)
    .from(user)
    .where(
        isAdult
        .and(hasValidEmail)
        .and(isVerified))
    .fetch();

위 코드는 세 가지 조건을 결합하여, 성인이면서 유효한 이메일을 가지고 인증된 사용자만을 조회합니다. 필요에 따라 조건을 제외하거나 다른 조건을 추가할 수 있습니다. 이와 같이 BooleanExpression을 사용하면 복잡한 쿼리 조건을 명확하고 관리하기 쉬운 방식으로 구성할 수 있습니다.

OrderSpecifier

OrderSpecifier는 Querydsl에서 정렬 순서를 지정하는 핵심 컴포넌트입니다. 이 클래스를 활용하면 개발자는 데이터를 특정 기준에 따라 동적으로 정렬할 수 있습니다.

다음은 OrderSpecifier 제공하는 함수들입니다:

  • asc(): 지정된 필드를 오름차순으로 정렬합니다.
  • desc(): 지정된 필드를 내림차순으로 정렬합니다.
  • nullsFirst(): 정렬 시 null 값을 가장 앞에 위치시킵니다.
  • nullsLast(): 정렬 시 null 값을 가장 뒤에 위치시킵니다.

이 메서드들은 OrderSpecifier 객체를 반환하고, 이 객체는 orderBy() 절에서 사용됩니다. OrderSpecifier의 사용법은 직관적이어서 데이터를 효과적으로 정렬하고 필요한 정보를 쉽게 추출할 수 있도록 도와줍니다.

예를 들어, 사용자의 이름을 오름차순으로 정렬하여 조회하는 방법은 다음과 같습니다:

QuerydslRepository.java
OrderSpecifier<String> orderByUsername = user.username.asc();

List<User> users = queryFactory
    .select(user)
    .from(user)
    .orderBy(orderByUsername)
    .fetch();

이 코드는 username 필드를 기준으로 사용자 정보를 오름차순으로 정렬하고 결과를 조회합니다. OrderSpecifier를 사용하면 정렬 조건을 간단하고 명확하게 적용할 수 있습니다.

또한, JPAQueryFactoryorderBy() 함수는 OrderSpecifier<T>를 가변 인자로 받기 때문에 여러 필드에 대한 정렬을 동시에 지정할 수 있습니다:

QuerydslRepository.java
OrderSpecifier<Integer> orderByAgeDesc = user.age.desc();
OrderSpecifier<String> orderByEmailAsc = user.email.asc();

List<User> users = queryFactory
    .select(user)
    .from(user)
    .orderBy(orderByAgeDesc, orderByEmailAsc)
    .fetch();

위 코드는 사용자를 나이가 많은 순으로 정렬하되, 동일 나이에서는 이메일 주소 순으로 정렬합니다. 이처럼 OrderSpecifier를 활용하여 다양한 정렬 조건을 적용하면 보다 정교한 데이터 조회와 분석이 가능해집니다.

OrderSpecifier를 활용하면 동적으로 정렬 조건을 적용할 수도 있습니다. 사용자 입력에 따라 정렬 필드를 결정하거나, 지정되지 않았을 경우 null을 할당하여 정렬을 생략함으로써 쿼리의 성능을 최적화합니다. 이 방법은 OrderSpecifiernull인 경우 해당 조건을 쿼리에서 무시하므로, 사용자의 요구에 따라 유연하게 정렬 조건을 조정해야 하는 상황에 특히 유용합니다.

Path and PathBuilder

PathPathBuilder는 Querydsl에서 중요한 구성 요소로, 엔티티의 특정 속성을 참조하고 조작하기 위해 사용됩니다. Path는 엔티티의 특정 속성을 나타내는 인터페이스이며, PathBuilder는 이러한 Path 객체를 동적으로 생성하는 데 사용되는 유틸리티 클래스입니다. 이들은 동적 쿼리 작성 시 타입 안전성을 유지하며, 복잡한 속성 경로를 구성하는 데 필수적입니다.

Path는 속성의 경로를 나타내고, 이 경로는 쿼리 내에서 엔티티의 특정 필드를 지정할 때 사용됩니다. 예를 들어, 사용자의 나이나 이름과 같은 특정 속성에 접근할 때 Path 객체가 사용됩니다. PathBuilder는 이러한 Path 객체를 더 쉽게 생성하고 조작할 수 있게 해주는 도구로, 특히 동적 쿼리에서 그 유용성이 두드러집니다.

다음은 Querydsl의 PathBuilder를 사용하여, 사용자의 나이 필드에 대한 조건을 추가하는 예시입니다:

QuerydslRepository.java
PathBuilder<User> userPath = new PathBuilder<>(User.class, "user");
BooleanExpression isAdult = userPath.getNumber("age", Integer.class).gt(18);

List<User> users = queryFactory
    .select(user)
    .From(user)
    .where(isAdult)
    .fetch();

이 코드는 User 클래스에 대한 PathBuilder를 선언하고, 그 안에서 나이가 18세 이상인 사용자를 필터링하는 논리 표현식을 만듭니다.

PathPathBuilder를 활용함으로써, 복잡한 쿼리 로직을 간단하고 명확하게 표현할 수 있으며, 이는 데이터 관리와 조회를 보다 효율적으로 만들어 줍니다. 이러한 기능은 특히 동적이고 복잡한 데이터 모델을 다룰 때 그 가치가 크게 나타납니다.

Writing Dynamic Queries

Querydsl을 활용하여 동적 쿼리를 작성하는 과정은 데이터베이스 작업의 유연성을 극대화하고, 복잡한 요구 사항을 보다 효과적으로 처리하는 데 중요한 역할을 합니다. 이제까지 살펴본 기본 컴포넌트들을 토대로 하여 실제로 JOIN, WHERE, ORDER BY 절을 포함한 다양한 SQL 쿼리 시나리오에서 동적 쿼리를 구성하는 방법을 자세히 살펴보겠습니다.

Using Dynamic Queries in JOIN

JOIN 연산은 여러 테이블에서 관련된 데이터를 결합하는 데 사용됩니다. Querydsl을 이용하면 ON 절에 동적 쿼리를 추가하여 다양한 테이블을 동적으로 JOIN할 수 있습니다.

다음은 POSTS 테이블과 USERS 테이블을 동적 조건에 따라 결합하는 예시입니다:

QuerydslRepository.java
@RequiredArgsConstructor
public class QuerydslRepository {

    private JPAQueryFactory queryFactory;

    public List<PostEntity> fetchBy(
            Long userId,
            Integer minAge,
            String currentRole,
            String currentTeam,
            boolean includePrivate,
            boolean includeDeleted) {
        return queryFactory
                .select(postEntity)
                .from(postEntity)
                .innerJoin(postEntity.user, userEntity)
                .on(postEntity.user.id.eq(userId),
                    dynamicIsUserMinAge(minAge),
                    dynamicIsUserRole(currentRole),
                    dynamicIsUserTeam(currentTeam))
                .fetch();
    }

    private BooleanExpression dynamicIsUserMinAge(Integer minAge) {
        return minAge != null ? user.age.goe(minAge) : null;
    }

    private BooleanExpression dynamicIsUserRole(String role) {
        return role != null ? user.role.eq(role) : null;
    }

    private BooleanExpression dynamicIsUserTeam(String team) {
        return team != null ? user.team.eq(team) : null;
    }
}

동적으로 조건을 추가하기 위해 다음과 같은 함수들을 정의하고 활용하였습니다:

  • dynamicIsUserMinAge(Integer minAge):
    • 사용자의 최소 나이를 파라미터 minAge를 기준으로 검사합니다.
    • minAgenull이 아니라면, 해당 나이 이상인 사용자만을 선택하는 조건을 추가합니다.
    • minAge 값이 null이면 이 조건은 무시되어, 최소 나이 제한이 없는 쿼리를 생성합니다.
  • dynamicIsUserRole(String role):
    • 사용자의 역할을 파라미터 role을 기준으로 검사합니다.
    • rolenull이 아니라면, 해당 역할을 가진 사용자만을 선택하는 조건을 추가합니다.
    • role 값이 null이면 이 조건은 무시되어, 역할에 관계없이 모든 사용자를 선택하는 쿼리를 생성합니다.
  • dynamicIsUserTeam(String team):
    • 사용자의 팀을 파라미터 team을 기준으로 검사합니다.
    • teamnull이 아니라면, 해당 팀에 속한 사용자만을 선택하는 조건을 추가합니다.
    • team 값이 null이면 이 조건은 무시되어, 특정 팀에 속한 사용자에 관계없이 모든 사용자를 선택하는 쿼리를 생성합니다.

저는 동적 쿼리를 작성할 때 주로 메서드로 정의하는 방식을 선호합니다. 이 방법을 사용하면 다른 쿼리에서도 해당 조건을 쉽게 재사용할 수 있기 때문입니다.

Using Dynamic Queries in WHERE

SQL WHERE 절은 데이터베이스 쿼리에서 데이터를 필터링하는 데 사용됩니다. Querydsl을 이용하면 실행 시점에 조건을 결정하여 WHERE 절을 동적으로 구성할 수 있습니다.

Querydsl을 활용하여 다음과 같이 WHERE 절에 동적 쿼리를 작성할 수 있습니다:

QuerydslRepository.java
@Slf4j
@RequiredArgsConstructor
public class PostQueryDslRepository {

    private final JPAQueryFactory queryFactory;

    public List<PostEntity> fetchBy(
            boolean includePrivate,
            boolean includeDeleted) {
        return queryFactory
                .select(post)
                .from(post)
                .innerJoin(post.user, user)
                .where(dynamicIsPostPrivate(includePrivate),
                       dynamicIsPostDeleted(includeDeleted))
                .fetch();
    }

    private BooleanExpression dynamicIsPostPrivate(boolean includePrivate) {
        return includePrivate ? null : post.isPrivate.eq(false);
    }

    private BooleanExpression dynamicIsPostDeleted(boolean includeDeleted) {
        return includeDeleted ? null : post.isDeleted.eq(false);
    }
}
  • dynamicIsPostPrivate(): 게시물의 공개 여부에 대한 동적 조건을 생성하는 메서드입니다. includePrivate 값이 true이면 null을 반환하고, 그렇지 않으면 공개되지 않은 게시물을 필터링하는 조건을 반환합니다.
  • dynamicIsPostDeleted(): 게시물의 삭제 여부에 대한 동적 조건을 생성하는 메서드입니다. includeDeleted 값이 true이면 null을 반환하고, 그렇지 않으면 삭제되지 않은 게시물을 필터링하는 조건을 반환합니다.

이와 같이, Querydsl을 사용하여 WHERE 절을 작성하면 필요에 따라 쿼리를 동적으로 조정할 수 있어서 불필요한 데이터 탐색 시간을 줄여 성능을 최적화할 수 있습니다. 또한, 코드의 간결성과 가독성이 향상되어 유지보수성도 높아집니다.

Using Dynamic Queries in ORDER BY

SQL의 ORDER BY 절은 결과 데이터를 특정 순서로 정렬하는 데 사용됩니다. Querydsl에서는 OrderSpecifier 클래스를 통해 이러한 정렬 조건을 동적으로 설정할 수 있습니다. 이는 사용자의 요구에 따라 데이터를 유연하게 정렬하고 결과를 적절하게 제어할 수 있도록 합니다, 특히 웹 애플리케이션에서 사용자 입력에 기반한 결과의 동적 조정에 매우 유용합니다.

다음은 OrderSpecifier를 사용하여 동적으로 정렬 조건을 설정합니다:

QuerydslRepository.java
@RequiredArgsConstructor
public class QuerydslRepository {

    private final JPAQueryFactory queryFactory;

    public List<User> fetchBy(Order order, String target) {
        return queryFactory
                .select(user)
                .from(user)
                .orderBy(dynamicSortCriteria(order, target))
                .fetch();
    }

    private OrderSpecifier<?> dynamicSortCriteria(Order order, String target) {
        return switch (target) {
            case "id" -> new OrderSpecifier<>(Order.DESC, user.id);
            case "username" -> new OrderSpecifier<>(Order.ASC, user.username);
            case "name" -> new OrderSpecifier<>(Order.ASC, user.name);
            case "signupDate" -> new OrderSpecifier<>(Order.DESC, user.signupDate);
            case "grade" -> new OrderSpecifier<>(Order.DESC, user.grade);
            default -> null;
        };
    }
}
  • dynamicSortCriteria(): 이 메서드는 전달받은 정렬 옵션을 바탕으로 동적으로 OrderSpecifier 객체를 생성합니다. 이 객체는 JPAQueryFactoryorderBy() 메서드에 전달되어 정렬 조건으로 활용됩니다. 만약 적합한 옵션이 없으면, null을 반환하여 정렬을 생략합니다.

또한, Querydsl의 orderBy() 메서드는 가변 인자를 받아 다양한 정렬 조건을 동시에 처리할 수 있습니다. 이 기능을 활용하면 사용자의 복잡한 정렬 요구를 유연하게 조합하고 적용할 수 있으며, 실제 프로덕션 레벨의 복잡한 정렬 로직에 효과적으로 대응할 수 있습니다:

QuerydslRepository.java
@RequiredArgsConstructor
public class QuerydslRepository {

    private final JPAQueryFactory queryFactory;

    public List<User> fetchBy(List<String> targets) {
        return queryFactory
                .select(user)
                .from(user)
                .orderBy(dynamicSortCriteria(targets))
                .fetch();
    }

    private OrderSpecifier<?>[] dynamicSortCriteria(List<String> targets) {
        List<OrderSpecifier<?>> orderSpecifiers = new ArrayList<>();
        for (String target : targets) {
            OrderSpecifier<?> orderSpecifier = switch (target) {
                case "username" -> new OrderSpecifier<>(Order.ASC, user.username);
                case "name" -> new OrderSpecifier<>(Order.ASC, user.name);
                case "signupDate" -> new OrderSpecifier<>(Order.DESC, user.signupDate);
                case "grade" -> new OrderSpecifier<>(Order.DESC, user.grade);
                default -> new OrderSpecifier<>(Order.DESC, user.id);
            };
            orderSpecifiers.add(orderSpecifier);
        }

        return orderSpecifiers.toArray(new OrderSpecifier[0]);
    }
}
  • dynamicSortCriteria(): 이 메서드는 사용자가 지정한 필드에 따라 다양한 OrderSpecifier 객체들을 생성하고, 이 객체들을 리스트에 추가합니다. 여기서는 각 정렬 필드명에 따라 정렬의 방향을 고정했지만 이 역시 매개 변수를 통해 동적으로 설정이 가능합니다. 모든 옵션을 순회하고 모인 OrderSpecifier 리스트는 마지막에 배열로 변환되어 orderBy() 메서드에 전달됩니다.

이러한 접근 방식은 코드의 유연성과 유지보수성을 향상시키며, 다양한 사용자 정렬 요구를 정밀하게 처리하는 동적 정렬 조건을 효과적으로 설계하고 구현할 수 있습니다.

  • Spring
  • JPA
  • Querydsl