스프링 데이터 JPA에서 Pageable, Page 객체를 사용하여 페이징을 구현하는 방법을 공부해보았다.
Spring Data JPA를 쓰면 동적 페이징 쿼리가 쉬워진다. Paging 덕분에 페이지에 대해 고민하지 않고 핵심 비즈니스에 집중할 수 있도록 한다
org.springframework.data.domain.Sort
: 정렬 기능org.springframework.data.domain.Pageable
: 페이징 기능(내부에 sort 포함)org.springframework.data.domain.Page
: 전체 데이터 건수를 조회하는 count 쿼리 결과를 포함하는 페이징org.springframework.data.domain.Slice
: 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1조회)List
(자바 컬렉션): 추가 count 쿼리 없이 결과만 반환PageRequest
객체를 사용한다.PageRequest
생성자의 파라미터는 현재 페이지, 조회할 데이터 수, 정렬 정보를 파라미터로 사용할 수 있다.프로젝트 의존성, application,yml 설정 포스팅 링크
컨트롤러에서 Pageable 인터페이스 타입 파라미터를 받는다.
@GetMapping("/{category_name}")
public String readAllPost(@PathVariable(required = false) String category_name,
@RequestParam(required = false, defaultValue = "0", value = "page") int pageNo,
Pageable pageable,
Model model){
...
/** ========== 페이징 처리 ========== **/
/*
클라이언트 페이지에서 받은 pageNo 와 실제 접근 페이지는 다르다. Page 객체는 페이지가 0부터 시작
따라서 실제 접근 페이지는 pageNo - 1 처리해주어야 한다.
*/
pageNo = (pageNo == 0) ? 0 : (pageNo - 1);
Page<PostDto.ResponsePageDto> postPageList =
postService.getPageList(pageable, pageNo, category_name, orderCriteria); // 페이지 객체 생성
PageVo pageVo = postService.getPageInfo(postPageList, pageNo);
model.addAttribute("postPageList", postPageList);
model.addAttribute("pageNo", pageNo);
model.addAttribute("pageVo", pageVo);
public Page<PostDto.ResponsePageDto> getPageList(Pageable pageable, int pageNo, String category_name, String orderCriteria) {
/* 넘겨받은 orderCriteria 를 이용해 내림차순하여 Pageable 객체 반환 */
pageable = PageRequest.of(pageNo, PAGE_POST_COUNT, Sort.by(Sort.Direction.DESC, orderCriteria));
/* category_name에 해당하는 post 페이지 객체 반환 */
Page<Post> page = postRepository.findByCategory_Name(category_name, pageable);
/** 페이징 정보 반환 **/
@Override
public PageVo getPageInfo(Page<PostDto.ResponsePageDto> postPageList, int pageNo) {
int totalPage = postPageList.getTotalPages();
// 현재 페이지를 통해 현재 페이지 그룹의 시작 페이지를 구함
int startNumber = (int)((Math.floor(pageNo/PAGE_POST_COUNT)*PAGE_POST_COUNT)+1 <= totalPage ? (Math.floor(pageNo/PAGE_POST_COUNT)*PAGE_POST_COUNT)+1 : totalPage);
// 전체 페이지 수와 현재 페이지 그룹의 시작 페이지를 통해 현재 페이지 그룹의 마지막 페이지를 구함
int endNumber = (startNumber + PAGE_POST_COUNT-1 < totalPage ? startNumber + PAGE_POST_COUNT-1 : totalPage);
boolean hasPrev = postPageList.hasPrevious();
boolean hasNext = postPageList.hasNext();
/* 화면에는 원래 페이지 인덱스+1 로 출력됨을 주의 */
int prevIndex = postPageList.previousOrFirstPageable().getPageNumber()+1;
int nextIndex = postPageList.nextOrLastPageable().getPageNumber()+1;
return new PageVo(totalPage, startNumber, endNumber, hasPrev, hasNext, prevIndex, nextIndex);
}
// 현재 페이지를 통해 현재 페이지 그룹의 시작 페이지를 구함
int startNumber = (int)((Math.floor(pageNo/PAGE_POST_COUNT)*PAGE_POST_COUNT)+1 <= totalPage ? (Math.floor(pageNo/PAGE_POST_COUNT)*PAGE_POST_COUNT)+1 : totalPage);
// 전체 페이지 수와 현재 페이지 그룹의 시작 페이지를 통해 현재 페이지 그룹의 마지막 페이지를 구함
int endNumber = (startNumber + PAGE_POST_COUNT-1 < totalPage ? startNumber + PAGE_POST_COUNT-1 : totalPage);
<!-- 페이지 영역 -->
<nav th:if="${pageVo.totalPage != 0}">
<div class="container">
<ul class="pagination pagination-primary m-4">
<li class="page-item ">
<!-- 첫 페이지로 이동 -->
<a class="page-link" th:href="@{/community/post/{category_name}(category_name = ${category_name}, page=1)}" aria-level="First">
<span aria-hidden="true"><i class="fa fa-angle-double-left" aria-hidden="true"></i></span>
</a>
</li>
<li class="page-item active">
<!-- 이전 페이지 -->
<li th:if="${pageVo.hasPrev} ? 'disabled'">
<a class="page-link" th:href="@{/community/post/{category_name}(category_name = ${category_name}, page=${pageVo.prevIndex})}" aria-level="Previous">‹</a>
<span aria-hidden="true"></span>
<li>
</li>
<!-- 페이지 번호 -->
<li th:each="page: ${#numbers.sequence(pageVo.startNumber, pageVo.endNumber)}"
th:class="(page == ${pageNo}+1) ? 'page-item active'">
<a class="page-link" th:text="${page}"
th:href="@{/community/post/{category_name}(category_name = ${category_name}, page=${page})}"></a>
</li>
<li class="page-item">
<!-- 다음 페이지 -->
<li th:if="${pageVo.hasNext} ? 'disabled'">
<a class="page-link" th:href="@{/community/post/{category_name}(category_name = ${category_name}, page=${pageVo.nextIndex})}" aria-level="Next">›</a>
<span aria-hidden="true"></span>
</li>
</li>
<li class="page-item">
<!-- 마지막 페이지 -->
<a class="page-link" th:href="@{/community/post/{category_name}(category_name = ${category_name}, page=${pageVo.totalPage})}" aria-level="Last">
<span aria-hidden="true"><i class="fa fa-angle-double-right" aria-hidden="true"></i></span>
</a>
</li>
</ul>
</div>
</nav>
public interface Slice<T> extends Streamable<T> {
int getNumber(); // 현재 페이지
int getSize(); // 페이지 크기
...
Pageable getPageable();
Pageable nextPageable();
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
Slice와 Page에 대해 간단하게 알아보자면,
출처
https://ivvve.github.io/2019/01/13/java/Spring/pagination_4/
https://velog.io/@dltkdgns3435/SpringBoot-Spring-Data-JPA-%EC%97%90%EC%84%9C-Page%EC%99%80-Slice
김영한 - 스프링 데이터 JPA
https://devlog-wjdrbs96.tistory.com/414
https://dev-monkey-dugi.tistory.com/34?category=943027