Spring Data JPA 의 Pageable 이용한 Pagination(페이징 처리)
1. JPA Pageable
Spring Data 의 Pageable 은 페이지네이션(페이징 처리)을 위한 인터페이스로, 대량의 데이터를 페이지 단위로 나누어 클라이언트에 제공할 수 있도록 돕는다.
JpaRepository 가 기본으로 제공하는 메소드로,
List<T> findAll();
위의 예시가 T 객체를 전부 찾아 List 로 반환하는 메소드라면
Page<T> findAll(Pageable pageable);
T 테이블에서 한 페이지의 레코드들만 조회하여 리턴한다.
구체적으로,
Pageable 객체에 담겨있는 페이지 번호와 정렬순서로, 한 페이지의 레코드들만 조회한다.
라고 할 수 있다.
2. Pagination 구현 방법
Pagination 구현은 Pageable 인터페이스의 자식 클래스인 PageRequest 를 사용한다.
Pageable 구현 방법
PageRequest pageRequest = PageRequest.of(페이지번호, 페이지크기, 정렬순서방향, 정렬기준속성명...) // 객체 생성
repository.findAll(pageRequest); // findAll 메소드 사용 시 파라미터에 넣기
① 원하는 페이지 속성을 넣어 PageRequest 객체를 생성
② repository 에 findAll 메소드를 사용할 때 PageRequest 를 파라미터로 넘겨주기
▶ PageRequest 객체 생성
PageRequest pageRequest = PageRequest.of(페이지번호, 페이지크기, 정렬순서방향, 정렬기준속성명...)
// PageRequest.of(page, size, sort)
PageRequest 클래스의 of 메소드는 PageRequest 객체를 생성하여 리턴한다.
of 메소드는 static 메소드 이기 대문에 enw 없이 클래스 이름으로 직접 호출할 수 있다.
of() 가 new PageRequest(...) 를 대신하는 정적 메소드인 것이다.
PageRequest 클래스 속성 |
설명 |
페이지 번호 (page) | 조회할 페이지 번호 (페이지 번호는 0부터 시작) |
페이지 크기 (size) | 한 페이지의 레코드 수 |
정렬 순서 방향 (sort) | Sort.Direction.ASC (오름차순) Sort Direction.DESC (내림차순) 둘 중 하나 |
정렬기준 속성명 | 정렬 기준이 될 엔티티 속성 이름을 문자열로 작성 |
3. Pageable 사용 예시
① PageRequest 객체를 생성
PageRequest pageRequest = PageRequest.of(0, 10, Sort.Direction.ASC, "id");
// id 속성 오름차순으로 정렬하여, 첫 페이지 10개의 레코드만 조회
페이지번호 : 0
페이지크기 : 10
정렬순서방향 : sort.Direction.ASC 오름차순
정렬기준속성명 : id
→ id 속성을 기준으로 오름차순 정렬하며, 첫 페이지(0) 부터 10개의 레코드를 조회한다.
② 레코드 조회
Page<Student> page = studentRepository.findAll(pageRequest);
// student 테이블 조회
pageRequest 객체에 담겨있는 정렬 순서와 페이지 번호를 적용하여 student 테이블을 조회한다.
이때, 그 페이지의 레코드들만 조회되어 리턴된다.
③ 테이블 전체 레코드 수 리턴
page.getTotalElements();
한 페이지에 조회된 레코드 수를 반환한다.
④ 조회 결과 레코드 리턴
page.getContent();
조회된 List<Student> 타입의 학생 목록이다.
버전 | Spring Boot 3 |
템플릿 엔진 | Thymeleaf |
▶ Pagination.java (모델)
package net.skhu.model;
import lombok.Data;
@Data
public class Pagination {
int pg = 1; // 현재 페이지 번호
int sz = 15; // 페이지 당 레코드 수
int recordCount; // 전체 레코드 수
public int getFirstRecordIndex() {
return (pg - 1) * sz;
}
public String getQueryString() {
return String.format("pg=%d&sz=%d", pg, sz);
}
}
액션 메소드와 뷰 사이에 전달되어야 하는 정보를 담은 객체다.
페이지 단위 조회 기능을 구현하기 위해 필요한 5가지 정보를 담았다.
① 현재 페이지 번호 = 1
② 페이지 당 레코드 수 = 15
③ 전체 레코드 수
④ 페이지의 첫번째 번호를 구하는 메소드
⑤ 페이징 구현을 위해 필요한 queryString 형태 반환 메소드
▶ BookController.java (컨트롤러)
@Controller
@Slf4j
@RequestMapping("book")
public class BookController {
@Autowired BookRepository bookRepository;
@Autowired CategoryRepository categoryRepository;
ModelMapper modelMapper = new ModelMapper();
/* 등록된 책 정보 출력 */
@GetMapping("list")
public String list(Model model, Pagination pagination) {
//List<Book> books = bookRepository.findAll(); // 페이징 적용 X
Page<Book> page = bookRepository.findAll(PageRequest.of(pagination.getPg() - 1, pagination.getSz(), Sort.Direction.ASC, "id"));
pagination.setRecordCount((int)page.getTotalElements());
model.addAttribute("books", page.getContent());
return "book/list";
}
// ... 생략 ....
}
책 조회 시 페이징을 적용한 controller 코드를 담았다.
코드 단순화를 위해 service 는 따로 구현하지 않았다.
▷ 페이지 단위 조회
Page<Book> page = bookRepository.findAll(PageRequest.of(
pagination.getPg() - 1, pagination.getSz(), Sort.Direction.ASC, "id"));
Pagination 모델 객체에 담겨있는 페이지 정보에 따라
현재 페이지의 레코드들을 조회해서 page 변수에 페이지 결과 객체로서 담긴다.
앞서 설계한 Pagination 모델 클래스에 따르면
① 현재 페이지 번호는 (입력된 페이지 번호 값 - 1)
② 페이지 당 레코드 수는 15
③ Book 객체의 id 속성명을 기준으로
④ 오름차순으로 정렬
되어 한 번에 최대 15개의 레코드가 조회된다.
▷ 전체 레코드 수 저장
pagination.setRecordCount((int)page.getTotalElements());
page.getTotalElements() 는 Spring Data JPA 에서 제공하는 Page 인터페이스의 메소드로 전체 레코드 수를 조회한다.
마지막 페이지의 경우 레코드가 최대 조회 개수에 미치지 못할 수 있다.
그리하여 조회된 레코드 개수를 따로 저장해주는 작업을 해야하는데, 이때 Spring Data JPA 가 제공하는 메소드를 사용했다.
▷ model 객체에 pagination 을 담지 않은 이유
model.addAttribute("books", page.getContent());
model 객체에 List<Book> 실질 데이터만을 담아 보냈는데, 그 이유는 매개변수로 받은 Pagination 객체에 있다.
reuqest Parameter 가 Pagination 객체로 받아지면 model 에 자동으로 add Attribute 되어 view 에 그려지기 때문이다.
<a th:href="'list?' + ${pagination.queryString}" class="btn">목록으로</a>
이후 전체 코드로 나올테지만,
list 를 나타내는 url 로 이동할 때, queryString 으로 pagination 객체의 정보를 담아 보낸다.
queryString 으로 전달된 파라미터들이 Pagination 객체로 받아지면서 자동으로 model 에 들어가 따로 넣어줄 필요가 없어지게 된다.
▶ common.js
function onClickHref(event) {
let url = event.currentTarget.getAttribute("href");
location.href = url;
}
function createPageTd(no, label, url) {
let td = document.createElement("td");
let a = document.createElement("a");
a.appendChild(document.createTextNode(label));
a.setAttribute("href", url.replace(/pg=[0-9]+/, "pg=" + no));
td.appendChild(a);
return td;
}
function pagination() {
let div = document.getElementsByClassName("pagination")[0];
if (div == undefined) return;
let recordCount = parseInt(div.getAttribute("data-record-count"));
let pageSize = parseInt(div.getAttribute("data-page-size"));
let currentPage = parseInt(div.getAttribute("data-current-page"));
let pageCount = Math.ceil(recordCount / pageSize);
if (currentPage > pageCount) currentPage = pageCount;
let url = location.href;
if (url.indexOf("pg=") < 0)
url = url + (url.indexOf("?") < 0 ? "?pg=1" : "&pg=1");
let table = document.createElement("table");
let tr = document.createElement("tr");
let baseNo = Math.floor((currentPage - 1) / 10) * 10;
if (baseNo > 0) tr.appendChild(createPageTd(baseNo, "<", url));
for (let i = 1; i <= 10; ++i) {
let no = baseNo + i;
if (no > pageCount) break;
let td = createPageTd(no, String(no), url)
if (no == currentPage) td.classList.add("active");
tr.appendChild(td);
}
let no = baseNo + 11;
if (no <= pageCount) tr.appendChild(createPageTd(no, ">", url));
table.appendChild(tr);
div.appendChild(table);
}
window.onload = pagination;
▶ book/list.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="/common.css" />
<script type="text/javascript" src="/common.js"></script>
<style>
form { margin-bottom: 4px; }
a.btn { float:right; margin-top: -37px; }
</style>
</head>
<body>
<div class="container">
<h1>책 목록</h1>
<!-- <a href="create" class="btn">책 등록</a> -->
<a th:href="'create?' + ${pagination.queryString}" class="btn">책 등록</a>
<table class="list">
<thead>
<tr>
<th>id</th>
<th>제목</th>
<th>저자</th>
<th>카테고리</th>
<th>가격</th>
<th>출판사</th>
</tr>
</thead>
<tbody>
<tr th:each="b : ${ books }">
<td th:text="${ b.id }"></td>
<td>
<!-- <a th:text="${ b.title }" th:href="@{ edit(id=${ b.id }) }"></a> -->
<a th:text="${b.title}"
th:href="'edit?id=' + ${b.id} + '&' + ${pagination.queryString}"></a>
</td>
<td th:text="${ b.author }"></td>
<td th:text="${ b.category.name }"></td>
<td th:text="${ b.price }"></td>
<td th:text="${ b.publisher }"></td>
</tr>
</tbody>
</table>
<div class="pagination"
th:data-record-count="${pagination.recordCount}"
th:data-page-size="${pagination.sz}"
th:data-current-page="${pagination.pg}"></div>
</div>
</body>
</html>
pagination 을 적용시킨 html 까지 작성해주면, 페이지는 다음과 같이 구현된다.
우리가 설정한 Pagination 객체 설정에 따라 한 페이지에 최대 15개의 레코드가 조회된다.
'Programming > SpringBoot & SpringDataJPA' 카테고리의 다른 글
[SpringBoot] 로그 메시지 출력 SLF4J (@Slf4j) 로 디버깅하기 (0) | 2025.03.26 |
---|---|
[SpringBoot] 게시글 이미지(파일) 미리보기 & 업로드 (ver.팝업창) (0) | 2024.12.01 |