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의 장점이 더욱 빛날 것이라고 생각한다.