
페이지네이션(Pagination)은 대량의 데이터를 여러 페이지로 나누어 보여주는 기법입니다.
→ 페이지당 10개씩
→ 총 5페이지
SELECT * FROM post ORDER BY created_at DESC LIMIT 10 OFFSET 0;-- 2페이지 (10~19번)
SELECT * FROM post ORDER BY created_at DESC LIMIT 10 OFFSET 10;
OFFSET = 페이지 번호 × 페이지 크기
-- 마지막 조회 ID 기준
WHERE id < :lastId ORDER BY id DESC LIMIT 10
Spring Data JPA는 페이지네이션을 위한 강력한 기능을 제공합니다.
페이징 정보를 담는 인터페이스로 페이지 번호, 크기, 정렬 정보를 포함합니다.
Pageable pageable = PageRequest.of(
0, // page: 페이지 번호 (0부터 시작)
10, // size: 페이지당 크기
Sort.by(Sort.Direction.DESC, "createdAt") // 정렬
);
페이징 결과를 담는 인터페이스로 데이터 + 페이징 메타데이터를 포함합니다.
Page page = postRepository.findAll(pageable);
// 제공 메서드
page.getContent(); // 실제 데이터 List
page.getTotalElements(); // 전체 데이터 개수
page.getTotalPages(); // 전체 페이지 수
page.getNumber(); // 현재 페이지 번호
page.getSize(); // 페이지 크기
page.isFirst(); // 첫 페이지 여부
page.isLast(); // 마지막 페이지 여부
public interface PostRepository extends JpaRepository {
// Page findAll(Pageable pageable)이 이미 제공
}
클라이언트에게 페이징 정보를 함께 전달하기 위한 응답 DTO입니다.
파일 위치: post/dto/PageResponse.java
package org.example.jinuweb.post.dto;
import lombok.Builder;
import lombok.Getter;
import org.springframework.data.domain.Page;
import java.util.List;
@Getter
@Builder
public class PageResponse {
private List content; // 실제 데이터 목록
private int currentPage; // 현재 페이지 번호
private int totalPages; // 전체 페이지 수
private long totalElements; // 전체 데이터 개수
private int size; // 페이지당 크기
private boolean first; // 첫 페이지 여부
private boolean last; // 마지막 페이지 여부
// Page 객체를 PageResponse로 변환하는 정적 팩토리 메서드
public static PageResponse of(Page page) {
return PageResponse.builder()
.content(page.getContent())
.currentPage(page.getNumber())
.totalPages(page.getTotalPages())
.totalElements(page.getTotalElements())
.size(page.getSize())
.first(page.isFirst())
.last(page.isLast())
.build();
}
}
<T> 사용으로 다양한 엔티티에 재사용 가능of()로 Spring의 Page 객체를 쉽게 변환파일: post/repository/PostRepository.java
package org.example.jinuweb.post.repository;
import org.example.jinuweb.post.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository {
// JpaRepository가 Page findAll(Pageable pageable)을 자동 제공
}
JpaRepository를 상속받으면 findAll(Pageable pageable) 메서드가 자동으로 제공됩니다파일: post/service/PostService.java
package org.example.jinuweb.post.service;
import lombok.RequiredArgsConstructor;
import org.example.jinuweb.post.dto.PageResponse;
import org.example.jinuweb.post.dto.PostResponse;
import org.example.jinuweb.post.entity.Post;
import org.example.jinuweb.post.repository.PostRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
/**
* 페이징 처리된 게시글 목록 조회
* @param page 페이지 번호 (0부터 시작)
* @param size 페이지당 크기
* @param sort 정렬 조건
* @return 페이징된 게시글 응답
*/
public PageResponse findAll(int page, int size, String sort) {
// 1. Sort 파싱
String[] sortParams = sort.split(",");
Sort.Direction direction = Sort.Direction.fromString(sortParams[1]);
Sort sortBy = Sort.by(direction, sortParams[0]);
// 2. Pageable 객체 생성
Pageable pageable = PageRequest.of(page, size, sortBy);
// 3. Repository에서 페이징 조회
Page postPage = postRepository.findAll(pageable);
// 4. Post 엔티티를 PostResponse DTO로 변환
Page responsePage = postPage.map(PostResponse::from);
// 5. PageResponse로 변환하여 반환
return PageResponse.of(responsePage);
}
}
1. Sort 파싱
String[] sortParams = sort.split(",");
Sort.Direction direction = Sort.Direction.fromString(sortParams[1]);
Sort sortBy = Sort.by(direction, sortParams[0]);
Sort.by()로 JPA가 이해할 수 있는 정렬 객체 생성2. Pageable 생성
Pageable pageable = PageRequest.of(page, size, sortBy);
PageRequest는 Pageable 인터페이스의 구현체OFFSET = page × size 계산3. 데이터 조회
Page postPage = postRepository.findAll(pageable);
SELECT ... LIMIT 10 OFFSET 0 SQL 생성Page 객체에 데이터 + 메타데이터가 함께 반환됨4. DTO 변환
Page responsePage = postPage.map(PostResponse::from);
Page.map() 메서드로 엔티티를 DTO로 변환5. 응답 생성
return PageResponse.of(responsePage);
파일: post/controller/PostController.java
package org.example.jinuweb.post.controller;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.example.jinuweb.post.dto.PageResponse;
import org.example.jinuweb.post.dto.PostResponse;
import org.example.jinuweb.post.service.PostService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/posts")
public class PostController {
private final PostService postService;
/**
* 게시글 전체 조회 (페이징)
* @param page 페이지 번호 (기본값: 0)
* @param size 페이지 크기 (기본값: 10)
* @param sort 정렬 조건
*/
@Operation(summary = "게시글 전체 조회")
@GetMapping
public ResponseEntity<PageResponse> findAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt,desc") String sort
) {
PageResponse response = postService.findAll(page, size, sort);
return ResponseEntity.ok(response);
}
}
defaultValue = "0": 파라미터를 전달하지 않으면 첫 페이지 조회defaultValue = "10": 기본 페이지 크기는 10개defaultValue = "createdAt,desc": 기본 정렬은 최신순전체 동작 흐름을 순서대로 정리하면:
1. Client: GET /api/posts?page=1&size=10&sort=createdAt,desc
2. Controller: 요청 파라미터를 받아 Service 호출
→ postService.findAll(1, 10, "createdAt,desc")
3. Service:
- Sort 파싱: "createdAt,desc" → Sort 객체
- Pageable 생성: PageRequest.of(1, 10, sort)
- Repository 호출: postRepository.findAll(pageable)
4. Repository (JPA):
- SQL 자동 생성: SELECT * FROM post ORDER BY created_at DESC LIMIT 10 OFFSET 10
- DB 조회 후 Page<Post> 반환
5. Service:
- Page<Post> → Page<PostResponse> 변환 (map 사용)
- Page<PostResponse> → PageResponse<PostResponse> 변환
6. Controller: ResponseEntity로 감싸서 클라이언트에 응답
1. 첫 페이지 조회 (기본값 사용)
GET http://localhost:8080/api/posts
2. 2페이지 조회
GET http://localhost:8080/api/posts?page=1&size=10
3. 제목 오름차순 정렬
GET http://localhost:8080/api/posts?page=0&size=10&sort=title,asc
{
"content": [
{
"id": 47,
"title": "최신 게시글",
"content": "내용...",
"writer": "작성자",
"createdAt": "2024-12-18T10:30:00",
"updatedAt": "2024-12-18T10:30:00"
}
// ... 9개 더
],
"currentPage": 0,
"totalPages": 5,
"totalElements": 47,
"size": 10,
"first": true,
"last": false
}
content: 실제 게시글 데이터 배열currentPage: 현재 페이지 번호 (0부터 시작)totalPages: 전체 페이지 수 (5페이지)totalElements: 전체 게시글 개수 (47개)size: 페이지당 크기 (10개)first: 첫 페이지 여부 (true)last: 마지막 페이지 여부 (false)application.yml에 다음 설정을 추가하면 실제 실행되는 SQL을 확인할 수 있습니다:
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
실행 결과:
-- 데이터 조회
SELECT
p.id, p.title, p.content, p.writer, p.created_at, p.updated_at
FROM
post p
ORDER BY
p.created_at DESC
LIMIT 10 OFFSET 10;
-- 전체 개수 조회 (totalElements 계산용)
SELECT
COUNT(p.id)
FROM
post p;
page=0 → 1페이지
page=1 → 2페이지
page=2 → 3페이지
Pageable pageable = PageRequest.of(0, 10, sort);
// 생성 후 수정 불가, 새로 생성해야 함