프로젝트를 진행하면서 크리에이터를 검색하는 쿼리를 짠 경험이 있다.
생성일 기준 오름차순, 내림차순 혹은 이름 기준 오름차순, 내림차순 혹은 키워드 검색이 있을 수 있고 무한스크롤링 구현을 위한 top값도 필요했다. 처음에는 정렬 조건이 creatorName일 때, createdDate일 때 기준값이 다르기 때문에 topName, topDate 이렇게 요상하게 param을 두고 api를 작성하였다. (JPA 처음 공부하던 시절..)
api/creator/search?name={keyword}&sort={asc/desc}&condition={creatorName/createdDate}&topDate={createdDate}&topName={creatorName}
보기만해도 api가 더럽다.
무한 스크롤을 위한 top, 이름순, 최신순, 오래된순과 같은 정렬 조건, 키워드 검색 등 동적인 쿼리가 많아 단순 JPA와 @NamedEntityGraph를 이용하기에는 동적인 쿼리문이 필요하기 때문에 복잡하다. JPQL를 이용해서 짤 수는 있지만 개발자가 이해하기 어려워진다.
@Query("""
select c from Creator c
where (:name is null or c.creatorName like %:name%)
AND (
(:condition = 'createDate' and (:topDate is null or (:sort = 'desc' and c.creatorProfileId < :topDate) or (:sort = 'asc' and c.creatorProfileId > :topDate)))
OR
(:condition = 'creatorName' and (:topName is null or (:sort = 'desc' and c.creatorName < :topName) or (:sort = 'asc' and c.creatorName > :topName)))
)
""")
Slice<Creator> findCreatorsWithScrolling(@Param("name") String name,
@Param("topDate") Long topDate,
@Param("topName") String topName,
@Param("condition") String condition,
@Param("sort") String sort,
Pageable pageable);
한 눈에 봤을 때 이해가 쉬운가? param이 있을 수도 있고 없을 수도 있는걸 모두 고려하면서 정렬 조건까지 고려했기 때문에 and,or 연산 파티에 정신이 없는 쿼리가 탄생했다.

나도 짜면서 이해하기 쉽지 않았는데 어떻게 이해하냐고!~!!~~ 이를 개선하기 위해 queryDSL을 이용하여 리팩토링 해줬다.
먼저 QueryDSL은 타입 안전한 쿼리를 작성할 수 있도록 지원하는 Java 기반의 ORM 프레임워크다. SQL, JPQL, MongoDB 등의 쿼리를 자바 코드로 작성할 수 있게 해주며, 컴파일 시점에 쿼리의 오류를 검출할 수 있어 코드의 안정성을 높이는 데 도움을 준다. 또한 복잡한 쿼리 작성 시 유용하게 사용된다.
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory queryFactory() {
return new JPAQueryFactory(em);
}
}
Repository 설정
@Repository
public interface CreatorCustomRepository {
List<Creator> findCreatorByCreatedDateWithScrolling(String sort, String keyword, LocalDateTime top);
List<Creator> findCreatorByCreatorNameWithScrolling(String sort, String keyword, String top);
}
@Repository
public interface CreatorRepository extends JpaRepository<Creator, Long>, CreatorCustomRepository {
...
}
query-dsl로 리팩토링한 api는 다음과 같다.
api/creator/search?name={keyword}&sort={asc/desc}&condition={creatorName/createdDate}&top={creatorId}
훨씬 깔끔한 param으로 변경됐다.
먼저 서비스 단에서 top param이 있다면 pk를 가지고 Creator Entity를 찾는다.
Creator creator = creatorRepository.findCreatorByCreatorId(creatorId)
그 후 condition 컬럼에 따라 해당 크리에이터의 이름, 생성일을 추출한다. 그 이후 queryDSL을 통해 상위 10개의 데이터를 불러온다.
@RequiredArgsConstructor
public class CreatorCustomRepositoryImpl implements CreatorCustomRepository {
private final JPAQueryFactory jpaQueryFactory;
@Override
public List<Creator> findCreatorByCreatedDateWithScrolling(String sort, String keyword, LocalDateTime top) {
OrderSpecifier<?> orderSpecifier = getCreatedDateOrderSpecifier(sort);
return jpaQueryFactory.selectFrom(creator1)
.where(
getKeyWord(keyword),
afterCreatedDateCursor(top, sort)
)
.orderBy(orderSpecifier)
.limit(10)
.fetch();
}
@Override
public List<Creator> findCreatorByCreatorNameWithScrolling(String sort, String keyword, String top) {
OrderSpecifier<?> orderSpecifier = getCreatorNameOrderSpecifier(sort);
return jpaQueryFactory.selectFrom(creator1)
.where(
getKeyWord(keyword),
afterCreatorNameCursor(top, sort)
)
.orderBy(orderSpecifier)
.limit(10)
.fetch();
}
private BooleanExpression getKeyWord(String word) {
return word != null ? creator1.creatorName.containsIgnoreCase(word) : null;
}
private BooleanExpression afterCreatedDateCursor(LocalDateTime top, String sort) {
if (top == null) {
return null;
} else if (sort.equals("asc")) {
return creator1.createdDate.gt(top);
} else {
return creator1.createdDate.lt(top);
}
}
private BooleanExpression afterCreatorNameCursor(String top, String sort) {
if (top == null) {
return null;
} else if (sort.equals("asc")) {
return creator1.creatorName.gt(top);
} else {
return creator1.creatorName.lt(top);
}
}
private OrderSpecifier<LocalDateTime> getCreatedDateOrderSpecifier(String sort) {
if (sort.equals("desc")) {
return creator1.createdDate.desc();
} else {
return creator1.createdDate.asc();
}
}
private OrderSpecifier<String> getCreatorNameOrderSpecifier(String sort) {
if(sort.equals("desc")){
return creator1.creatorName.desc();
} else {
return creator1.creatorName.asc();
}
}
}
@Override
public List<Creator> findCreatorByCreatedDateWithScrolling(String sort, String keyword, LocalDateTime top) {
OrderSpecifier<?> orderSpecifier = getCreatedDateOrderSpecifier(sort);
return jpaQueryFactory.selectFrom(creator1)
.where(
getKeyWord(keyword),
afterCreatedDateCursor(top, sort)
)
.orderBy(orderSpecifier)
.limit(10)
.fetch();
}
orderSpecifier 객체를 통해 정렬한다.getKeyWord 함수를 통해 크리에이터이름을 기준으로 like 연산을 실행한다.afterCreatedDateCursor함수를 통해 현재 조회된 크리에이터 기준 다음에 있는 데이터만 가져온다.limit을 통해 가져올 데이터의 개수를 제한한다.private BooleanExpression getKeyWord(String word) {
return word != null ? creator1.creatorName.containsIgnoreCase(word) : null;
}
private OrderSpecifier<LocalDateTime> getCreatedDateOrderSpecifier(String sort) {
if (sort.equals("desc")) {
return creator1.createdDate.desc();
} else {
return creator1.createdDate.asc();
}
}
private BooleanExpression afterCreatedDateCursor(LocalDateTime top, String sort) {
if (top == null) {
return null;
} else if (sort.equals("asc")) {
return creator1.createdDate.gt(top);
} else {
return creator1.createdDate.lt(top);
}
}
[
{
"creatorId": 27,
"creatorName": "크리에이터27",
"profileImg": "creator_img27.jpg"
},
{
"creatorId": 15,
"creatorName": "크리에이터15",
"profileImg": "creator_img15.jpg"
},
...
{
"creatorId": 11,
"creatorName": "크리에이터11",
"profileImg": "creator_img11.jpg"
}
]
api/creator/search?condition=createdDate&sort=desc&top=11을 요청할 것이고 마지막 데이터의 creatorId가 11이니 크리에이터 11보다 늦게 생성된 행만 필터링 해줘야 한다.코드리뷰할때도 훨씬 이해가 잘돼서 좋았음!!!
간단한 쿼리문은 JPA, JPQL을 이용해서 짜는게 편하지만 복잡한 동적 조건문이 많은 쿼리문은 query-dsl로 짜는게 유지보수 측면에서 훨씬 좋은 것 같다. 조건 하나하나를 모듈화해서 관리할 수 있으니!! 다들 query-dsl 하세요~~ (단점 초기 세팅 너무 귀찮음ㅜ)