QueryDSL로 코드 리팩토링

김영식·2023년 8월 13일
0

기존에 작성한 검색 쿼리문이 가독성이 떨어진다는 문제를 해결하기위해
QueryDSL을 이용해서 리팩토링을 해보았습니다.

QueryDSL은 다양한 장점을 가지고 있습니다.

1. 타입 안전성: 자바 코드로 쿼리를 작성하므로 컴파일러의 타입 체크를 활용하여 런타임 오류를 사전에 방지할 수 있습니다. 이는 안정적이고 오류를 줄이는데 도움이 됩니다.
2. 가독성과 유지보수성: 코드 기반으로 쿼리를 작성하므로 SQL 문자열의 혼란과 오타를 줄이고, 쿼리와 애플리케이션 로직 사이의 관계를 명확하게 표현할 수 있습니다. 이는 코드의 가독성과 유지보수성을 크게 향상시켜줍니다.
3. 동적 쿼리 작성: 조건문과 조합하여 동적 쿼리를 손쉽게 작성할 수 있습니다. 이는 다양한 상황에 유연하게 대응할 수 있는 장점을 제공합니다.
4. JPQL 및 네이티브 쿼리 지원: QueryDSL은 JPQL 뿐만 아니라 네이티브 SQL 쿼리도 생성할 수 있어, 데이터베이스 종속적인 쿼리 작성에 유용합니다. 이를 통해 더욱 높은 수준의 제어와 최적화가 가능합니다.

리팩토링

기존 Service단에 작성한 검색 메서드입니다. 가독성이 떨어지고 그때문에 유지보수도 힘들어 보입니다.

// 전달받은 재료중 하나라도 포함하는 레시피 검색
 public Page<Recipe> searchAllRecipesByIngredients(List<String> ingredients, Pageable pageable) {
        String countQuery = "SELECT COUNT(DISTINCT r) FROM Recipe r JOIN r.ingredients i WHERE i.ingredientName IN :ingredients " +
                "GROUP BY r " +
                "HAVING COUNT(DISTINCT i.ingredientName) = :ingredientCount " +
                "AND COUNT(DISTINCT i.ingredientName) = :ingredientListCount";
        TypedQuery<Long> countTypedQuery = entityManager.createQuery(countQuery, Long.class);
        countTypedQuery.setParameter("ingredients", ingredients);
        countTypedQuery.setParameter("ingredientCount", Long.valueOf(ingredients.size()));
        countTypedQuery.setParameter("ingredientListCount", (long) ingredients.size());
        Long totalCount = countTypedQuery.getSingleResult();

        String query = "SELECT r FROM Recipe r JOIN r.ingredients i WHERE i.ingredientName IN :ingredients " +
                "GROUP BY r " +
                "HAVING COUNT(DISTINCT i.ingredientName) = :ingredientCount " +
                "AND COUNT(DISTINCT i.ingredientName) = :ingredientListCount";
        TypedQuery<Recipe> typedQuery = entityManager.createQuery(query, Recipe.class);
        typedQuery.setParameter("ingredients", ingredients);
        typedQuery.setParameter("ingredientCount", Long.valueOf(ingredients.size()));
        typedQuery.setParameter("ingredientListCount", (long) ingredients.size());
        typedQuery.setFirstResult((int) pageable.getOffset());
        typedQuery.setMaxResults(pageable.getPageSize());
        List<Recipe> resultList = typedQuery.getResultList();

        return new PageImpl<>(resultList, pageable, totalCount);
    }
// 전달받은 재료를 모두 포함하는 레시피 검색
    public Page<Recipe> searchRecipesByIngredients(List<String> ingredients, Pageable pageable) {
        String countQuery = "SELECT COUNT(DISTINCT r) FROM Recipe r JOIN r.ingredients i WHERE i.ingredientName IN :ingredients";
        TypedQuery<Long> countTypedQuery = entityManager.createQuery(countQuery, Long.class);
        countTypedQuery.setParameter("ingredients", ingredients);
        Long totalCount = countTypedQuery.getSingleResult();

        String query = "SELECT DISTINCT r FROM Recipe r JOIN r.ingredients i WHERE i.ingredientName IN :ingredients";
        TypedQuery<Recipe> typedQuery = entityManager.createQuery(query, Recipe.class);
        typedQuery.setParameter("ingredients", ingredients);
        typedQuery.setFirstResult((int) pageable.getOffset());
        typedQuery.setMaxResults(pageable.getPageSize());
        List<Recipe> resultList = typedQuery.getResultList();

        return new PageImpl<>(resultList, pageable, totalCount);
    }

QueryDSL을 사용하려면 몇가지 설정이 필요합니다. 저는 아래 블로그를 참고하여 설정했습니다.
https://may9noy.tistory.com/854

설정 후 QueryDSLConfig 클래스를 작성하여 JPAQueryFactory를 빈으로 등록하고 프로젝트 전역에서 사용할 수 있게 했습니다.

@Configuration
public class QueryDSLConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

새로 작성한 Repository 클래스입니다. recipe와 ingredient는 Q클래스를 static import 하여 사용했습니다.

Q클래스는 엔티티클래스의 메타정보를 담고있는 클래스로 QueryDSL은 이를 이용하여 객체를 생성할 필요 없이 타입안정성을 보장하며 쿼리를 작성할 수 있습니다.

또한 매서드 내부에서 결과의 갯수만 반환하는 카운트쿼리와 그보다 비교적 복잡한 검색쿼리를 나누어서 성능최적화, 데이터베이스 부하 최소화, 높은 유지보수성을 기대할 수 있게 만들었습니다.

@Repository
@RequiredArgsConstructor
public class RecipeQueryRepository {
    private final JPAQueryFactory queryFactory;
    
    public Page<Recipe> searchAllRecipesByIngredients(List<String> ingredients, Pageable pageable) {
        Long totalCount = queryFactory
                .select(recipe)
                .from(recipe)
                .innerJoin(recipe.ingredients, ingredient)
                .where(ingredient.ingredientName.in(ingredients))
                .groupBy(recipe)
                .having(ingredient.ingredientName.count().eq((long) ingredients.size()))
                .fetchCount();

        List<Recipe> resultList = queryFactory
                .selectFrom(recipe)
                .innerJoin(recipe.ingredients, ingredient)
                .where(ingredient.ingredientName.in(ingredients))
                .groupBy(recipe)
                .having(ingredient.ingredientName.count().eq((long) ingredients.size()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        return new PageImpl<>(resultList, pageable, totalCount);
    }

    public Page<Recipe> searchRecipesByIngredients(List<String> ingredients, Pageable pageable) {
        BooleanExpression ingredientNameInList = ingredient.ingredientName.in(ingredients);

        long totalCount = queryFactory
                .select(recipe)
                .from(recipe)
                .innerJoin(recipe.ingredients, ingredient)
                .where(ingredientNameInList)
                .fetchCount();

        List<Recipe> resultList = queryFactory
                .select(recipe)
                .distinct()
                .from(recipe)
                .innerJoin(recipe.ingredients, ingredient)
                .where(ingredientNameInList)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        return new PageImpl<>(resultList, pageable, totalCount);
    }
}

직접 사용해 보니 가독성이 올라갔고 그로인해 유지보수하기도 쉬워졌습니다.

컴파일시에 에러를 잡을 수 있다는것도 그렇고 여러모로 장점이 참 많습니다.

0개의 댓글