이전 개발 보러 가기
https://taetae99.tistory.com/39
[Board] 벡앤드 토이 프로젝트 - 게시글 수정, 삭제 기능 개발 (8)
이전 개발 보러 가기https://taetae99.tistory.com/38 [Board] 벡엔드 토이 프로젝트 - 게시글 생성 기능 구현 (7)이전 개발 단계 보러 가기https://taetae99.tistory.com/36 벡엔드 토이 프로젝트(게시판) - MemberContro
taetae99.tistory.com
목차
0. 페이징의 목적
게시글이 누적되어 데이터가 많아진다면
한 페이지에 모든 게시글을 불러오는 경우 매번 많은 양의 데이터를 조회해야 하므로 성능과 사용자 경험이 저하될 수 있다.
페이징을 구현하여 필요한 데이터만 불러와 성능이 향상되고 사용자는 효율적으로 정보를 탐색할 수 있을 것이다.
💡 페이징 요구사항
- 한 페이지에 10개의 게시글이 표시되도록 한다.
- 최대 5개의 페이지 버튼만 화면에 표시되도록 한다.
- 페이지 버튼 앞뒤로 '이전'과 '다음' 버튼이 표시되도록 한다.
- 첫 번째 페이지에서는 '이전' 버튼이 비활성화되고, 마지막 페이지에서는 '다음' 버튼이 비활성화되도록 한다.
아래는 페이징이 구현된 모습이다. 임의의 게시글을 101개 작성했다.

1. Repository
public Page<Post> findPostsWithPaging(Pageable pageable) {
List<Post> posts = em.createQuery("select p from Post p order by p.id desc", Post.class)
.setFirstResult((int) pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
long totalCount = em.createQuery("select count(p) from Post p", Long.class).getSingleResult();
return new PageImpl<>(posts, pageable, totalCount);
}
Repository에서 JPQL을 작성하여 게시글을 검색한다.
- id를 기준으로 내림차순으로 정렬한다.
- setFirstResult는 검색할 첫 번째 결과의 위치를 지정한다.
- pageable.getOffset은 현재 페이지에서 조회를 시작할 데이터의 위치를 반환한다.
- 예를 들어 한 페이지에 10개씩 출력하고 현재 페이지가 5페이지라면 5 * 10 = 50으로 50번째 데이터부터 조회한다.
- setMaxResult는 반환할 결과의 최대 개수를 지정한다.
- pageable.getPageSize()는 한 페이지에 표시할 데이터 개수를 반환한다.
- totalCount는 전체 페이지 수를 계산하는 데 필요하므로 DB에서 전체 게시글 수를 조회하여 저장한다.


반환 시에는 PageImpl을 사용하여 조회된 List<Post>를 Page<Post>로 변환한다.
Page<T>는 Spring Data JPA에서 페이징 처리된 데이터를 담는 인터페이스이다.
PageImpl<T>는 Page<T>의 구현체로 List<T>와 페이징 정보를 함께 받아 Page<T> 객체로 변환하여 반환한다.

2. Service
public Page<Post> findPagePosts(Pageable pageable) {
return postRepository.findPostsWithPaging(pageable);
}
pageable을 인자로 받아 페이징 처리된 게시글 데이터를 반환한다.
3. Controller
@GetMapping({"/", "/board"})
public String home(@AuthenticationPrincipal MemberDetail memberDetail, Model model,
@PageableDefault(page = 0,size = 10) Pageable pageable) {
if (memberDetail != null) {
model.addAttribute("nickname", memberDetail.getNickname());
}
Page<Post> posts = postService.findPagePosts(pageable);
int limit = 5;
int startPage = getStartPage(pageable, limit);
int endPage = getEndPage(startPage, limit, posts.getTotalPages());
PageInfoDto pageInfo = PageInfoDto.createPageInfo(startPage, endPage, pageable.getPageNumber(), posts.getTotalPages(), posts);
model.addAttribute("pageInfo", pageInfo);
return "board";
}
private static int getEndPage(int startPage, int limit, int totalPages) {
return Math.min(startPage + limit - 1, totalPages);
}
private static int getStartPage(Pageable pageable, int limit) {
return ((pageable.getPageNumber() / limit) * limit) + 1;
}
컨트롤러에서는 기존의 메인화면에 게시글 목록을 불러오는 메서드를 수정하였다.
- @PageableDefault를 사용하여 Pageable객체에 값을 설정한다.
- limit는 화면에 표시되는 페이지 버튼의 개수를 설정하는 변수이다.
- 예를 들어 limit가 5인 경우 1번~5번 페이지까지 표시하고 6번 페이지를 클릭하면 6번~10번까지 표시한다.
- startPage는 현재 페이지를 기준으로 화면에 표시되는 페이지 버튼의 시작 값을 담은 변수이다.
- 예를 들어 네비게이션에 6,7,8,9,10 페이지 버튼이 존재한다면 startPage의 값은 6이다.
- endPage는 startPage에서 계산된 페이지 범위 내에서의 끝 값이다.
- 예를 들어 네비게이션에 6,7,8,9,10 페이지 버튼이 존재한다면 startPage는 6이고 endPage는 10이다.

@PageableDefault 어노테이션은 Pageable 객체에 값을 설정하는 데 사용된다.
- page 속성은 조회를 시작하는 페이지 번호를 설정한다. 기본값은 0이다.
- UI에서는 페이지 번호를 1부터 사용하지만 Spring에서 Page는 기본값으로 0으로 설정되어 있기 때문에 주의하여 사용해야 한다.
- size는 한 페이지에 표시할 데이터의 개수를 설정한다. 기본값은 10이다.
- sort를 사용해 데이터의 정렬 기준을 설정할 수 있다.
4. Thymleaf
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${pageInfo.currentPage == 0} ? 'disabled'">
<a class="page-link" th:href="@{/board(page=${(pageInfo.currentPage - 1)})}">이전</a>
</li>
<li class="page-item" th:if="${pageInfo.totalPages>0}" th:each="pageNum : ${#numbers.sequence(pageInfo.startPage,pageInfo.endPage)}"
th:classappend="${pageNum == (pageInfo.currentPage+1)} ? 'active'">
<a class="page-link" th:href="@{/board(page=${(pageNum - 1)})}" th:text="${pageNum}"></a>
</li>
<li class="page-item" th:classappend="${(pageInfo.currentPage+1) >= pageInfo.totalPages} ? 'disabled'">
<a class="page-link" th:href="@{/board(page=${pageInfo.currentPage + 1})}">다음</a>
</li>
</ul>
</nav>
@PageableDefault에서 page 값을 0으로 설정했지만 네비게이션에 표시되는 페이지 번호는 1부터 시작하도록 설정했다.
이로 인해 실제 페이지 값(@PageableDefault의 page)과 네비게이션에 표시되는 번호 사이에 모순이 발생한다.
이를 위해 currentPage와 pageNum을 다룰 때 값을 1씩 증감하여 처리한다.
또한 페이지 네비게이션의 '이전' 버튼은 현재 페이지가 0일 때 비활성화되고 '다음' 버튼은 마지막 페이지에서 비활성화된다.


5. PageInfoDto
@Getter
public class PageInfoDto {
private int startPage;
private int endPage;
private int currentPage;
private int totalPages;
private Page<Post> posts;
protected PageInfoDto() {
}
private PageInfoDto(int startPage, int endPage, int currentPage, int totalPages, Page<Post> posts) {
this.startPage = startPage;
this.endPage = endPage;
this.currentPage = currentPage;
this.totalPages = totalPages;
this.posts = posts;
}
public static PageInfoDto createPageInfo(int startPage, int endPage, int currentPage, int totalPages, Page<Post> posts) {
return new PageInfoDto(startPage, endPage, currentPage, totalPages, posts);
}
}
페이징 처리된 데이터를 화면에 전달하기 위해 Dto를 생성했다.
- startPage, endPage는 시작과 끝 페이지 정보를 담고 있다.
- currentPage는 현재 페이지 정보를 담고 있다.
- totalPages는 전체 페이지 수이다.
- posts는 현재 페이지에 해당하는 게시글을 담고 있다.
6. 실행 모습
두 가지 경우에 대해서 실행 모습을 보여주고자 한다.
6-1. 게시글이 101개 존재할 때
게시글이 101개 존재한다면 한 페이지에 10개의 게시물이 존재하므로 총 11개의 페이지가 생긴다.

1페이지에서는 '이전' 버튼이 비활성화된다.

6페이지로 넘어가면 6-10까지의 버튼이 보인다.

마지막 페이지에 도달하면 '다음' 버튼이 비활성화된다.
6-2. 게시글이 한 개 존재할 때
게시글이 한 개 존재한다면 페이지는 한 개 존재한다.

게시글을 삭제하면 페이지가 존재하지 않는다.

'프로젝트 > Board 프로젝트' 카테고리의 다른 글
| [JPA] 게시판 프로젝트 - 마이그레이션 : H2에서 MySQL (11) (1) | 2025.02.28 |
|---|---|
| [JPA] 게시판 프로젝트 - 비동기 없이 댓글 수정 및 댓글 생성, 삭제 구현 (10) (0) | 2025.02.24 |
| [JPA] 게시판 프로젝트 - 게시글 수정, 삭제 기능 개발 (8) (1) | 2025.02.09 |
| [JPA] 게시판 프로젝트 - 게시글 생성 기능 구현 (7) (4) | 2025.02.03 |