20241210 TIL : QueryDsl이란?

MCS·2024년 12월 10일

TIL

목록 보기
22/45

오늘 학습한 내용

  • QueryDsl이란?
    • QueryDSL 기본 사용법
    • 사용 시 주의사항
    • JPA와 QueryDsl 함께 사용하기

QueryDsl이란?

QueryDsl은 Java 애플리케이션에서 타입 안전하고 간결한 SQL 쿼리 작성을 지원하는 오픈소스 프레임워크이다. (JPQL에 비해 타입 안정성을 가진다는 점이 중요하다.)

QueryDSL을 사용하면 JPA의 Method Query로는 짤 수 없는 복잡한 쿼리를 작성할 수 있고, JPQL로는 짤 수 없는 동적 쿼리를 작성할 수 있다.

QueryDSL 기본 사용법

다음 의존성을 추가한다.

QueryDslConfig를 추가한다.

Product와 관련해 QueryDSL을 사용하는 예시 코드이다.
ProductRepositoryCustom 인터페이스를 만들어 메서드를 생성하고, ProductRepositoryImpl을 만들어 이 인터페이스를 구현하며, ProductRepository에 ProductRepositoryCustom를 extends 한다.

  • ProductRepositoryCustom
public interface ProductRepositoryCustom {

    ResGetSellingProductsDTOApiV1 getSellingProductsByQueryDsl(Integer searchType, String searchValue);

}
  • ProductRepositoryImpl
@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();
    }

}
  • ProductRepository
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와 QueryDsl 함께 사용하기

JPA에서 일반적으로 사용하는 Method Query에 QueryDsl을 연동하여 Search를 간편하게 구현할 수 있다.

1. QuerydslPredicateExecutor

QuerydslPredicateExecutor는 Predicate를 JpaRepository에서 추출할 수 있도록 한다.

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

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

2. BooleanBuilder

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

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

3. QuerydslBinderCustomizer

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

QueryDsl의 한계점

  • 다중 조인이나 복잡한 서브쿼리를 필요로 하는 고급 쿼리에서는 제한적일 수 있다.
  • Controller, Service가 Querydsl에 의존하게 된다.
  • 복잡한 환경에서 사용하기 어렵다.

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

profile
백엔드를 잘 하고 싶은 사람

0개의 댓글