📄 Pageable로 페이징 처리 쉽게 구현하기 (+Spring Data JPA)
블로그나 커뮤니티 게시판처럼 많은 데이터를 다루는 서비스에서 모든 데이터를 한 번에 내려주는 것은 비효율적일 뿐만 아니라 사용자 경험까지 해칠 수 있다.
서버 입장에서는 과도한 부하가 발생하고 클라이언트 브라우저는 불필요한 네트워크 낭비와 함께 렌더링 시간 지연을 겪게 된다. 이런 문제를 해결하는 대표적인 방법이 바로 페이징 처리(Pagination)이다.
백엔드에서 데이터를 일정 단위로 나누어 전달하면 클라이언트는 필요한 페이지만 요청하고 효율적으로 화면에 데이터를 표시할 수 있다.
Spring Boot에서는 Pageable을 활용해 매우 간단하게 페이징 처리를 구현할 수 있다.
먼저 컨트롤러에서 클라이언트의 페이징 요청을 받는다.
@GetMapping
public PageResponse<PostListResponse> getAllPosts(
@PageableDefault(page = 0, size = 10) Pageable pageable) {
Page<PostListResponse> page = userPostService.getAllPosts(pageable)
.map(PostListResponse::from);
return PageResponse.from(page);
}
Spring Boot에서 페이징 처리를 할 때 Pageable을 파라미터로 받으면 클라이언트가 쿼리 파라미터로 전달한 page, size, sort 값을 자동으로 매핑해준다.
예를 들어 /posts?page=0&size=10 이라는 요청이 들어오면 Spring은 해당 값을 읽어 Pageable 객체로 만들어준다. 하지만 클라이언트가 아무런 값을 전달하지 않는 경우를 대비해 기본값을 지정해줄 수 있는 어노테이션이 @PageableDefault이다.
@PageableDefault에는 page(페이지 번호), size(페이지 크기), sort(정렬 필드), direction(정렬 방향)의 4가지 파라미터를 전달할 수 있다.
응답으로 Page<T>를 그대로 반환할 수도 있지만 불필요한 정보를 제거하고 프론트엔드에 필요한 데이터만 깔끔하게 전달하기 위해 PageResponse<T>라는 별도의 응답 객체를 정의해 사용하였다.
이후에는 전달받은 Pageable 객체를 가지고 서비스를 구현한다.
public Page<PostListDto> getAllPosts(Pageable pageable) {
Page<Post> page = postRepository.findVisiblePosts(pageable);
return page.map(post -> PostListDto.of(post);
}
findVisiblePosts()는 레포지토리에 @Query를 사용하여 직접 작성한 커스텀 JPA 쿼리 메서드이다.
@Query("""
SELECT p FROM Post p
WHERE p.isPublic = true AND p.isDeleted = false
ORDER BY p.createdAt DESC
""")
Page<Post> findVisiblePosts(Pageable pageable);
JPA는 Pageable 파라미터를 통해 페이징 처리를 자동으로 지원하며 실제로 실행되는 SLQ(PostgreSQL 기준)은 다음과 같다.
select
p1_0.id,
p1_0.title,
p1_0.slug,
p1_0.content,
p1_0.is_public,
p1_0.is_deleted,
p1_0.created_at,
p1_0.updated_at
from
post p1_0
where
p1_0.is_public=true
and p1_0.is_deleted=false
order by
p1_0.created_at desc
fetch
first ? rows only
fetch first ? rows only 구문의 경우 PostgreSQL에서 사용하는 표준 SQL 페이징 문법으로 MySQL의 LIMIT ? 과 동일한 역할을 한다. 즉 앞에서부터 ?개의 행만 가져오라는 의미이다.
또한 첫 페이지를 요청하는 경우 OFFSET은 생략되어 실행되지 않는다. JPA는 이러한 페이징 구문을 Pageable 객체를 통해 자동으로 생성해주기 때문에 SQL을 직접 작성하지 않고도 효율적인 페이징 처리를 구현할 수 있다.