[Spring Data JPA] Page/Pageable로 페이징 처리

이지연·2026년 1월 28일

Spring Data JPA 페이징(Page, Pageable) 정리

게시글 목록, 상품 리스트처럼 “전체 데이터 중 일부만 잘라서 보여주는” 기능은 대부분의 서비스에서 필수다. Spring Data JPA에서는 일반적으로 PagePageable을 활용해서 페이징을 구현한다.

핵심은 간단하다. 클라이언트(프론트)가 page, size, sort 같은 조건을 쿼리 파라미터로 보내면, 백엔드는 그 조건에 맞는 “해당 구간 데이터 + 메타 정보”를 내려준다. 프론트는 응답에 포함된 totalPages 같은 정보를 이용해 페이지네이션 UI를 그리거나 다음 페이지를 요청한다.

  • okky커뮤니티의 페이징처리 예시

페이징이 동작하는 방식(슬라이싱 관점)

일반적인 페이지 기반 페이징은 전체 데이터에서 특정 구간을 슬라이싱해서 가져오는 구조다.

예를 들어 전체 데이터가 500개이고 page size를 10으로 두면:

  • page=0이면 0~9번째 데이터
  • page=2이면 20~29번째 데이터

즉, page=n일 때 가져오는 범위는 대략 아래처럼 이해하면 된다.

  • 시작 인덱스: (n \times size)
  • 끝 인덱스: (n \times size + (size - 1))

여기서 중요한 포인트는 Spring의 기본 page 값이 (0)부터 시작한다는 점이다.
(사용자에게 1페이지부터 보여주고 싶다면 프론트에서 1 기반으로 관리하고 서버 요청 시 0 기반으로 변환하거나, 서버에서 별도 처리를 한다.)


Pageable: 요청 파라미터를 캡슐화하는 입력 객체

Spring에서는 페이징 처리를 지원하는 기능으로 Pageable을 제공한다. Pageable은 클라이언트가 보낸 page, size, sort 요청 파라미터를 “페이징 조건 객체”로 바인딩해주는 역할을 한다.

  • page: 현재 페이지 번호(기본값 0)
  • size: 페이지당 데이터 수(기본값 20)
  • sort: 정렬 기준(기본값 없음)

즉, 컨트롤러 메소드 파라미터로 Pageable을 받기만 하면 스프링이 알아서 요청 파라미터를 매핑해준다.

요청 예시는 보통 이런 형태다.

  • /posts?page=0&size=5&sort=title,asc

Page: 조회 결과 + 메타 정보를 담는 출력 객체

Page<T>는 특정 페이지의 조회 결과를 “데이터(content) + 페이징 메타 정보”로 묶어서 내려주는 리턴 타입이다. 프론트 입장에서 중요한 이유는, 단순 목록뿐 아니라 페이지네이션 UI에 필요한 정보(총 페이지 수, 총 개수 등)를 함께 받을 수 있기 때문이다.

Page에서 자주 쓰는 값은 아래와 같다.

  • content: 실제 조회된 데이터 목록(List)
  • totalElements: 전체 데이터 수
  • totalPages: 전체 페이지 수
  • number: 현재 페이지 번호(0 기반)
  • size: 페이지당 데이터 수
  • first, last: 첫 페이지/마지막 페이지 여부
  • numberOfElements: 현재 페이지에 담긴 원소 수

컨트롤러 예시(@PageableDefault)

아래처럼 컨트롤러에서 Pageable을 파라미터로 받고, 서비스에서 그대로 넘겨 Page를 반환하는 방식이 가장 흔하다.

// 요청 예시: localhost:8080/posts?page=0&size=5&sort=title,asc
// Pageable 바인딩은 스프링이 자동으로 처리
@GetMapping("/posts")
public Page<PostListDto> postListDto(
        @PageableDefault(size = 10, sort = "id", direction = Sort.Direction.DESC)
        Pageable pageable
) {
    return postService.findAll(pageable);
}
  • @PageableDefault: page/size/sort 기본값을 지정
  • 클라이언트가 size, sort 등을 주면 기본값보다 요청 값이 우선 적용

리포지토리는 보통 아래처럼 Pageable을 받아 Page를 반환한다.

Page<Post> findAll(Pageable pageable);

실제 응답(JSON) 구조 예시

Page를 그대로 반환하면 Spring이 아래처럼 content와 메타 데이터를 포함한 JSON을 만들어준다(프로젝트 설정에 따라 필드 구성은 조금 다를 수 있음).

{
  "content": [
    {
      "id": 6,
      "category": "politics",
      "title": "tttttt",
      "authorEmail": "brad@naver.com"
    },
    {
      "id": 5,
      "category": "politics",
      "title": "tttttt",
      "authorEmail": "brad@naver.com"
    },
    {
      "id": 4,
      "category": "politics",
      "title": "tttttt",
      "authorEmail": "brad@naver.com"
    }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 5,
    "sort": {
      "empty": false,
      "unsorted": false,
      "sorted": true
    },
    "offset": 0,
    "unpaged": false,
    "paged": true
  },
  "last": true,
  "totalElements": 5,
  "totalPages": 1,
  "size": 5,
  "number": 0,
  "sort": {
    "empty": false,
    "unsorted": false,
    "sorted": true
  },
  "first": true,
  "numberOfElements": 5,
  "empty": false
}

프론트는 보통 다음처럼 활용한다.

  • content로 목록 렌더링
  • totalPages, number, first, last로 페이지네이션 UI 구성
  • 또는 무한 스크롤이라면 lasttrue일 때 추가 요청 중단

무한 스크롤과의 관계

무한 스크롤도 결국 “다음 페이지를 계속 요청해서 이어붙이는 방식”이라 백엔드 입장에서는 동일하다. 스크롤 이벤트 감지와 “언제 다음 페이지를 요청할지”는 프론트 책임이고, 백엔드는 page/size/sort 조건에 맞는 결과를 안정적으로 내려주는 역할에 집중하면 된다.

profile
Eazy하게

0개의 댓글