페이징/페이지네이션 - 스프링 데이터 JPA Pageable , Page

jyleever·2022년 6월 3일
1

스프링 데이터 JPA에서 Pageable, Page 객체를 사용하여 페이징을 구현하는 방법을 공부해보았다.

  • 페이지네이션
    • 한 번에 모든 데이터를 요청하는 게 아니라 사용자가 볼 만큼만 서버에 요청하면서 서버에 부하를 줄이도록 하는 기술

Spring Data JPA를 쓰면 동적 페이징 쿼리가 쉬워진다. Paging 덕분에 페이지에 대해 고민하지 않고 핵심 비즈니스에 집중할 수 있도록 한다

  • 스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 파라미터를 제공한다
    • org.springframework.data.domain.Sort : 정렬 기능
    • org.springframework.data.domain.Pageable : 페이징 기능(내부에 sort 포함)
  • Pageable을 사용할 때 반환 타입
    • org.springframework.data.domain.Page : 전체 데이터 건수를 조회하는 count 쿼리 결과를 포함하는 페이징
    • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1조회)
    • List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

Pageable

  • Pageable은 인터페이스이므로 실제로 사용할 때에는 인터페이스를 구현한 PageRequest 객체를 사용한다.
    그렇게 파라미터로 넘기면 반환 타입에 따라 쿼리가 날아가고 totalCount 를 날릴지 안 날릴지도 결정됨
  • PageRequest 생성자의 파라미터는 현재 페이지, 조회할 데이터 수, 정렬 정보를 파라미터로 사용할 수 있다.
  • 이 때, 페이지 index는 0부터 시작한다.
  • build.gradle에 jpa 의존성을 추가하고 application.yml에 JPA 관련 설정, 데이터베이스 설정은 본 포스팅에서는 생략하겠다.

프로젝트 의존성, application,yml 설정 포스팅 링크

Controller

컨트롤러에서 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);
  • 화면에서 pageNo 변수를 통해 페이지 번호를 받는다
  • 클라이언트 페이지에서 받은 pageNo 와 실제 접근 페이지는 다르다. 실제 사용자는 페이지를 선택할 때 1, 2, 3,.. 이렇게 선택한다. 하지만 Page 객체는 페이지가 0부터 시작한다.
    따라서 실제 접근 페이지는 pageNo - 1 처리해주어야 한다.
  • 페이징 처리의 책임은 서비스단으로 넘겼다.

Service

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);
  • PageRequest의 of 메소드에 페이지 번호, 데이터 수, 정렬 기준을 넘겨 Pageable 객체를 반환한다.
  • JpaRepository를 사용하여 쿼리 메서드에 Pageable 인터페이스로 파라미터를 넘기면 페이징을 사용할 수 있다. 그리고 반환 타입은 Page 인터페이스다.
    /** 페이징 정보 반환 **/
    @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);
  • 현제 페이지를 통해 현재 페이지 그룹의 시작 페이지를 구하는 로직
  • 전체 페이지 수와 현재 페이지 그룹의 시작 페이지를 통해 현재 페이지 그룹의 마지막 페이지를 구하는 로직

view

        <!-- 페이지 영역 -->
        <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">&lsaquo;</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">&rsaquo;</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>


Page

  • Page 인터페이스는 페이징을 구현할 때 필요한 값들을 getTotalPages(), getTotalElements()와 같은 메소드로 추상화 시켜놓은 인터페이스다.
  • Page 인터페이스는 Slice 인터페이스를 상속받고 있으며 Slice 인터페이스 내부를 살펴보면 다음과 같다.
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에 대해 간단하게 알아보자면,

  • page
    • 조회 쿼리 이후 전체 데이터 개수를 한번 더 조회하는 카운트 쿼리가 실행된다.
    • 게시판과 같이 총 데이터 개수가 필요할 때 사용하면 좋다.
  • Slice
    • slice는 limit(size)+1 된 값을 조회한다.
    • slice는 카운트쿼리가 나가지 않고 다음 slice가 존재하는지 여부만 확인하기 때문에 데이터 양이 많으면 slice를 사용하는것이 성능 상 유리하다.
    • 총 데이터 개수가 필요하지 않을 때, 다음 페이지 여부 확인(최근 모바일 리스트 생각해보면 됨, 다음 것을 미리 로딩, 무한 스크롤 등)에 사용된다.

출처
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

0개의 댓글