- QueryDsl이란?
- QueryDSL 기본 사용법
- 사용 시 주의사항
- JPA와 QueryDsl 함께 사용하기
QueryDsl은 Java 애플리케이션에서 타입 안전하고 간결한 SQL 쿼리 작성을 지원하는 오픈소스 프레임워크이다. (JPQL에 비해 타입 안정성을 가진다는 점이 중요하다.)
QueryDSL을 사용하면 JPA의 Method Query로는 짤 수 없는 복잡한 쿼리를 작성할 수 있고, JPQL로는 짤 수 없는 동적 쿼리를 작성할 수 있다.
다음 의존성을 추가한다.

QueryDslConfig를 추가한다.

Product와 관련해 QueryDSL을 사용하는 예시 코드이다.
ProductRepositoryCustom 인터페이스를 만들어 메서드를 생성하고, ProductRepositoryImpl을 만들어 이 인터페이스를 구현하며, ProductRepository에 ProductRepositoryCustom를 extends 한다.
public interface ProductRepositoryCustom {
ResGetSellingProductsDTOApiV1 getSellingProductsByQueryDsl(Integer searchType, String searchValue);
}
@Repository
@RequiredArgsConstructor
public class ProductRepositoryImpl implements ProductRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
@Override
public ResGetSellingProductsDTOApiV1 getSellingProductsByQueryDsl(Integer searchType, String searchValue) {
JPAQuery<ResGetSellingProductsDTOApiV1.Product> query = jpaQueryFactory
// .select(Projections.constructor(ResGetSellingProductsDTOApiV1.Product.class
.select(new QResGetSellingProductsDTOApiV1_Product( // DTO로 리턴
product.id,
new CaseBuilder() // case when,
.when(product.isDiscount.eq(true))
.then(product.name.append(Expressions.stringTemplate("' (할인 중)'")))
.otherwise(product.name),
product.isDiscount,
product.price,
product.discountPrice,
JPAExpressions // 스칼라 서브 쿼리
.select(productKind.name)
.from(productKind)
.where(productKind.id.eq(product.productKind.id)),
productMaker.name,
product.stock
))
.from(product) // 기준 테이블
.join(productMaker).on(product.productMaker.id.eq(productMaker.id)) // 조인
.where(customSearch(searchType, searchValue)) // 동적 조건을 위한 함수 호출
.orderBy(product.id.desc()); // 역정렬
return ResGetSellingProductsDTOApiV1.builder()
.productList(query.fetch())
.build();
}
// searchType에 따라 동적으로 쿼리를 변경시킨다.
private Predicate customSearch(Integer searchType, String searchValue) {
BooleanBuilder booleanBuilder = new BooleanBuilder();
booleanBuilder.and(product.name.isNotEmpty());
// 주의 productKind처럼 기준테이블이나 조인에 쓰이지 않은 테이블은 조건에 넣으면 안된다.
if (searchType == 1) {
booleanBuilder.and(product.name.contains(searchValue));
} else if (searchType == 2) {
booleanBuilder.and(productMaker.name.contains(searchValue));
}
return booleanBuilder;
}
//////////////////////////////////////
// 여기서 부터는 참고용 코드 입니다.//
//////////////////////////////////////
// 상품 목록 조회
public List<Product> getProducts(Integer searchType, String searchValue) {
JPAQuery<Product> query = jpaQueryFactory
.select(product)
.from(product)
.where(customSearch(searchType, searchValue));
return query.fetch();
}
// 단일 상품 조회
public Product getProductById(Long id) {
JPAQuery<Product> query = jpaQueryFactory
.select(product)
.from(product)
.where(product.id.eq(id));
return query.fetchOne();
}
// 상품 수량 조회
public Long getProductCount() {
JPAQuery<Long> query = jpaQueryFactory
.select(product.count())
.from(product);
return query.fetchOne();
}
// QueryDsl은 from절에 서브쿼리가 불가능하다.
// 배민에서는 조건을 미리 걸어 쿼리를 하여 리스트를 가져오고
// 해당 리스트를 다음 쿼리의 where 조건으로 넣어서 해결했다고 한다.
public List<Product> getProductsByInlineViewSubQuery() {
List<Long> idList = jpaQueryFactory
.select(product.id)
.from(product)
.where(customSearch(2, "농심"))
.fetch();
JPAQuery<Product> query = jpaQueryFactory
.select(product)
.from(product)
.where(product.id.in(idList))
.where(product.stock.gt(0));
return query.fetch();
}
}
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
}
일반적으로 QueryDSL을 JPA 환경에서 사용하면 별도의 인터페이스 상속과 구현체를 만들어야 합니다. 하지만 굳이 상속이나 구현 없이, JPQLQueryFactory(예: JPAQueryFactory)를 DI 받아서 사용하는 것만으로도 QueryDSL 기능을 100% 활용할 수 있습니다. 즉, 기존에 구현/상속 구조를 제거하고도 깔끔하게 QueryDSL을 사용할 수 있습니다.
JPA에서 연관 관계를 맺을 때, 비효율을 방지하기 위해 LAZY 옵션을 주로 사용한다.


이 경우 N+1 문제가 발생할 수 있고, QueryDsl에서 join 사용 시 데이터를 가져오지 못하는 경우가 있다.


위 옵션들을 추가하여 QueryDsl에서 join 사용 후 연관관계 조회 시 N+1 문제를 방지할 수 있다.
또는 일반 join이 아닌 fetchJoin을 사용할 수도 있다.

JPA에서 일반적으로 사용하는 Method Query에 QueryDsl을 연동하여 Search를 간편하게 구현할 수 있다.
QuerydslPredicateExecutor는 Predicate를 JpaRepository에서 추출할 수 있도록 한다.

Controller에서 Predicate와 Pageable을 매개변수로 받는다.

Service에서 Repository를 호출 할 때 Predicate와 Pageable을 전달한다.

BooleanBuilder를 이용하여 검색조건을 추가하거나 제한할 수 있다.
Controller에서 매개변수로 idList를 받아서 서비스로 전달한다.

Service에서 BooleanBuilder를 선언하고 Predicate를 매개변수로 포함하며, idList가 null이 아니라면 리스트를 In절로 검색하도록 추가한다. stock (재고량)이 0보다 큰 것만 조회하도록 한다.

QuerydslBinderCustomizer는 추출된 Predicate의 조건을 수정할 수 있다.
앞서 소개한 방법은 문자 검색 시 equals 조건만 검색이 되었다. 이번에는 문자 검색 시 contains 조건으로 바꾸어 보자.


하지만 MSA 구조에서는 각 서비스의 환경이 크게 복잡해지지 않기 때문에, QueryDsl을 사용하는 것이 CRUD(특히 Search)를 구현하는 데 유리하다.