해당 게시글은 점프 투 스프링부트 교재를 통한 개인 학습 용도이며 기초 세팅은 생략하였습니다.
자바 8, 스프링부트 2.7.7버전 입니다.
테스트 데이터 만들기
페이징을 구현하기 전에 페이징을 테스트할 수 있을 정도로 충분한 데이터를 생성하자.
대량의 테스트 데이터를 만드는 가장 간단한 방법은 스프링부트의 테스트 프레임워크를 이용하는 것이다.
PracticeApplicationsTests를 수정하자.
package com.example.board;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.board.question.QuestionService;
@SpringBootTest
class PracticeApplicationTests {
@Autowired
private QuestionService questionService;
@Test
void testJpa() {
for (int i = 1; i <= 300; i++) {
String subject = String.format("테스트 데이터입니다:[%03d]", i);
String content = "내용무";
this.questionService.create(subject, content);
}
}
}
서버를 중지하고 JUnit Test를 실행한 후 다시 서버를 재시작 해보자.
현재 페이징 처리가 안되기 때문에 게시물 300개를 작성하면 한 페이지에 300개의 게시물이 모두 조회된다.
등록한 게시물도 최근 순으로 보여야 하는데 등록한 순서로 보이는 문제도 있다.
페이징 구현하기
JPA 환경 구축시 설치했던 JPA 관련 라이브러리에 이미 페이징을 위한 패키지들이 들어있다.
- org.springframework.data.domain.Page
- org.springframework.data.domain.PageRequest
- org.springframework.data.domain.Pageable
QuestionRepository에 다음과 같은 findAll 메서드를 추가하자.
package com.example.board.question;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
Page<Question> findAll(Pageable pageable);
}
Pageable 객체를 입력으로 받아 Page<Question> 타입 객체를 리턴하는 findAll 메서드를 생성했다.
QuestionService도 다음과 같이 수정하자.
(... 생략 ...)
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
(... 생략 ...)
public class QuestionService {
(... 생략 ...)
public Page<Question> getList(int page) {
Pageable pageable = PageRequest.of(page, 10);
return this.questionRepository.findAll(pageable);
}
(... 생략 ...)
}
질문 목록을 조회하는 getList 메서드를 위와 같이 변경했다. getList 메서드는 정수 타입의 페이지번호를 입력받아 해당 페이지의 질문 목록을 리턴하는 메서드로 변경했다. Pageable 객체를 생성할때 사용한 PageRequest.of(page, 10)에서 page는 조회할 페이지의 번호이고 10은 한 페이지에 보여줄 게시물의 갯수를 의미한다. 이렇게 하면 데이터 전체를 조회하지 않고 해당 페이지의 데이터만 조회하도록 쿼리가 변경된다.
getList 메서드의 입출력 구조가 변경되었으므로 QuestionController도 수정해야 한다.
package com.example.board.question;
(... 생략 ...)
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.data.domain.Page;
(... 생략 ...)
public class QuestionController {
(... 생략 ...)
@GetMapping("/list")
public String list(Model model, @RequestParam(value="page", defaultValue="0") int page) {
Page<Question> paging = this.questionService.getList(page);
model.addAttribute("paging", paging);
return "/question/question_list";
}
(... 생략 ...)
}
http://localhost:8080/question/list?page=0 처럼 GET 방식으로 요청된 URL에서 page값을 가져오기 위해
@RequestParam(value="page", defaultValue="0") int page 매개변수가 list 메서드에 추가되었다.
URL에 페이지 파라미터 page가 전달되지 않은 경우 디폴트 값으로 0이 되도록 설정했다.
스프링부트의 페이징은 첫페이지 번호가 1이 아닌 0이다.
그리고 템플릿에 Page<Question> 객체인 paging을 전달했다. Page 객체에는 다음과 같은 속성이 있다.
다음의 속성들은 템플릿에서 페이징을 처리할 때 사용할 것이다.
항목 | 설명 |
paging.isEmpty | 페이지 존재 여부 (게시물이 있으면 false, 없으면 true) |
paging.totalElements | 전체 게시물 개수 |
paging.totalPages | 전체 페이지 개수 |
paging.size | 페이지당 보여줄 게시물 개수 |
paging.number | 현재 페이지 번호 |
paging.hasPrevious | 이전 페이지 존재 여부 |
paging.hasNext | 다음 페이지 존재 여부 |
기존에 전달했던 이름인 "questionList" 대신 "paging" 이름으로 템플릿에 전달했기 때문에
question_list.html도 다음과 같이 변경해야 한다.
(... 생략 ...)
<tbody>
<tr th:each="question, loop : ${paging}">
(... 생략 ...)
</tr>
</tbody>
(... 생략 ...)
http://localhost:8080/question/list?page=0 화면
http://localhost:8080/question/list?page=1 화면
템플릿에 페이지 이동 기능 구현하기
질문 목록에서 페이지를 이동하려면 페이지를 이동할 수 있는 "이전", "다음" 과 같은 링크가 필요하다.
question_list.html 템플릿 파일의 </table> 태그 바로 밑에 다음 코드를 추가하자.
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
(... 생략 ...)
</table>
<!-- 페이징처리 시작 -->
<div th:if="${!paging.isEmpty()}">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
<a class="page-link"
th:href="@{|?page=${paging.number-1}|}">
<span>이전</span>
</a>
</li>
<li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
th:if="${page >= paging.number-2 and page <= paging.number+2}"
th:classappend="${page == paging.number} ? 'active'"
class="page-item">
<a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
</li>
<li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
<a class="page-link" th:href="@{|?page=${paging.number+1}|}">
<span>다음</span>
</a>
</li>
</ul>
</div>
<!-- 페이징처리 끝 -->
<a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>
페이지가 없는 경우에는 링크가 비활성화(disabled)되도록 하였다.
페이지 리스트를 루프 돌면서 해당 페이지로 이동할 수 있는 링크를 생성하였다.
이때 루프 도중의 페이지가 현재 페이지와 같을 경우에는 active클래스를 적용하여 강조표시(선택표시)도 해 주었다.
타임리프의 th:classappend="조건식 ? 클래스값" 속성은 조건식이 참인 경우 클래스값을 class 속성에 추가한다.
페이징 기능 | 코드 |
이전 페이지가 없으면 비활성화 | th:classappend="${!paging.hasPrevious} ? 'disabled'" |
다음 페이지가 없으면 비활성화 | th:classappend="${!paging.hasNext} ? 'disabled'" |
이전 페이지 링크 | th:href="@{|?page=${paging.number-1}|}" |
다음 페이지 링크 | th:href="@{|?page=${paging.number+1}|}" |
페이지 리스트 루프 | th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}" |
현재 페이지와 같으면 active 적용 | th:classappend="${page == paging.number} ? 'active'" |
#numbers.sequence(시작, 끝)은 시작 번호부터 끝 번호까지의 루프를 만들어 내는 타임리프의 유틸리티이다.
그리고 페이지 리스트를 보기 좋게 표시하기 위해 부트스트랩의 pagination 컴포넌트를 이용하였다.
템플릿에 사용한 pagination, page-item, page-link 등이 부트스트랩 pagination 컴포넌트의 클래스이다.
부트스트랩 pagination : https://getbootstrap.com/docs/5.2/components/pagination/
해당 코드를 삽입하여 페이지 표시 제한 기능을 구현했다.
th:if="${page >= paging.number-2 and page <= paging.number+2}"
이 코드는 페이지 리스트가 현재 페이지 기준으로 좌우 2개씩 보이도록 만든다.
루프내에 표시되는 페이지가 현재 페이지(paging.number) 보다 2만큼 작거나 큰 경우에만 표시되도록 한 것이다.
작성일시 역순으로 조회하기
현재 질문 목록은 등록한 순서로 데이터가 표시된다. 게시판은 가장 최근에 작성한 게시물이 가장 먼저 보이는 것이 일반적이다.
QuestionService 수정
(... 생략 ...)
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Sort;
(... 생략 ...)
public class QuestionService {
(... 생략 ...)
public Page<Question> getList(int page) {
List<Sort.Order> sorts = new ArrayList<>();
sorts.add(Sort.Order.desc("createDate"));
Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
return this.questionRepository.findAll(pageable);
}
(... 생략 ...)
}
역순으로 조회하기 위해서는 PageRequest.of 메서드의 세번째 파라미터로 Sort 객체를 전달해야 한다.
Sort.Order 객체로 구성된 리스트에 Sort.Order 객체를 추가하고 Sort.by(소트리스트)로 소트 객체를 생성할 수 있다.
작성일시(createDate)를 역순(Desc)으로 조회하려면 Sort.Order.desc("createDate") 같이 작성한다.
만약 작성일시 외에 추가로 정렬조건이 필요할 경우에는 sorts 리스트에 추가하면 된다.
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!