Springboot로 디자인이 하나도 없고 매우 간단한 게시판을 구현하는 시리즈입니다.

최종 소스는 깃헙에서 확인하실 수 있습니다.

 

이 글에서는 게시글 검색과 페이징에 대해 알아보겠습니다.

 

검색은 Repository에서 함수만 잘 작성해주면 Like 검색이 되고,

페이징은 Service에서 Page 객체를 활용하는 방법과 알고리즘을 구현하면 됩니다!

 


 

1. 검색

1) 검색 - 퍼블리싱

src/main/resources/templates/board/list.html

<!-- 검색 form -->
<form action="/board/search" method="GET">
    <div>
        <input name="keyword" type="text" placeholder="검색어를 입력해주세요">
    </div>

    <button>검색하기</button>
</form>
  • 기존에 있던 파일이며, 푸터 위에 검색 form만 추가해줬습니다.
  • 초기 페이지 진입 시 리스트를 보여주는 파일인 동시에 검색 결과로도 사용되는 페이지입니다.
 
 
 
 
2) 검색 - Controller

src/main/java/com/victolee/board/controller/BoardController.java

@GetMapping("/board/search")
public String search(@RequestParam(value="keyword") String keyword, Model model) {
    List<BoardDto> boardDtoList = boardService.searchPosts(keyword);
    
    model.addAttribute("boardList", boardDtoList);
    
    return "board/list.html";
}
  • 기존에 존재했던 컨트롤러에서 매핑함수 search() 를 작성합니다.
  • 특별한 것은 없고, 클라이언트에서 넘겨주는 keyword를 검색어로 활용합니다.

 

 

 

 

3) 검색 - Service

src/main/java/com/victolee/board/service/BoardService.java

@Transactional
public List<BoardDto> searchPosts(String keyword) {
    List<BoardEntity> boardEntities = boardRepository.findByTitleContaining(keyword);
    List<BoardDto> boardDtoList = new ArrayList<>();

    if (boardEntities.isEmpty()) return boardDtoList;

    for (BoardEntity boardEntity : boardEntities) {
        boardDtoList.add(this.convertEntityToDto(boardEntity));
    }

    return boardDtoList;
}

private BoardDto convertEntityToDto(BoardEntity boardEntity) {
    return BoardDto.builder()
            .id(boardEntity.getId())
            .title(boardEntity.getTitle())
            .content(boardEntity.getContent())
            .writer(boardEntity.getWriter())
            .createdDate(boardEntity.getCreatedDate())
            .build();
}
  • searchPosts()
    • Repository에서 검색 결과를 받아와 비즈니스 로직을 실행하는 함수입니다.
    • 마찬가지로 특별한 것은 없으며, Controller <---> Service 간에는 Dto 객체로 전달하는 것이 좋으므로 이와 관련된 로직만 있을 뿐입니다.
  • convertEntityToDto()
    • 시리즈의 이전 게시물에서 Entity를 Dto로 변환하는 작업이 중복해서 발생했었는데, 이를 함수로 처리하도록 개선하였습니다.

 

 

 

 

4) 검색 - Repository

src/main/java/com/victolee/board/domain/repository/BoardRepository.java

import com.victolee.board.domain.entity.BoardEntity;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface BoardRepository extends JpaRepository<BoardEntity, Long> {
    List<BoardEntity> findByTitleContaining(String keyword);
}
  • findByTitleContaining()​
    • 드디어 검색을 직접적으로 호출하는 메서드가 나왔습니다.
    • JpaRepository에서 메서드명의 By 이후는 SQL의 where 조건 절에 대응되는 것인데, 이렇게 Containing을 붙여주면 Like 검색이 됩니다.
      • 즉, 해당 함수는 %{keyword}% 이렇게 표현이 됩니다.
 
여기를 참고하시면, JPA like query를 사용하는 다양한 방법이 나옵니다.
  • StartsWith
    • 검색어로 시작하는 Like 검색
    • {keyword}%
  • EndsWith
    • 검색어로 끝는 Like 검색
    • %{keyword}
  • IgnoreCase
    • 대소문자 구분 없이 검색
  • Not
    • 검색어를 포함하지 않는 검색

 

 
5) 검색 - 테스트
이제 데이터를 추가하고, 검색을 해보시면 검색이 잘 되는것을 확인할 수 있습니다.
 
 

 

 

 
2. 페이징
1) 페이징 - 퍼블리싱
src/main/resources/templates/board/list.html
<div>
    <span th:each="pageNum : ${pageList}" th:inline="text">
        <a th:href="@{'/?page=' + ${pageNum}}">[[${pageNum}]]</a>
    </span>
</div>
  • 위에서 작성한 검색 영역 위에 페이징 div 엘리먼트를 작성합니다.
  • 서버로부터 페이징 번호를 리스트 pageList 변수에 받아와서 번호를 뿌려줍니다.
    • 각 번호를 누르면, 새로운 페이지 번호를 서버로 전달합니다.
 
 
 
 
2) 페이징 - Controller
src/main/java/com/victolee/board/controller/BoardController.java
/* 게시글 목록 */
@GetMapping("/")
public String list(Model model, @RequestParam(value="page", defaultValue = "1") Integer pageNum) {
List<BoardDto> boardList = boardService.getBoardlist(pageNum);
Integer[] pageList = boardService.getPageList(pageNum);

model.addAttribute("boardList", boardList);
model.addAttribute("pageList", pageList);

return "board/list.html";
}
  • 기존에 존재했던 컨트롤러의 매핑함수 list()를 수정한 것입니다.
  • @RequestParam(value="page", defaultValue = "1") Integer pageNum
    • page 이름으로 넘어오면 파라미터를 받아주고, 없으면 기본 값으로 1을 설정합니다.
    • 페이지 번호는 서비스 계층의 getPageList() 함수로 넘겨줍니다.
 
 
3) 페이징 - Service
src/main/java/com/victolee/board/service/BoardService.java
public class BoardService {
...

    private static final int BLOCK_PAGE_NUM_COUNT = 5; // 블럭에 존재하는 페이지 번호 수
    private static final int PAGE_POST_COUNT = 4; // 한 페이지에 존재하는 게시글 수

    @Transactional
    public List<BoardDto> getBoardlist(Integer pageNum) {
        Page<BoardEntity> page = boardRepository.findAll(PageRequest.of(pageNum - 1, PAGE_POST_COUNT, Sort.by(Sort.Direction.ASC, "createdDate")));

        List<BoardEntity> boardEntities = page.getContent();
        List<BoardDto> boardDtoList = new ArrayList<>();

        for (BoardEntity boardEntity : boardEntities) {
            boardDtoList.add(this.convertEntityToDto(boardEntity));
        }

        return boardDtoList;
    }

    @Transactional
    public Long getBoardCount() {
        return boardRepository.count();
    }

    public Integer[] getPageList(Integer curPageNum) {
        Integer[] pageList = new Integer[BLOCK_PAGE_NUM_COUNT];

// 총 게시글 갯수
        Double postsTotalCount = Double.valueOf(this.getBoardCount());

// 총 게시글 기준으로 계산한 마지막 페이지 번호 계산 (올림으로 계산)
        Integer totalLastPageNum = (int)(Math.ceil((postsTotalCount/PAGE_POST_COUNT)));

// 현재 페이지를 기준으로 블럭의 마지막 페이지 번호 계산
        Integer blockLastPageNum = (totalLastPageNum > curPageNum + BLOCK_PAGE_NUM_COUNT)
                ? curPageNum + BLOCK_PAGE_NUM_COUNT
                : totalLastPageNum;

// 페이지 시작 번호 조정
        curPageNum = (curPageNum <= 3) ? 1 : curPageNum - 2;

// 페이지 번호 할당
        for (int val = curPageNum, idx = 0; val <= blockLastPageNum; val++, idx++) {
            pageList[idx] = val;
        }

        return pageList;
    }

...
}
  • getBoardList() 메서드는 기존에 존재했던 메서드이며, 페이징을 할 수 있게 수정하였습니다.
    • boardRepository.findAll(PageRequest.of(pageNum - 1, PAGE_POST_COUNT, Sort.by(Sort.Direction.ASC, "createdDate")));
    • repository의 find() 관련 메서드를 호출할 때 Pageable 인터페이스를 구현한 클래스(PageRequest.of())를 전달하면 페이징을 할 수 있습니다. ( 참고 )
      • 첫 번째 인자
        • limit을 의미합니다.
        • "현재 페이지 번호 - 1"을 계산한 값이며, 실제 페이지 번호와 SQL 조회시 사용되는 limit은 다르기 때문입니다.
      • 두 번째 인자
        • offset을 의미합니다.
        • 몇 개를 가져올 것인가?
      • 세 번째 인자
        • 정렬 방식을 결정합니다.
        • createDate 컬럼을 기준으로 오름차순으로 정렬하여 가져옵니다.
    • 반환된 Page 객체의 getContent() 메서드를 호출하면, 엔티티를 리스트로 꺼낼 수 있습니다.
  • getBoardCount() 메서드는 신규로 추가한 메서드이며, 전체 게시글 개수를 가져옵니다.
  • getPageList() 메서드도 신규로 추가한 메서드이며, 프론트에 노출시킬 페이지 번호 리스트를 계산하는 로직입니다.
    • 하나의 페이지에는 4개의 게시글을 가져온다.
    • 총 5개의 번호를 노출한다.
    • 번호를 5개 채우지 못하면(= 게시글이 20개가 안되면), 존재하는 번호까지만 노출한다.
    • UI를 어떻게 구현하냐에 따라 페이징 로직이 조금씩 다른데요, 제가 구현한 방식은 다음과 같습니다.
 
페이징 알고리즘을 구현하는 부분이 복잡해서 그렇지, JPA에서 페이징하여 조회하는 부분은 간단합니다.
페이징은 각자가 원하는 UI가 있으므로 상황에 맞게 구현하시면 되고, 참고만 하시길 바랍니다.
  • 다양한 방법
    • 최초 첫 번째 블럭 페이지 번호 : 1 ~ 5
    • 오른쪽 버튼 클릭시, 두 번째 블럭 페이지 번호 : 6 ~ 10
    • 블럭 내의 번호 클릭 시, 번호 변동없음
    • 한 블럭에 페이지 번호는 5개로 고정하고, 양 끝에 화살표 버튼을 위치시켜 5개가 한 셋트씩 움직이도록 할 수도 있습니다.
    • 또 예전에 구현했던 페이징 알고리즘이 있는데, 여기도 참고하시면 좋을 것 같네요.
 
 
 
 
4) 페이징 - 테스트 
 
 
 
 
 
이상으로 검색과 페이징을 하는 방법에 대해 알아보았습니다.
이 글에서는 구현하지 않았지만, 검색한 목록에 대해서도 페이징 처리를 할 수가 있습니다.
URL query string으로 keyword, page 값을 동시에 넘겨주면 됩니다.
 
 
 
[참고 자료]