간단한 CRUD기능을 구현한 커뮤니티에 필요한 기능을 하나씩 추가해보려고 한다.
두번째는 페이징 기능을 추가하려고한다. 게시글 목록을 조회할 때 데이터가 추가될 때마다 전체 게시글을 불러오는 건 지금은 문제가 없지만 데이터가 많아질수록 데이터베이스도, 서버에서도 부하가 심해진다.
사용자의 눈에 보일만큼만 불러오기 위해 Spring Data Jpa로 간단하게 구현해보려고 한다.
Spring Data JPA 페이징, 정렬 처리는 어떻게 ?
Spring JPA Data를 사용하면 보통 JpaRepository를 사용한다.
CRUD 기능만 필요한 Reposiotry라면 CrudRepository를 사용해도 상관없고
페이징과 정렬까지 들어간 Repository가 필요하면 JpaRepository를 사용하면 된다.
JPARepositry<> 에서 findAll() 메서드 사용시 Pageable를 파라미터로 전달하면 거기에 맞는 Page를 리턴받을 수 있다.
간단하게 구현하기위해 @PageableDefault와 PageDTO와 타임리프를 사용했다.
PostService
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final UserRepository userRepository;
public Page<PostResponse> getList(Pageable pageable) {
return postRepository.findAll(pageable).map(PostResponse::new);
}
}
메소드 파라미터 타입을 Page로 지정하면, 반드시 파라미터로 Pageable을 받아야 한다.
Repository에서 찾은 Page<> 객체 속엔 엔티티가 들어있고 map() 메서드로 DTO로 바꿔줄 수 있다.
Page<> 내엔 전체 데이터 수, 현재 페이지, 현재 페이지에 해당 하는 게시글들이 있는데 게시글들을 DTO로 바꿔주겠다고
PageImpl등으로 바꿨었는데 그러면 다른 정보는 너어가지 않고 Pageable에서 요구한 게시글들만 넘어간다.
백엔드가 페이지를 관리하는 서버 사이드 렌더링을 할꺼면 map()메서드를 활용해 Page 객체로 넘겨주자
PostController
@Controller
@RequiredArgsConstructor
@RequestMapping("/board")
public class PostController {
private final PostService postService;
public String list(Model model, @PageableDefault(size = 2, sort = "id", direction = Sort.Direction.DESC)
Pageable pageable) {
Page<PostResponse> posts = postService.getList(pageable);
PageDto<PostResponse> postList = PageDto.of(posts);
model.addAttribute("posts", postList);
return "post/posts";
}
}
@PageableDefault
size, page, value, direction을 default값이 있는 상태의 Pageable로 설정하고 파라미터로 수정도 가능하다.
게시판의 1페이지는 최신글, 마지막 페이지는 가장 먼저 쓰인글이 보여야하므로 Post의 id로 정렬하고, 내림차순으로 정렬한 Pageable을 Service로 넘겨줘 Repository에서 글들을 찾아온다.
PageDto는 Repository에서 찾아온 Page<> 객체를 사용해 페이징의 처리를 위한 정보들을 포함해 View로 넘기기 위해 사용한다.
PageDto
@NoArgsConstructor
@Getter
public class PageDto<T> {
private final static int VIEWPAGESIZE = 5; // View에 보일 페이지 갯수 +1
private int startPage;
private int endPage;
private int nowPage;
private int totalPages;
private Page<T> contents;
private PageDto(int startPage, int endPage,int nowPage, int totalPages, Page<T> contents) {
this.startPage = startPage;
this.endPage = endPage;
this.nowPage = nowPage;
this.totalPages = totalPages;
this.contents = contents;
}
public static <T> PageDto of( Page<T> contents) {
int totalPages =contents.getTotalPages();
int nowPage = contents.getPageable().getPageNumber() + 1;
int startPage = 1 * VIEWPAGESIZE * (nowPage / VIEWPAGESIZE);
int endPage = startPage + VIEWPAGESIZE - 1;
return new PageDto<>(startPage == 0 ? 1 : startPage, endPage > totalPages ? totalPages : endPage,nowPage, totalPages, contents);
}
}
페이지 navbar에 보일 첫 페이지, 마지막페이지, 현재페이지와 페이지 객체를 포함한 PageDto를 만들어 View로 넘겨준다.
Thymeleaf Paging View
<main class="flex-shrink-0">
<div class="container">
<h2 class="mt-5">게시판</h2>
<div>총 건수 : <span th:text="${posts.contents.totalElements}"></span></div>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">작성자</th>
</tr>
</thead>
<tbody>
<tr th:each="post : ${posts.contents}">
<th th:text="${post.id}">1</th>
<td><a th:text="${post.title}" th:href="@{|/board/posts/${post.id}|}">Mark</a></td>
<td th:text="${post.user.username}">작성자 들어갈 곳</td>
</tr>
</tbody>
</table>
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${1==posts.nowPage} ? 'disabled'">
<a class="page-link" th:href="@{/board/posts(page=${posts.nowPage -2})}">Previous</a>
</li>
<li class="page-item" th:classappend="${i==posts.nowPage} ? 'disabled'"
th:each="i : ${#numbers.sequence(posts.startPage, posts.endPage)}">
<a class="page-link" th:href="@{/board/posts(page=${i - 1})}" th:text="${i}">1</a></li>
<li class="page-item" th:classappend="${posts.totalPages==posts.nowPage} ? 'disabled'">
<a class="page-link" href="#" th:href="@{/board/posts(page=${posts.nowPage})}">Next</a>
</li>
</ul>
</nav>
<div class="text-end">
<a type="button" class="btn btn-primary" th:href="@{/board/write}">Write</a>
</div>
</div>
</main>
총 게시물 수는 Page 내의 totalElement, 현재페이지의 게시물들은 Iterable 상속받은 Page 내의 PostResponse를 th:each로 뿌려준다.
previous, next는 현재페이지 전후로 이동하고 첫 페이지, 마지막페이지일 때 th:classappend로 disable 속성 추가
숫자로 보이는 페이지들은 #numbers.sqeunce + th.each로 PageDto의 startPage부터 endPage까지 뿌려주고 클릭시 해당 페이지로 이동하고 현재 페이지는 classappend로 disable 속성을 추가해줬다.
Spring Data JPA로 페이징 처리를 해주면 첫 페이지가 0으로 시작하므로 유의하기
결과
페이지 중 1페이지와 Previous 버튼이 비활성화되있고 @PageableDefault로 설정해둬 id를 기준으로 내림차순으로 정렬되어 마지막 글부터 가져온다. PageDTO에서 설정한 ViewPageSize만큼 보인다.
첫페이지와 마찬가지로 4페이지는 비활성화, 남은 글이 있기 때문에 Next는 활성화
6페이지, 마지막 페이지이므로 6페이지와 Next 버튼 비활성화
마무리
간단한 페이징처리 마무리했고 다음은 Spring Data JPA를 이용해 검색과 페이징을 진행할 예정이다.