QueryDSL은 JPA의 JPQL 생성기라고 생각하면 이해하기 쉽습니다. JPA 엔티티 정보를 활용하여 객체 지향적으로 쿼리를 작성할 수 있게 돕는 쿼리 빌더입니다. SQL을 문자열로 작성하는 대신, Q클래스와 메서드를 통해 쿼리를 객체처럼 다루어 타입 안전성을 보장하고 코드의 가독성과 유지보수성을 높일 수 있습니다.
BooleanBuilder
조건을 if
문으로 추가하는 방식으로, 가독성이 떨어지고 메서드화가 어려워 재사용성이 낮은 단점이 있습니다.
BooleanExpression
BooleanExpression
을 사용하면 조건을 메서드 단위로 관리하여 가독성과 재사용성을 높일 수 있습니다.
BooleanExpression
을 조합해 새로운 조건을 생성할 수 있습니다.다음은 User
엔티티의 username
과 password
를 이용해 특정 사용자를 조회하는 예제입니다.
@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()
: 쿼리를 실행하고 결과 리스트를 반환합니다.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"
}
JPAQueryFactory 설정
JPAQueryFactory
는 EntityManager
가 필요하므로, 이를 빈으로 등록하여 주입받아 사용합니다.
@Configuration
public class JPAConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
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
을 사용해 동적 쿼리를 관리할 수 있어 복잡한 조건에서도 유연하게 코드를 작성할 수 있습니다.