현재 2개의 프로젝트 진행하고 있다.
두 프로젝트 모두 글 또는 영상을 페이징해서 프론트에 뿌려주는 API가 필요했다.
그래서 이번에 PageRequest를 사용해 페이징 하는 것을 공부했고 공유하고자 한다.
사용해보면서 느끼고 배운 지식이라 잘못된 내용이 있을 수 있습니다.
피드백 부탁드립니다.

JPA Paging이란?

DB에 저장된 Entity들을 페이지로 나누는 것이다.
예를들어, DB에 책이 20권 저장되어있다.
프론트에서 "DB에 있는 책을 5권씩 분류해서, 두 번째 파트를 줘!" 라고 요청한다.
그러면 백엔드에서는 5권씩 분류하고, 분류된 책들의 두 번째 파트를 프론트에게 넘겨준다.
위 상황과 같이, 일정 갯수만큼 분류하고, 분류된 부분들 중 어떤 부분을 보내주는 것이 JPA Paging이다.

어떻게?

사용법은 매우 간단하다.
repository의 findAll 메서드의 parameter에 Pageable 또는 Pageable의 구현체인 PageRequest를 넣어주면 된다.
인터넷을 찾아보면 Controller에서 Pageable을 받아서 사용하는 코드들이 있는데 따라서 해보니 안된다.....
그래서 내가 이번에 구현한 PageRequest를 사용하는 방법만 소개한다.

PageRequest란

몇 페이지, 한 페이지의 사이즈, Sorting 방법(Option)을 가지고,
Repository에 Paging을 요청할 때 사용하는 것

PageRequest의 구조

image.png
위와 같이 인터페이스인 Pageable과 Serializable을 implements하는 AbstractPageRequest라는 추상 클래스가 있다.
그리고 PageRequest class는 이 AbstractPageRequest를 상속한다.

PageRequest의 생성자

PageRequest의 생성에는 찾을 page와 한 페이지의 size를 필수 인자로 받는다.
그리고 정렬해서 paging을 하는 경우에, Sort를 생성자 인자로 추가해서 PageRequest를 생성할 수 있다.
아래는 PageRequest class의 생성자 코드.
현재 다른 생성자들은 deprecated되고,
가장 아래 코드에서 보이는 정적 팩토리 메서드로 생성해야 한다. (of 메서드)

image.png

image.png

image.png

image.png

PageRequest의 사용

image.png

Repository에서 findAll 메서드를 살펴보면 위와 같이 Pageable을 인자로 줄 수 있다.
PageRequest는 Pageable 클래스를 implements한 AbstractPageReqeust 추상 클래스의 구현체이므로 findAll의 인자로 넣을 수 있다.

따라서 page,size,sort(option)으로 PageRequest를 생성하고,
Repository의 findAll 메서드의 인자에 PageRequest를 넣어주면 된다.
그러면 반환은 Page<Entity>이 된다.

사용 예시

코드를 보면서 알아보자.
다른 복잡한 것들은 생략하고 설명에 필요한 구조는 다음과 같다.

Package - Class Name

controller - BookController
service - BookService
repository - BookRepository
domain - Book

<Situation>
몇 번째 페이지인지 의미하는 page와 한 페이지의 사이즈를 의미하는 size를 controller에 주고 해당 페이지의 Book들을 가져오는 API 구현하기
/paging URL에 RequestParam으로 pagesize가 주어짐

// BookController class
@GetMapping("/paging")
    public List<Book> findBooksByPageRequest(@RequestParam Integer page, Integer size) {
        return bookService.findBooksByPageRequest(page,size);
    }
  • BookController에서는 BookService에게 요청
// BookService class
public List<Book> findBooksByPageRequest(Integer page, Integer size) {
        PageRequest pageRequest = PageRequest.of(page,size);
        return bookRepository.findAll(pageRequest).getContent();
}
  • BookService class에서는 주어진 page, size로 PageRequest를 생성한다.
    (정적 팩토리 메서드를 사용해야 함)
  • 생성한 PageRequest을 BookRepository의 findAll 메서드 인자로 준다.
  • 반환은 List로 하기위해 getContent를 사용한다.
    (getContent를 사용하지 않으면 Page<Book>이 findAll(pageRequest)의 반환 타입이다.

만약 Entity가 생성된 시간의 역순으로 Sorting해서 Paging하고 싶다면?

// BookService class
public List<Book> findBooksByPageRequest(Integer page, Integer size) {
        PageRequest pageRequest = PageRequest.of(page, size, Sort.by("createdAt").descending());
        return bookRepository.findAll(pageRequest).getContent();
}
  • PageRequest의 마지막 인자로 Sort를 추가하면 된다.
  • 작성자는 Book을 추가한 날짜를 Book에 createdAt 이라고 했기 때문에 Sort.by의 인자에 "createdAt"을 넣었다.
  • 그리고 최신순으로 Paging해야 하므로 내림차순으로 정렬해야한다. 따라서 descending을 해줬다.


  (2019. 8.25 추가 내용)

Pagerequest를 컨트롤러에서 인자로 전달 받기

위의 내용대로 page, size을 컨트롤러에서 인자로 받도록 구현했더니 다음과 같은 피드백이 왔다.

image.png< 우아한 형제들 개발자 강현구님의 피드백>
(피드백 받은 코드는 다른 프로젝트에서 똑같이 jpa paging한 것)

내가 했던 방법의 문제점

  • 정렬하기 위한 column이나 정렬 방법이 바뀌면 service layer에 새로운 메서드를 계속 추가해야 한다.
  • 응답으로 List형을 반환하기 때문에 클라이언트에서 추가적인 내용을 알 수 없다.(예를들어, 총 페이지 수, 첫 페이지, 마지막 페이지 등의 정보)

위와 같은 문제점이 있다는 것을 피드백 받고 PageRequest를 클라이언트에서 전달 받도록 수정했다.

수정한 코드

  // BookController class
  @GetMapping("/paging")
  public Page<Book> findBooksByPageRequest(final Pageable pageable) {
        return bookService.findBooksByPageRequest(pageable);
  }
  // BookService class
  public Page<Book> findBooksByPageRequest(Pageable pageable) {
     return bookRepository.findAll(pageable);
   }

직접 써보면서, 해당 클래스들(PageRequest, Pageable 등등)에 직접 들어가보면서 알아본 내용들을 토대로 했기 때문에 내용이 부실할 수 있습니다.
본 프로젝트에는 ResponseEntity와 DTO를 사용했지만, 보기 쉽도록 생략했음을 알려드립니다.
궁금한 점, 피드백 언제나 환영합니다!

이미지 참고 : http://www.shangyan.site/2018-02-13-spring-boot-jpa-2/