Skip to content

[BE] QueryDSL 사용 이유

wyc edited this page Sep 22, 2023 · 3 revisions

Querydsl


Spring Boot + Spring Data JPA를 조합해서 사용할 때 대부분의 상황에서 모자란 기능은 없지만, 한계는 명확히 존재합니다. JPA가 JdbcTemplate 만큼 복잡한 쿼리에 대해서 잘 지원해주지 않기 때문인데요.

이런 문제를 Querydsl로 해소시켜줄 수 있습니다. 이는 자바 언어의 한계를 넘어서 쿼리를 문자열 그 자체가 아닌, 실제 자바 코드로 작성할 수 있게끔 도와줍니다.

더 이상 실수는 용납하지 않는다.

@Override
@Query(
    "SELECT pf FROM PetFood pf" +
       "JOIN FETCH pf.brand b" +
       "JOIN FETCH pf.primaryIngredients pi"
)
List<PetFood> findPetFoods();

위 문법은 치명적인 오류가 있습니다. 바로 "(큰따옴표) 앞에 공백을 추가하지 않았다는 것입니다. 이는 개발자가 육안으로 찾아내지 못한다면, IDE는 물론, 컴파일 시점에도 파악할 수 없는 예외로 간주되기 때문에 무조건 런타임까지 가서야 파악할 수 있게됩니다.

하지만 Querydsl은 다음과 같은 문법으로 위 문제를 해결해줍니다.

public List<PetFood> findPetFoods() {
    return queryFactory.selectFrom(petFood)
        .join(petFood.brand b)
        .fetchJoin()
        .join(petFood.primaryIngredients pi)
        .fetchJoin()
}

위는 jpql 쿼리와 똑같이 동작하는 코드입니다. 다만 자바 코드로 쿼리를 작성하기 때문에 IDE와 컴파일러가 파악할 수 있습니다. 또한 부가적인 기능으로 자동완성 기능을 지원합니다.

우리 쿼리가 달라졌어요

요구사항이 항상 순탄하지만은 않았습니다. 특히 필터링 관련 동적쿼리 요구사항과 jpa 내부에서 발생하는 문제들을 해결하기 위해 쿼리가 매우 복잡해지는 상황이 빈번하게 일어났습니다.

아래는 저희가 만난 실제 쿼리입니다.

@Query("""
    SELECT pf FROM PetFood pf
           JOIN FETCH pf.brand b
           JOIN FETCH pf.primaryIngredients pi
           JOIN FETCH pf.functionalities f
           WHERE (:brandsName IS NULL OR b.name IN :brandsName)
           AND (:standards IS NULL OR
               (:unitedStates = true AND pf.hasStandard.unitedStates = true)
               OR (:europe = true AND pf.hasStandard.europe = true))
           AND (:primaryIngredientList IS NULL OR pi.name IN :primaryIngredientList)
           AND (:functionalityList IS NULL OR f.name IN :functionalityList)
    """
    )
    List<PetFood> findPetFoods(
            @Param("brandsName") List<String> brandsName,
            @Param("standards") Boolean standards,
            @Param("unitedStates") Boolean unitedStates,
            @Param("europe") Boolean europe,
            @Param("primaryIngredientList") List<String> primaryIngredientList,
            @Param("functionalityList") List<String> functionalityList
    );

저희 집고팀은 저런 괴물 쿼리를 두고만 볼 수는 없었습니다. 집고팀이 Querydsl을 도입한 결과 위 쿼리는 다음과 같이 변했습니다.

@Repository
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PetFoodQueryRepositoryImpl implements PetFoodQueryRepository {

    private final JPAQueryFactory queryFactory;

    public List<PetFood> findPagingPetFoods(
            List<String> brandsName,
            List<String> standards,
            List<String> primaryIngredientList,
            List<String> functionalityList,
            Long lastPetFoodId,
            int size
    ) {
        return queryFactory
                .select(petFood)
                .from(petFood)
                .join(petFood.brand, brand)
                .fetchJoin()
                .join(petFood.petFoodPrimaryIngredients, petFoodPrimaryIngredient)
                .join(petFood.petFoodFunctionalities, petFoodFunctionality)
                .where(
                        isLessThan(lastPetFoodId),
                        isContainBrand(brandsName),
                        isMeetStandardCondition(standards),
                        isContainPrimaryIngredients(primaryIngredientList),
                        isContainFunctionalities(functionalityList)
                )
                .orderBy(petFood.id.desc())
                .limit(size)
                .fetch();
    }

    private BooleanExpression isLessThan(Long lastPetFoodId) {
        if (lastPetFoodId == null) {
            return null;
        }
        return petFood.id.loe(lastPetFoodId);
    }

    private BooleanExpression isContainBrand(List<String> brandsName) {
        if (brandsName.isEmpty()) {
            return null;
        }
        return petFood.brand.name.in(brandsName);
    }

    private BooleanExpression isMeetStandardCondition(List<String> standards) {
        if (standards.isEmpty()) {
            return null;
        }
        for (String standard : standards) {
            if (standard.equals("미국")) {
                return petFood.hasStandard.unitedStates.isTrue();
            }
            if (standard.equals("유럽")) {
                return petFood.hasStandard.europe.isTrue();
            }
        }
        return null;
    }

    private BooleanExpression isContainPrimaryIngredients(List<String> primaryIngredientList) {
        if (primaryIngredientList.isEmpty()) {
            return null;
        }
        return petFood.petFoodPrimaryIngredients.any()
                .primaryIngredient.name.in(primaryIngredientList);
    }

    private BooleanExpression isContainFunctionalities(List<String> functionalityList) {
        if (functionalityList.isEmpty()) {
            return null;
        }
        return petFood.petFoodFunctionalities.any()
                .functionality.name.in(functionalityList);
    }

}

애플리케이션 코드가 생기면서 코드 양 자체는 약간 늘어났지만, 대신 누가 봐도 이전코드보다 이해하기 쉽게 변했습니다. 코드가 늘어났다고 해서 성능이 낮아지거나 하지도 않습니다.

Clone this wiki locally