본문 바로가기
Spring Boot Project/Plming

[Plming] 게시판 기능 코드 리뷰 4편 (검색 기능)

by slchoi 2022. 4. 15.
728x90
SMALL

저번 글에서는 게시글 신청과 관련된 코드를 리뷰해보았다. 이번 글에서는 게시판 검색과 관련된 코드들을 리뷰해보려 한다.

게시판 검색 조건은 제목, 내용, 제목+내용에 포함된 키워드를 통해 검색할 수 있으며, 카테고리, 모집 상태, 태그, 기간, 최대 참여 가능한 인원수별로 검색이 가능하다. 또한, 검색 조건이 아무것도 없는 경우에는 게시글 전체 리스트를 반환한다.

1. SearchApiController

@RestController
@RequestMapping("/posts")
@RequiredArgsConstructor
public class SearchApiController {

    private final SearchService searchService;

    @GetMapping
    public ResponseEntity<Object> search(@Nullable @RequestParam final String searchType,
                                         @Nullable @RequestParam final String keyword, @Nullable @RequestParam final List<String> category,
                                         @Nullable @RequestParam final List<String> status, @Nullable @RequestParam final List<Integer> tagIds,
                                         @Nullable @RequestParam final List<Integer> period, @Nullable @RequestParam final List<Integer> participantMax
            , final Pageable pageable) {

        SearchRequestDto params = SearchRequestDto.builder()
                .searchType(searchType).keyword(keyword).category(category)
                .status(status).tagIds(tagIds).period(period).participantMax(participantMax)
                .build();

        return searchService.search(params, pageable);
    }
}
  • GET "/posts" 요청이 들어오면 게시글 검색 로직이 수행된다.
  • 쿼리 파라미터로 검색 타입(제목, 내용, 제목+내용), 검색 키워드, 카테고리, 모집 상태, 태그 Id, 기간, 최대 참여 인원수를 받아온다. 모든 정보는 필수로 입력되어야 하는 것이 아니고, null 값이 들어올 수 있으므로 @Nullable 애노테이션을 붙여준다. 또한, 페이징을 적용해 검색 결과를 응답해줄 것이므로 Pageable 정보도 받아온다.
  • 쿼리 파라미터로 받아온 정보를 바탕으로 새로운 SearchRequestDto 객체를 생성하고 Pageable 정보와 함께 SearchService의 search 메서드의 매개변수로 넘겨주고, 이 메서드를 호출해 반환받은 결과를 그대로 반환한다.
🙄 궁금한 점 (Spring)
@Nuallable 애노테이션과 @RequestParam 애노테이션의 required = false 옵션의 차이


처음 코드를 구현했을 때는 모든 쿼리 파라미터가 반드시 들어와야 하는 것은 아니기 때문에 @RequestParam 애노테이션의 required 옵션을 false로 지정해주었다. 하지만 테스트를 진행하면서 아무 조건도 들어오지 않을 경우 NullpointException이 발생했고, 이 오류를 해결하기 위해 기존에 지정해둔 required 옵션을 지우고 @Nullable 애노테이션을 붙여줬다. 
@RequestParam의 required 옵션과 @Nullable 애노테이션의 차이를 정리해봐야겠다.
🏭 Refactoring

처음 계획은 검색 URI로 "/search"를 사용할 예정이어서 URI에 "/posts"가 들어가는 다른 API들과 별도로 관리하기 위해 다른 Controller 클래스를 새로 만들었지만, 검색 조건이 모두 null 값인 경우 전체 게시글 리스트를 반환하도록 하면서 검색 API가 전체 게시글 리스트 조회 API로 통합되었다. 즉, GET "/posts" 요청이 들어오면 검색 로직이 수행되도록 수정하면서 검색 API를 다른 Controller에서 관리할 필요가 없어졌다. 
따라서 SearchApiController을 BoardApiController에 통합시켜야겠다. 

 

2. SearchService

search

public ResponseEntity<Object> search(final SearchRequestDto params, final Pageable pageable) {

    if(isConditionNull(params)) {
        return ResponseEntity.status(200).body(boardService.findAllByDeleteYn(pageable));
    } else {
        if(params.getPeriod() != null) {
            Collections.sort(params.getPeriod());
            if(params.getPeriod().size() == 1){
                params.setPeriod(List.of(params.getPeriod().get(0), params.getPeriod().get(0)));
            }
        }

        if(params.getParticipantMax() != null) {
            Collections.sort(params.getParticipantMax());
            if(params.getParticipantMax().size() == 1) {
                params.setParticipantMax(List.of(params.getParticipantMax().get(0), params.getParticipantMax().get(0)));
            }
        }

        return searchAllCondition(params, pageable);
    }
}
  • 매개변수로 SearchRequestDto 객체와 Pageable 정보를 받아온다.
  • 매개변수로 받아온 SearchRequestDto를 isConditionNull 메서드의 매개변수로 넣어주어 검색 조건이 모두 null인지 확인한다.
  • 검색 조건이 모두 null인 경우 BoardService의 findAllByDeleteYn 메서드에 Pageable 정보를 매개변수로 전달하고, 이 메서드를 호출해 반환받은 삭제되지 않은 게시글 전체 리스트를 response body에 담고 상태 코드 200과 함께 전달한다.
  • 검색 조건이 null이 아닌 경우
    • SearchRequestDto에서 기간 정보를 꺼내와 정렬해주고, 기간 정보 리스트의 size가 1인 경우 같은 값을 한 번 더 넣어준다.
    • 다음으로 SearchRequestDto에서 최대 참여 인원수 정보를 꺼내와 정렬해주고, size가 1인 경우 같은 값을 한 번 더 넣어준다.
    • Querydsl의 between 기능을 사용하기 위해 리스트 값을 정렬해주고, 크기가 1인 경우 같은 값을 한 번 더 넣어주었다.
    • searchAllCondition 메서드의 매개변수에 SearchRequestDto 객체와 Pageable 정보를 넣어주고, 이 메서드를 호출해 반환받은 값을 그대로 반환한다.

 

isConditionNull

/**
 * 모든 조건이 null인지 검사
 */
private boolean isConditionNull(SearchRequestDto params) {
    return params.getSearchType() == null && params.getKeyword() == null
            && params.getCategory() == null && params.getPeriod() == null && params.getStatus() == null
            && params.getTagIds() == null && params.getParticipantMax() == null;
}
  • 매개변수로 받아온 SearchRequestDto 객체가 포함하고 있는 모든 정보들에 대해 null인지 검사하고, 모든 정보들이 null일 경우 true를 반환한다.

 

searchAllCondition

/**
 * 모든 조건 적용해서 검색
 */
@Transactional
public ResponseEntity<Object> searchAllCondition(final SearchRequestDto params, final Pageable pageable) {

    return ResponseEntity.ok(boardService.getBoardListResponseFromPage(boardRepository.searchAllCondition(params, pageable)));
}
  • 매개변수로 SearchRequestDto 객체와 Pageable 정보를 받아와 BoardRepository의 searchAllCondition 메서드의 매개변수로 전달한다.
  • BoardRepository의 searchAllCondition 메서드에서 반환받은 결과를 BoardService의 getBoardListResponseFromPage 메서드의 매개변수로 넣어주고, 이 메서드를 호출해 반환받은 값을 response body에 넣어주고 상태 코드 200과 함께 반환한다.

 

3. BoardService

findAllByDeleteYn

public Page<BoardListResponseDto> findAllByDeleteYn(final Pageable pageable) {

    Page<Board> list = boardRepository.findAllPageSort(pageable);
    return getBoardListResponseFromPage(list);
}
  • 매개변수로 받아온 Pageable 정보를 BoardRepository의 findAllPageSort 메서드의 매개변수로 넣어주고, 이 메서드를 호출해 반환받은 값을 list 변수에 저장한다.
  • list 변수를 getBoardListResponseFromPage의 매개변수로 전달해, 이 메서드를 호출하고 반환받은 값을 그대로 반환한다.

 

getBoardListResponseFromPage

/**
 * 각 게시글의 태그 이름 조회 후 BoardListResponseDto 반환
 */
@Transactional
public Page<BoardListResponseDto> getBoardListResponseFromPage(Page<Board> list) {

    List<BoardListResponseDto> result = new ArrayList<BoardListResponseDto>();
    List<Board> boards = list.getContent();
    for (Board post : boards) {
        Integer participantNum = applicationService.countParticipantNum(post.getId());
        result.add(new BoardListResponseDto(post, participantNum, boardTagService.findTagNameByBoardId(post.getId())));
    }

    return new PageImpl<>(result);
}
  • 매개변수로 페이징 정보가 포함된 Board 객체 페이지를 받아온다.
  • 매개변수로 받아온 Board 객체 페이지에서 Board 객체 리스트만 빼와 boards 변수에 저장한다.
  • boards 리스트에 포함되어 있는 모든 Board 객체에 대해 BoardListResponseDto 변수를 생성해 result 리스트에 추가한다.
  • result 메서드를 content 값으로 가지는 새로운 Page 객체를 생성해 반환한다.

 

4. BoardRepository

searchAllCondition

더보기
@Override
public Page<Board> searchAllCondition(SearchRequestDto params, Pageable pageable) {

    if(params.getKeyword() == null) {

        // content를 가져오는 쿼리
        List<Board> query = jpaQueryFactory
                .select(board).from(board)
                .leftJoin(board.boardTags, boardTag)
                .fetchJoin()
                .where(keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct()
                .orderBy(board.id.desc(), board.createDate.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // count만 가져오는 쿼리
        JPQLQuery<Board> count = jpaQueryFactory.selectFrom(board)
                .leftJoin(board.boardTags, boardTag)
                .where(keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct();

        return PageableExecutionUtils.getPage(query, pageable, () -> count.fetchCount());

    } else if(params.getSearchType().equals("title")) {

        // content를 가져오는 쿼리
        List<Board> query = jpaQueryFactory
                .select(board).from(board)
                .leftJoin(board.boardTags, boardTag)
                .fetchJoin()
                .where(keywordInTitle(params.getKeyword()), keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct()
                .orderBy(board.id.desc(), board.createDate.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // count만 가져오는 쿼리
        JPQLQuery<Board> count = jpaQueryFactory.selectFrom(board)
                .leftJoin(board.boardTags, boardTag)
                .where(keywordInTitle(params.getKeyword()), keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct();

        return PageableExecutionUtils.getPage(query, pageable, () -> count.fetchCount());

    } else if(params.getSearchType().equals("content")) {

        // content를 가져오는 쿼리
        List<Board> query = jpaQueryFactory
                .select(board).from(board)
                .leftJoin(board.boardTags, boardTag)
                .fetchJoin()
                .where(keywordInContent(params.getKeyword()), keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct()
                .orderBy(board.id.desc(), board.createDate.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // count만 가져오는 쿼리
        JPQLQuery<Board> count = jpaQueryFactory.selectFrom(board)
                .leftJoin(board.boardTags, boardTag)
                .where(keywordInContent(params.getKeyword()), keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct();

        return PageableExecutionUtils.getPage(query, pageable, () -> count.fetchCount());

    } else {

        // content를 가져오는 쿼리
        List<Board> query = jpaQueryFactory
                .select(board).from(board)
                .leftJoin(board.boardTags, boardTag)
                .fetchJoin()
                .where(keywordInTitleAndContent(params.getKeyword()), keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct()
                .orderBy(board.id.desc(), board.createDate.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // count만 가져오는 쿼리
        JPQLQuery<Board> count = jpaQueryFactory.selectFrom(board)
                .leftJoin(board.boardTags, boardTag)
                .where(keywordInTitleAndContent(params.getKeyword()), keywordInCategory(params.getCategory())
                        , keywordInStatus(params.getStatus()), keywordInTag(params.getTagIds())
                        , isPeriod(params.getPeriod()), isParticipantMax(params.getParticipantMax())
                        , board.deleteYn.eq('0'))
                .distinct();

        return PageableExecutionUtils.getPage(query, pageable, () -> count.fetchCount());
    }
}

 

  • 매개변수로 SearchRequestDto 객체와 Pageble 정보를 가져온다.
  • 경우 1. keyword가 null인 경우
    • keyword 검색을 제외한 나머지 조건들과 일치하는 Board 객체의 정보를 id와 생성 날짜를 기준으로 내림차순으로 정렬한 뒤, Pageable 정보를 적용해 반환한다.
  • 경우 2. searchType이 title인 경우
    • keyword가 제목에 포함되어 있고 나머지 조건들과 일치하는 Board 객체의 정보를 id와 생성 날짜를 기준으로 내림차순으로 정렬한 뒤, Pageable 정보를 적용해 반환한다.
  • 경우 3. searchType이 content인 경우
    • keyword가 제목에 포함되어 있고 나머지 조건들과 일치하는 Board 객체의 정보를 id와 생성 날짜를 기준으로 내림차순으로 정렬한 뒤, Pageable 정보를 적용해 반환한다.
  • 경우 4. 경우 1, 2, 3에 포함되지 않는 나머지
    • keyword가 제목 또는 내용에 포함되어 있고 나머지 조건들과 일치하는 Board 객체의 정보를 id와 생성 날짜를 기준으로 내림차순으로 정렬한 뒤, Pageable 정보를 적용해 반환한다.
🏭 Refactoring
searchAllCondition 메서드 코드를 보면 where문의 조건들만 다르고 나머지 부분은 일치하는 것을 확인할 수 있다. 이 부분을 매개변수로 where 조건문에 들어갈 조건 정보를 받아오는 private 메서드를 생성해 searchAllConditon 메서드에서는 이 private 메서드를 호출하도록 수정해서 같은 코드의 반복을 줄여야겠다.

 

findAllPageSort

@Override
public Page<Board> findAllPageSort(Pageable pageable) {

    // content를 가져오는 쿼리
    List<Board> query = jpaQueryFactory
            .select(board).from(board)
            .leftJoin(board.boardTags, boardTag)
            .fetchJoin()
            .where(board.deleteYn.eq('0'))
            .orderBy(board.id.desc(), board.createDate.desc())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

    // count만 가져오는 쿼리
    JPQLQuery<Board> count = jpaQueryFactory.selectFrom(board)
            .leftJoin(board.boardTags, boardTag)
            .where(board.deleteYn.eq('0'));

    return PageableExecutionUtils.getPage(query, pageable, () -> count.fetchCount());
}
  • 매개변수로 Pageable 정보를 받아와 board.deleteYn의 값이 '0'인 Board 객체 리스트에 Paging 정보를 적용한 Board 객체 Page를 반환한다.

 

이번 글을 마지막으로 게시판 기능과 관련된 모든 코드 리뷰는 끝이 났다. 맡았던 파트의 개발은 끝이 나서 4월 안으로 초기 배포를 하기 위해 다른 팀원분이 맡기로 했었던 알림 기능 구현을 진행하기로 했다. 원래는 댓글 기능 관련 코드 리뷰를 진행하려 했으나 우선은 알림 기능 개발을 진행하면서 시간이 날 때마다 코드 리뷰를 진행해야겠다.  

728x90
LIST

댓글