QueryDSL로 더 안전하고 효율적인 쿼리 작성하기

coldrice99·2024년 11월 12일
0
post-thumbnail

QueryDSL이란?

QueryDSL은 JPA의 JPQL 생성기라고 생각하면 이해하기 쉽습니다. JPA 엔티티 정보를 활용하여 객체 지향적으로 쿼리를 작성할 수 있게 돕는 쿼리 빌더입니다. SQL을 문자열로 작성하는 대신, Q클래스와 메서드를 통해 쿼리를 객체처럼 다루어 타입 안전성을 보장하고 코드의 가독성유지보수성을 높일 수 있습니다.


QueryDSL의 주요 개념

  • Q클래스: 각 엔티티마다 자동으로 생성되는 쿼리 전용 클래스입니다. 엔티티 필드에 안전하게 접근할 수 있으며, 직관적이고 간결한 쿼리 작성이 가능합니다.
  • JPAQueryFactory: QueryDSL의 핵심 클래스입니다. Q클래스와 함께 작동하여 객체 지향 쿼리를 생성하고 조건을 설정할 수 있습니다.

QueryDSL의 장점

  • 타입 안전성: 자바 문법으로 쿼리를 작성해 컴파일 시점에 에러를 잡을 수 있어 런타임 에러를 줄이고 안전한 쿼리 작성이 가능합니다.
  • 동적 쿼리 작성: 다양한 상황에 맞는 조건을 추가하거나 제거하여 동적으로 쿼리를 생성할 수 있습니다.

QueryDSL에서 동적 쿼리 처리 방법

  1. BooleanBuilder
    조건을 if문으로 추가하는 방식으로, 가독성이 떨어지고 메서드화가 어려워 재사용성이 낮은 단점이 있습니다.

  2. BooleanExpression
    BooleanExpression을 사용하면 조건을 메서드 단위로 관리하여 가독성과 재사용성을 높일 수 있습니다.

    • 조건들을 메서드로 분리하여 where 절의 조건을 쉽게 확인할 수 있으며, 여러 BooleanExpression을 조합해 새로운 조건을 생성할 수 있습니다.
    • null이 반환되면 where 절에서 자동으로 무시되기 때문에 null에 안전합니다.

QueryDSL 기본 사용 예시

다음은 User 엔티티의 usernamepassword를 이용해 특정 사용자를 조회하는 예제입니다.

@PersistenceContext
EntityManager em;

public List<User> selectUserByUsernameAndPassword(String username, String password) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QUser user = QUser.user;

    return queryFactory
            .selectFrom(user)
            .where(user.username.eq(username)
                  .and(user.password.eq(password)))
            .fetch();
}
  • .selectFrom(user): 조회할 엔티티를 지정합니다.
  • .where(...): 쿼리 조건을 설정합니다. .eq()= 연산자와 동일하며, .and()를 통해 여러 조건을 추가할 수 있습니다.
  • .fetch(): 쿼리를 실행하고 결과 리스트를 반환합니다.

QueryDSL을 위한 설정

  1. Q클래스 생성
    build.gradle에 다음 설정을 추가해 QueryDSL이 Q클래스를 자동 생성하도록 설정합니다.

    plugins {
        id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    }
    
    dependencies {
        implementation "com.querydsl:querydsl-jpa"
        annotationProcessor "com.querydsl:querydsl-apt:${querydslVersion}:jpa"
    }
  2. JPAQueryFactory 설정
    JPAQueryFactoryEntityManager가 필요하므로, 이를 빈으로 등록하여 주입받아 사용합니다.

    @Configuration
    public class JPAConfiguration {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        @Bean
        public JPAQueryFactory jpaQueryFactory() {
            return new JPAQueryFactory(entityManager);
        }
    }

예시 구현: 내가 멘션된 Slack 쓰레드 조회 기능

Slack 프로젝트에서 내가 멘션된 쓰레드를 조회하는 기능을 구현합니다. 각 쓰레드는 다음과 같은 정보를 포함합니다:

  • 채널 이름, 작성자 정보 (이름 및 프로필 이미지)
  • 쓰레드 본문, 이모지 정보(이모지별 카운트 포함)
  • 댓글 목록 및 각 댓글의 이모지 정보
package me.whitebear.jpa.thread;

import static me.whitebear.jpa.follow.QFollow.follow;
import static me.whitebear.jpa.thread.QThread.thread;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.hibernate.Hibernate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;

@RequiredArgsConstructor
public class ThreadRepositoryQueryImpl implements ThreadRepositoryQuery {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Page<Thread> findThreadsByFollowingUser(FollowingThreadSearchCond cond, Pageable pageable) {
        var query = query(thread, cond)
                .orderBy(createOrderSpecifier(cond.getSortType()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize());

        var threads = query.fetch();
        long totalSize = countQuery(cond).fetch().get(0);

        return PageableExecutionUtils.getPage(threads, pageable, () -> totalSize);
    }

    private <T> JPAQuery<T> query(Expression<T> expr, ThreadSearchCond cond) {
        return jpaQueryFactory.select(expr)
                .from(thread)
                .leftJoin(thread.channel).fetchJoin()
                .leftJoin(thread.emotions).fetchJoin()
                .leftJoin(thread.comments).fetchJoin()
                .leftJoin(thread.mentions).fetchJoin()
                .where(
                    channelIdEq(cond.getChannelId()),
                    mentionedUserIdEq(cond.getMentionedUserId())
                );
    }

    private BooleanExpression channelIdEq(Long channelId) {
        return Objects.nonNull(channelId) ? thread.channel.id.eq(channelId) : null;
    }

    private BooleanExpression mentionedUserIdEq(Long mentionedUserId) {
        return Objects.nonNull(mentionedUserId) ? thread.mentions.any().user.id.eq(mentionedUserId) : null;
    }

    private OrderSpecifier<?> createOrderSpecifier(FollowingThreadSearchCond.SortType sortType) {
        return switch (sortType) {
            case CREATE_AT_DESC -> thread.createdAt.desc();
            case USER_NAME_ASC -> thread.user.username.asc();
        };
    }
}

요약

QueryDSL을 사용하면 JPA와 통합하여 JPQL 생성기처럼 쿼리를 객체 지향적으로 작성할 수 있습니다. 이를 통해 타입 안전성가독성을 높일 수 있으며, BooleanExpression을 사용해 동적 쿼리를 관리할 수 있어 복잡한 조건에서도 유연하게 코드를 작성할 수 있습니다.

profile
서두르지 않으나 쉬지 않고

0개의 댓글