기존에 작성한 검색 쿼리문이 가독성이 떨어진다는 문제를 해결하기위해
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);
}
}
직접 사용해 보니 가독성이 올라갔고 그로인해 유지보수하기도 쉬워졌습니다.
컴파일시에 에러를 잡을 수 있다는것도 그렇고 여러모로 장점이 참 많습니다.