웹 사이트를 이용하며 게시판을 둘러볼 때, 게시글 목록 하단에 게시글을 일정 수로 나누어 페이지화 시켜놓은걸 볼 수 있는데 이를 Pagination(페이지네이션) 이라고 합니다.
💡 직접 구현해본 pagination 예제
직관적으로 게시글을 검색할 수 있기 때문에 커뮤니티뿐 아니라 많은 게시글을 보여주는 곳에서 구현 및 사용하고 있습니다.
pagination을 구현하기 위해서는 현재 페이지번호, 각 페이지별 보여질 내용의 수, 페이지에 들어갈 컨텐츠, 전체 페이지 개수, 전체 데이터의 수가 필요합니다.
이 데이터들을 얻으려면 최소 2번의 API 요청(데이터 요청, 데이터 카운트 콜)을 통해 데이터를 가져와야 합니다. 2번의 요청이 싫다면, 한번의 요청으로 모든 데이터를 가져와야 하는데 이 경우에는 데이터가 매우 많을 경우에 성능 이슈가 생길 수 있습니다.
Spring 프레임워크에서는 이러한 고민을 해결 할 수 있는 방법 중 하나가, Spring-Data-JPA
라이브러리의 Page
와 Pageable
을 이용하는 것입니다.
Pageable은 Spring JPA에서 DB 쿼리에 쉽고 유연하게 limit 쿼리를 사용할 수 있게 해준다. 특히 JPA를 사용할 때, 자동으로 Pageable 타입의 변수를 넘겨주면 JPA가 DB에 접근해 데이터를 가져올 때 자동으로 limit 조건을 붙여 데이터를 가져온다.
Spring의 Controller의 메서드 파라미터에 해당 어노테이션이 선언 된 Pageable 타입 파라미터를 선언하게 되면, API 요청 시 Pageable 객체에 대한 파라미터를 넘겨주지 않아도 자동으로 기본값을 가진 Pageable 타입 파라미터를 제공해준다.
이 어노테이션을 사용하지 않는다면 HandlerMethodArgumentResolver
같은 인터페이스를 구현한 클래스를 만들어 조건에 맞게 매핑을 해줘야 한다.
Pageble을 파라미터로하여 가져온 결과물은 Page 형태로 반환 되며, Page를 사용한다면 대부분 다수의 row를 가져오기 때문에 Page<List>의 형태로 반환을 한다. 이 페이지 객체에는 Pagination을 구현할 때 사용하면 좋은 메서드가 있으며 이는 다음과 같다.
getTotalElements()
쿼리 결과물의 전체 데이터 개수이다. 즉, Pageable에 의해 limit키워드가 조건으로 들어가지 않는 쿼리 결과의 수 인데, 주의해야 할 점은 쿼리 결과의 갯수만 가져오지 전체 데이터를 가져오지 않는다는 점이다.
이 메서드는 게시판 기능 사용자에게 전체 데이터 개수를 알려주는 등에 사용하기 좋다.
getTotalPages()
쿼리를 통해 가져온 요소들을 size크기에 맞춰 페이징하였을 때 나오는 총 페이지의 갯수이다.
이를 활용해 쉽게 페이지 버튼의 생성이 가능하다.
getSize()
쿼리를 수행한 전체 데이터에 대해 일정 수 만큼 나눠 페이지를 구성하는데, 이 일정 수의 크기이다.
getNumber()
요소를 가져온 페이지의 번호를 의미한다.
getNumberOfElements()
페이지에 존재하는 요소의 개수이다. 최대 size의 수 만큼 나올 수 있다.
public interface PostsRepository extends JpaRepository<Posts, Long> {
@Query("SELECT p FROM Posts p ORDER BY p.id DESC")
List<Posts> findAllDesc();
Page<Posts> findByDeletedOrderByIdDesc(boolean nonDeleted, Pageable pagaeble);
}
JPARepository를 상속받아 repository를 구현.
findByDeletedOrderByIdDesc(boolean nonDeleted, Pageable pagaeble);
💬 삭제된 데이터를 제외하고 Id를 기준으로 내림차순으로(최신순) 모든 정보를 가져온다.
매개변수로 Pageable 타입의 객체를 넘겨주면, 객체의 정보를 읽고 page 조건을 설정하여 데이터를 가져온다.
public Page<Posts> findPosts(int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size);
return postsRepository.findByDeletedOrderByIdDesc(false, pageRequest);
}
page 개수와 size(page별 데이터 개수)를 매개변수로 받아 PageRequest
객체를 생성하여 repository로 반환한다.
PageRequest는 Pageable을 구현하며, Sort
객체를 매개변수로 받아 정렬조건을 설정할수도 있다.
@GetMapping("api/v1/postslist")
@Operation(summary = "게시물 목록", description = "게시물 목록 정보를 요청합니다.", tags = {"Posts"})
public ResponseEntity getPostsList(@Positive @RequestParam int page, @Positive @RequestParam int size) {
Page<Posts> postsPage = postsServiceImpl.findPosts(page-1, size);
PageInfo pageInfo = new PageInfo(page, size, (int) postsPage.getTotalElements(), (int) postsPage.getTotalPages());
List<Posts> posts = postsPage.getContent();
List<PostsListResponse> postsListResponses = posts.stream().map(PostsListResponse::new).collect(Collectors.toList());
return new ResponseEntity<>(new AllPageInfo(postsListResponses, pageInfo), HttpStatus.OK);
}
page와 size를 query parameter로 받는다.
PostsServiceImpl에서 반환한 Page객체(memberPage)에서 제공하는 페이지 개수, 페이지 당 데이터 개수, 총 데이터 수 등의 정보를 이용해 페이지 정보를 담고있는 PageInfo 객체를 생성했다. (page는 0부터 시작이기 때문에 page-1)
@Getter
@AllArgsConstructor
public class PageInfo {
private int page;
private int size;
private int totalElements;
private int totalPages;
}
Posts 반환 + dto로 변환
이후 Page객체의 getContent를 호출해 실제 데이터를 받아오고, Dto 객체로 변환한다.*
@Getter
public class AllPageInfo<T> {
private T data;
private PageInfo pageInfo;
public AllPageInfo(T data, PageInfo pageInfo) {
this.data = data;
this.pageInfo = pageInfo;
}
}
@Getter
public class PostsListResponse {
private Long id;
private String title;
private String author;
public PostsListResponse(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
}
}
전체 posts
와 page정보
를 함께 반환하는 별도의 Dto클래스(AllPageInfo) 생성 후, postsListResponses와 pageInfo 객체를 Dto로 반환한다.
요청 URI 형식 : http://localhost:8081/api/v1/posts?page=1,size=20
https://velog.io/@albaneo0724/Spring-Pagination%EA%B3%BC-Page-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Pageable
https://velog.io/@bagt/0704-Spring-Pagination-API-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98