본문 바로가기

Programming/SpringBoot & SpringDataJPA

[JPA] Pageable 이용한 Pagination(페이징 처리)


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 까지 작성해주면, 페이지는 다음과 같이 구현된다.

 

css 별도

 

우리가 설정한 Pagination 객체 설정에 따라 한 페이지에 최대 15개의 레코드가 조회된다.