Dev/Backend

[BackEnd] 커뮤니티 게시물 목록 조회 API 쿼리를 QueryDsl로 구현해보기

sebinChu 2024. 10. 23. 00:57

개요


코알라 커뮤니티 기능 중 게시판 목록 조회 API를 위해 QueryDsl을 학습 및 구현해보았다.

요구사항은 다음과 같다.

  • 정렬 (최신순, 조회수, 좋아요수)
  • 검색 (제목, 내용, 작성자)
  • 페이징

 

이 세 가지 조건 하에, DB에서 값을 가져오기 위해 QueryDsl을 활용하였다.
* JPQL 대신 QueryDsl을 사용한 이유 

 

 

필요한 Entity, Request


게시판 전체 목록을 조회할 때 단순히 GET을 하는 것이 아니라, 개요에서 언급한 3가지 조건을 만족시켜야 한다.

 

조회 조건을 만족하기 위해 다음과 같은 Request record를 생성한다.

@Schema(description = "게시글 목록 검색 요청 객체")
public record SearchBoardRequest(

    @Schema(description = "검색 키워드(게시글 제목, 게시글 내용, 작성자)")
    String searchKeyword,

    @Schema(description = "게시글 카테고리")
    BoardCategory category,

    @Schema(description = "게시글 정렬 조건", allowableValues = {"LATEST", "LIKE", "VIEW_COUNT"})
    BoardSort sort,

    @Min(1)
    @Schema(description = "페이지 번호", type = "integer", requiredMode = RequiredMode.REQUIRED)
    int page,

    @Min(10)
    @Schema(description = "페이지별 개수", type = "integer", requiredMode = RequiredMode.REQUIRED)
    int size
) {

    public PageRequest pageRequest() {
        return PageRequest.of(page - 1, size);
    }

}
  • PageRequestJPA에서 제공하는 페이지 번호는 0부터 시작한다. 하지만 웹사이트 상에서의 페이지는 1부터 시작하므로 항상 -1을 설정한다.
    - 이 Request 쿼리를 실행하면 결과로 Page<T> 객체가 반환되는데, 전체 데이터 수, 페이지 수, 현재 페이지의 데이터 목록 등을 포함하고 있다. 요청한 페이지 번호와 페이지 크기(한 페이지에 포함될 데이터 수)를 기반으로, DB 쿼리에서 데이터 범위를 지정할 수 있다.

 

  • Request 객체를 record로 설정하는 이유
    1. 기본적으로 불변이라서 일관성 유지에 좋다.
    2. 생성자, toString() , hashCode(), equals() 메소드를 자동으로 생성한다.
    3. DTO(데이터 전송 객체)로 사용되어, 필드의 의미가 명확하다.

 

전체 코드를 나눠서 쿼리 살펴보기


1. select로 조건에 따른 결과를 Dto 형태로 반환

  List<ListBoardDto> boardList =
      queryFactory
        .select(Projections.fields(
            ListBoardDto.class,
            board.id.as("boardId"),
            board.category.stringValue().as("category"),
            board.title,
            board.member.name.as("createdName"),
            board.createdTime,
            Expressions.booleanTemplate(
                "case when {0} > {1} then true else false end",
                board.createdTime, LocalDateTime.now().minusDays(3))
              .as("newBoardYn"),
            board.viewCount,
            board.fixYn,
            board.deleteYn
          )
        )

 

 

  • category는 다음과 같은 Enum 타입으로 정의되어 있다.
    이런 enum 값을 문자열로 변환해야 DB에서 사용할 수 있다. 따라서 stringValue()로 변환해준다.
@Getter
@AllArgsConstructor
public enum BoardCategory {
    NOTICE("공지"),
    FREE("자유"),
    QUESTION("질문"),
    INFO("정보"),
    PROMOTION("홍보");

    private final String name;
}

 

  • Expressions.booleanTemplate
    최신글 여부를 위한 조건부로직(SQL의 CASE)이다. Template을 사용해서 복잡한 SQL 쿼리 로직을 동적으로 보다 간편하게 작성할 수 있다.

 

2. where 절로 카테고리, 키워드 설정

.where(
    searchCategory(request.category()),
    searchKeyword(request.searchKeyword()),
    board.saveYn.isTrue()
)

  • searchCategory 메소드에 따라 게시물의 카테고리랑 요청 카테고리랑 비교해서 일치하는 게시물만 반환한다.board 객체의 category를 QueryDsl의 eq 메소드로 일치하는지 비교한다.
private static Predicate searchCategory(BoardCategory category) {
    if (category == null) {
      return null;
    }
    return board.category.eq(category);
  }

 

  • searchKeyword도 마찬가지.
    그런데 키워드는 세 가지 옵션이 가능하기에 다음과 같은 BooleanBuilder로 구현한다.
  private Predicate searchKeyword(String searchKeyword) {
        if (!StringUtils.hasText(searchKeyword)) {
            return null;
        }
        BooleanBuilder builder = new BooleanBuilder();
        builder.or(board.title.contains(searchKeyword));
        builder.or(board.content.contains(searchKeyword));
        builder.or(board.member.name.contains(searchKeyword));
        return builder;
    }

 

builder.or(...)를 사용하여 여러 조건을 결합하면, 주어진 키워드가 제목이나 내용, 작성자 이름 중 하나라도 포함될 경우 조건이 참이 된다.

 

  • BooleanBuilder
    조건을 or, and로 결합할 때 가독성이 좋아진다.
    여러 조건을 동적으로 추가할 수 잇다.

 

 

프로젝트에서 처음 QueryDsl을 적용하며 Template이나 where절에 넣는 메소드 등 배운 점이 많아서 정리해보았다!