BE/QueryDsl
Querydsl을 활용한 조건 검색 기능 개선
cjsrhd94
2022. 8. 7. 23:51
프로젝트 내 검색 기능을 동적 쿼리를 활용해 개선해보고 싶어 개인 프로젝트에 Querydsl을 사용하기로 결정했다.
기존 코드의 문제점
PostController
// PostController.class
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/post")
public class PostController {
private final PostService postService;
// controller code...
@GetMapping("/search")
public Page<PostsResponseDto> getPostsBySearchKeywordInBoard(
@RequestParam("board") Long boardId,
@RequestParam Integer page,
@RequestParam(required = false) String type,
@RequestParam(required = false) String keyword) {
// type이 null로 들어올 경우 빈 값으로 변환
if (StringUtils.isEmpty(type)) {
type = "";
}
if (type.equals(PostSearchType.TICON.stringValue)) {
return postService.getPostsByPostTitleOrPostContentInBoard(boardId, keyword, page);
}
if (type.equals(PostSearchType.TITLE.stringValue)) {
return postService.getPostsByPostTitleInBoard(boardId, keyword, page);
}
if (type.equals(PostSearchType.CONTENT.stringValue)) {
return postService.getPostsByPostContentInBoard(boardId, keyword, page);
}
if (type.equals(PostSearchType.NICKNAME.stringValue)) {
return postService.getPostsByUserNicknameInBoard(boardId, keyword, page);
}
return postService.getPostsInBoard(boardId, page);
}
// controller code...
}
// PostSearchType.class
@Getter
@RequiredArgsConstructor
public enum PostSearchType {
TICON("ticon","제목 + 내용"),
TITLE("title","제목"),
CONTENT("content","내용"),
NICKNAME("nickname", "닉네임");
public final String stringValue;
public final String description;
}
위 코드는 기존에 구현한 검색 기능이다. Controller layer에서 parameter로 들어오는 type과 keyword에 따라 어떤 service를 호출할지 결정하는 방식이다. type은 Enum타입으로 정해진 type만 받을 수 있게 하였다.
PostRepository
//PostRepository.class
public interface PostRepository extends JpaRepository<Post, Long> {
// repository code...
@Query("select distinct new com.project.board.domain.post.dto.PostsQueryDto(" +
"p.id, pc.topic, p.title, u.nickname, p.createdDate, p.likeCount) " +
"from Post p " +
"join p.user u " +
"join p.postCategory pc " +
"join pc.board b " +
"where b.id =:id " +
"and p.title like concat('%',:title,'%') " +
"order by p.id desc")
Page<PostsQueryDto> findPostsByPostTitleInBoard(@Param("id") Long id,
@Param("title") String title,
Pageable pageable);
@Query("select distinct new com.project.board.domain.post.dto.PostsQueryDto(" +
"p.id, pc.topic, p.title, u.nickname, p.createdDate, p.likeCount) " +
"from Post p " +
"join p.user u " +
"join p.postCategory pc " +
"join pc.board b " +
"where b.id =:id " +
"and p.content like concat('%',:content,'%') " +
"order by p.id desc")
Page<PostsQueryDto> findPostsByPostContentInBoard(@Param("id") Long id,
@Param("content") String content,
Pageable pageable);
@Query("select distinct new com.project.board.domain.post.dto.PostsQueryDto(" +
"p.id, pc.topic, p.title, u.nickname, p.createdDate, p.likeCount) " +
"from Post p " +
"join p.user u " +
"join p.postCategory pc " +
"join pc.board b " +
"where b.id =:id " +
"and p.title like concat('%',:keyword,'%')" +
"or p.content like concat('%',:keyword,'%') " +
"order by p.id desc")
Page<PostsQueryDto> findPostsByPostTitleOrPostContentInBoard(@Param("id") Long id,
@Param("keyword") String keyword,
Pageable pageable);
@Query("select distinct new com.project.board.domain.post.dto.PostsQueryDto(" +
"p.id, pc.topic, p.title, u.nickname, p.createdDate, p.likeCount) " +
"from Post p " +
"join p.user u " +
"join p.postCategory pc " +
"join pc.board b " +
"where b.id =:id " +
"and p.user.nickname like concat('%',:nickname,'%') " +
"order by p.id desc")
Page<PostsQueryDto> findPostsByUserNicknameInBoard(@Param("id") Long id,
@Param("nickname") String nickname,
Pageable pageable);
// repository code...
}
위 코드는 기능상 문제는 없으나, 몇가지 개선이 필요하다.
- 비슷한 쿼리가 반복된다. 위 레포지토리의 쿼리문을 보면 like 뒤에 "어떤 컬럼에서 검색할 것인가"를 제외하면 나머지 부분은 동일하다. 지금은 검색 조건이 적어서 직접 작성해도 큰 문제는 아니지만, 검색 조건이 늘어날 때마다 비슷한 정적 쿼리를 계속 만들어야 하는 문제점이 있다.
- 런타임 에러가 발생할 수 있다. 쿼리문은 문자열로 작성되기 때문에 개발자가 잘못 작성해도 컴파일 단계에서 에러가 발생하지 않고, 실제 동작할 때 런타임 에러를 발생시킨다.
Querydsl을 활용한 개선
@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepositoryCustom {
private final JPAQueryFactory queryFactory;
// repository code...
@Override
public Page<PostsQueryDto> findPostsBySearchWordInBoard(Long boardId, String postSearchType, String searchWord, Pageable pageable) {
QueryResults<PostsQueryDto> results = queryFactory
.select(Projections.constructor(PostsQueryDto.class,
post.id,
postCategory.topic,
post.title,
user.nickname,
post.createdDate,
post.likeCount
))
.from(post)
.join(post.user, user)
.join(post.postCategory, postCategory)
.join(postCategory.board, board)
.where(board.id.eq(boardId),
postSearchTypeEq(postSearchType, searchWord))
.orderBy(post.id.desc())
.fetchResults();
List<PostsQueryDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
private BooleanExpression postSearchTypeEq(String postSearchType, String searchWord) {
if (postSearchType.equals(PostSearchType.TICON.stringValue)) {
return ticonEq(searchWord);
}
if (postSearchType.equals(PostSearchType.TITLE.stringValue)) {
return titleEq(searchWord);
}
if (postSearchType.equals(PostSearchType.CONTENT.stringValue)) {
return contentEq(searchWord);
}
if (postSearchType.equals(PostSearchType.NICKNAME.stringValue)) {
return nicknameEq(searchWord);
}
return null;
}
private BooleanExpression ticonEq(String searchWord) {
return searchWord != null ? post.title.contains(searchWord).or(post.content.contains(searchWord)) : null;
}
private BooleanExpression titleEq(String searchWord) {
return searchWord != null ? post.title.contains(searchWord) : null;
}
private BooleanExpression contentEq(String searchWord) {
return searchWord != null ? post.content.contains(searchWord) : null;
}
private BooleanExpression nicknameEq(String searchWord) {
return searchWord != null ? user.nickname.contains(searchWord) : null;
}
// repository code...
}
위 코드는 Querydsl을 활용하여 앞서 말한 문제점들을 개선한 코드다.
- Querydsl에서는 동적 쿼리를 활용할 수 있다. Querydsl에서는 BooleanExpression 타입의 조건 메서드를 만들어 해당 메서드의 return값에 따라 where절에 조건을 동적으로 추가할 수 있다.
- 쿼리문이 문자열이 아닌 자바 코드로 작성되기 때문에 컴파일 시점에 문법 오류를 확인하여 런타임 에러를 방지할 수 있다. 또한 자바 코드로 작성된 부분들은 재사용이 가능해 유지 보수가 좋다.
이번 프로젝트는 검색 조건이 많지 않아 Querydsl이 가지는 장점을 극적으로 보여주기 어려웠다. 그러나 수많은 검색 조건들이 and로 결합되어야 할 때 Querydsl의 장점이 더욱 빛날 것이라고 생각한다.