페이징이란 사용자가 어떤 데이터를 요청했을 때, 전체 데이터 중 일부를 원하는 정렬 옵션에 따라 제공하는 방식을 의미한다.
일반적인 시스템들은 수많은 데이터를 데이터베이스에 저장하고 있고, 모든 데이터를 한 번에 응답으로 보낼 경우, 클라이언트에게 적시에, 적절한 성능으로 데이터를 제공하기 어렵다. 따라서 대부분의 서비스들은 이 페이징 개념을 이용한 페이지네이션 기능을 제공한다.
클라이언트가 데이터를 얻기 위해 페이징 기법을 이용해 구현한 API에 요청을 보낸다면, 보통
동반되는 파라미터들은 다음과 같다.
page : 페이징 기법 적용시 원하는 페이지size : 한 페이지에 담겨져 올 데이터 개수sort : 정렬 기준예시 요청 URL : /members/orders?page=1&size=20&sort=id.DESC
Spring data JPA는 개발자가 이런 페이지네이션 기능을 편리하게 개발할 수 있도록 다음과 같은 기능들을 제공한다.
org.springframework.data.domain.Sort : 정렬 기능org.springframework.data.domain.Pageable : 페이징 기능(내부에 Sort 포함)org.springframework.data.domain.Page : 추가 COUNT 결과를 포함하는 페이징org.springframework.data.domain.Slice : 추가 COUNT 결과 없이 다음 페이지만 확인 가능List (자바 컬렉션) : 추가 COUNT 쿼리 없이 결과만 반환public interface Page<T> extends Slice<T>{
int getTotalPage(); // 전체 페이지 수
long getTotalElements(); // 전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); // 변환기
}
public interface Slice<T> extends Streamable<T> {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
페이징을 바탕으로 한 데이터 조회에 대한 요청은 PageRequest 라는 클래스로 치환하여 기능을 구현할 수 있다. Pageable 인터페이스를 구현할 구체 클래스의 형태로, 이 클래스를 사용하면 DB에서 가져올 데이터의 일부를 특정 페이지로 제한하고, 필요에 따라 정렬 옵션을 지정할 수 있다.
public class PageRequest extends AbstractPageRequest {
private static final long serialVersionUID = -4541509938956089562L;
private final Sort sort;
protected PageRequest(int page, int size, Sort sort) {
super(page, size);
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
}
public static PageRequest of(int page, int size) {
return of(page, size, Sort.unsorted());
}
public static PageRequest of(int page, int size, Sort sort) {
return new PageRequest(page, size, sort);
}
public static PageRequest of(int page, int size, Direction direction, String... properties) {
return of(page, size, Sort.by(direction, properties));
}
public static PageRequest ofSize(int pageSize) {
return PageRequest.of(0, pageSize);
}
@GetMapping("/members")
public ResponseEntity<MemberPageResponse> members(Pageable pageable) {
Page<Member> page = memberService.getMembers(pageable);
return ResponseEntity.ok(MemberConverter.toPageResponse(page));
}
Controller에서는 위 코드처럼 사용자가 보낸 페이징 정보를 Pageable 로 매핑하여 처리할 수 있다.
혹은 페이징 관련 요청 정보를 포함한 DTO로 매핑하고 Service에서 Pageable 로 치환하여 처리 가능
public Page<Member> getMembers(Pageable pageable) {
int page = (pageable.getPageNumber() == 0) ? 0 : (pageable.getPageNumber()-1);
int size = pageable.getPageSize();
PageRequest pageRequest = PageRequest.of(page, size);
return memberRepository.findAll(pageRequest);
}
Page는 기본적으로 인덱스가 0부터 시작하게 된다. 따라서 클라이언트에서 요청 받는 page 의 값과 관련된 검증이 별도로 존재하지 않으면 위와 같이 Service에서 별도로 PageRequest 를 생성하여 Repository에서 데이터를 조회하는 형식으로 로직을 작성할 수도 있다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findAll(Pageable pageable);
}
JpaRepository 인터페이스의 메서드 네이밍 기능을 이용하여 페이지네이션 기능을 구현할 때
반환형은 Page , Slice , List 가 가능하다.
만약 클라이언트가 page , size 혹은 sort 정보를 쿼리 스트링으로 넘겨주지 않는다면 어떤 일이 발생할까? 결론적으로 한 페이지에 20개의 사이즈로 분리된 페이지 중 첫 페이지를 반환한다.
내부 구현을 들여다 보면 PageableHandlerMethodArgumentResolverSupport 에 정의된 fallbackPageable 을 이용하고 있으며 이는 기본적으로 PageRequest.of(0, 20) 인 DEFAULT_PAGE_REQUEST 를 초기화되어 있다.

한편, @PageableDefault 어노테이션은 기본 페이징 및 정렬 옵션을 정하는데 사용되는데, 해당 어노테이션을 사용하며 별다른 옵션을 설정하지 않을 시 위 fallbackPageable 옵션이 사용된다.
@RestController
@RequiredArgsConstuructor
@RequestMapping("/member")
public class MemberController {
@GetMapping("/reviews")
public ResponseEntity<ReviewPageResponseDTO> memberReviews(
@PageableDefault Pageable pageable) {
Page<Review> reviews = memberService.getReviews(pageable);
ReviewPageResponseDTO response = ReviewConverter.toReviewPageResponse(reviews);
return ResponseEntity.ok(response);
}
}
default 설정을 변경하고 싶다면 아래와 같이 옵션을 줄 수 있다.
@GetMapping("/reviews")
public ResponseEntity<ReviewPageResponseDTO> memberReviews(
@PageableDefault(
page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC)
Pageable pageable) {
Page<Review> reviews = memberService.getReviews(pageable);
ReviewPageResponseDTO response = ReviewConverter.toReviewPageResponse(reviews);
return ResponseEntity.ok(response);
}
}
@PageableDefault 어노테이션에는 다양한 default 속성을 지원한다.
DefaultPage의 경우 개발자가 정한 기본 Page의 형식이다. 기타 설정을 해주지 않으면 FallbackPage 의 설정으로 실행된다. Fallback 의 경우, 적합한 방식이 없을 때, 만일을 대비해 만들어둔 설정이다. DefaultPage는 @PageableDefault 어노테이션을 통해 설정할 수 있다.