겉바속촉

[점프투스프링부트] 3-02. 페이징 본문

IT 일기 (상반기)/SPRING 기초

[점프투스프링부트] 3-02. 페이징

겉바속촉 2023. 3. 3. 10:12
728x90
반응형

 

점프투스프링부트 3-02

 

 

 

목표 : 페이징 적용하기

 

SBB의 질문 목록은 현재 페이징 처리가 안되기 때문에 게시물 300개를 작성하면 한 페이지에 300개의 게시물이 모두 조회된다. 이번 장에서는 페이징(Paging)을 적용하여 이 문제를 해결할 것.

 

 

 

 

 

 

 

대량 테스트 데이터 만들기

 

페이징을 구현하기 전에 페이징을 테스트할 수 있을 정도로 충분한 데이터를 생성하자.

대량의 테스트 데이터를 만드는 가장 간단한 방법은 스프링부트의 테스트 프레임워크를 이용하는 것이다.

다음처럼 테스트 케이스를 작성하자.

 

[파일명:/sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java]

package com.mysite.sbb;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.mysite.sbb.question.QuestionService;

@SpringBootTest
class SbbApplicationTests {

    @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);
        }
    }
}

 

총 300개의 테스트 데이터를 생성하는 테스트 케이스를 작성했다.

만약 로컬서버가 실행 중이라면 로컬 서버를 중지하고 [Run -> Run As -> Junit Test]로 testJpa 메서드를 실행하자.

그리고 다시 로컬서버를 실행한 후에 브라우저에서 질문 목록을 확인해 보자.

 

 

 

테스트 케이스로 등록한 데이터가 보일 것이다.

그리고 300개 이상의 데이터가 한 페이지 보여지는 것을 확인할 수 있다.

이러한 이유로 페이징은 반드시 필요하다.

 

그리고 등록한 게시물도 최근 순으로 보여야 하는데 등록한 순서로 보이는 문제도 있다.
이 문제도 함께 해결해 보자.

 

 

 

 

페이징 구현하기

 

페이징을 구현하기 위해 추가로 설치해야 하는 라이브러리는 없다.

JPA 환경 구축시 설치했던 JPA 관련 라이브러리에 이미 페이징을 위한 패키지들이 들어있기 때문이다.

 

다음의 클래스들을 이용하면 페이징을 쉽게 구현할 수 있다.

 

  • org.springframework.data.domain.Page
  • org.springframework.data.domain.PageRequest
  • org.springframework.data.domain.Pageable

 

 

위에 소개한 클래스를 적용해 페이징을 구현해 보자.

먼저 Question 리포지터리에 다음과 같은 findAll 메서드를 추가하자.

 

 

[파일명:/sbb/src/main/java/com/mysite/sbb/question/QuestionRepository.java]

package com.mysite.sbb.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 메서드를 생성했다.

그리고 Question 서비스도 다음과 같이 수정하자.

 

 

[파일명:/sbb/src/main/java/com/mysite/sbb/question/QuestionService.java]

(... 생략 ...)
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은 한 페이지에 보여줄 게시물의 갯수를 의미한다.

이렇게 하면 데이터 전체를 조회하지 않고 해당 페이지의 데이터만 조회하도록 쿼리가 변경된다.

 

Question 서비스의 getList 메서드의 입출력 구조가 변경되었으므로 Question 컨트롤러도 다음과 같이 수정해야 한다.

 

[파일명: sbb/src/main/java/com/mysite/sbb/question/QuestionController.java]

package com.mysite.sbb.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_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" 이름으로 템플릿에 전달했기 때문에 템플릿도 다음과 같이 변경해야 한다.

 

[파일명:/sbb/src/main/resources/templates/question_list.html]

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
    <table class="table">
        (... 생략 ...)
        <tbody>
            <tr th:each="question, loop : ${paging}">
                (... 생략 ...)
            </tr>
        </tbody>
    </table>
    <a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>

 

이렇게 수정하고 브라우저에서 http://localhost:8080/question/list?page=0 URL을 요청해 보자.

다음과 같이 첫페이지에 해당하는 게시물 10개만 조회되는 것을 확인할 수 있다.

 

 

다시 http://localhost:8080/question/list?page=1 URL을 요청하면 두번째 페이지에 해당하는 게시물이 조회된다.

 

 

 

 

 

 

템플릿에 페이지 이동 기능 구현하기

 

질문 목록에서 페이지를 이동하려면 페이지를 이동할 수 있는 "이전", "다음" 과 같은 링크가 필요하다.

이번에는 질문 목록에 페이지를 이동할 수 있는 링크들을 추가해 보자.

 

question_list.html 템플릿 파일의 </table> 태그 바로 밑에 다음 코드를 추가하자.

 

[파일명:/sbb/src/main/resources/templates/question_list.html]

<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: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>

 

상당히 많은 양의 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/

 

 

 

 

페이지 리스트

 

여기까지 수정하고 질문 목록을 조회해 보자.

 

 

 

페이징 처리는 잘 되었지만 한 가지 문제를 발견할 수 있다.

문제는 위에서 보듯이 이동할 수 있는 페이지가 모두 표시된다는 점이다.

 

이 문제를 해결하기 위해 다음과 같이 질문 목록 템플릿을 수정하자.

 

[파일명:/sbb/src/main/resources/templates/question_list.html]

(... 생략 ...)
    <!-- 페이징처리 시작 -->
    <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-5 and page <= paging.number+5}"
                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>

 

다음의 코드를 삽입하여 페이지 표시 제한 기능을 구현했다.

th:if="${page >= paging.number-5 and page <= paging.number+5}"

 

이 코드는 페이지 리스트가 현재 페이지 기준으로 좌우 5개씩 보이도록 만든다.

루프내에 표시되는 페이지가 현재 페이지를 의미하는 paging.number 보다 5만큼 작거나 큰 경우에만 표시되도록 한 것이다.

 

만약 현재 페이지가 15라면 다음처럼 페이지 리스트가 표시될 것이다.

 

 

 

 

작성일시 역순으로 조회하기

 

현재 질문 목록은 등록한 순서로 데이터가 표시된다.

하지만 게시판은 가장 최근에 작성한 게시물이 가장 먼저 보이는 것이 일반적이다.

 

이를 구현하기 위해 Question 서비스를 다음과 같이 수정하자.

 

 

[파일명:/sbb/src/main/java/com/mysite/sbb/question/QuestionService.java]

(... 생략 ...)
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 리스트에 추가하면 된다.

 

이렇게 수정하고 첫번째 페이지를 조회하면 이제 가장 최근에 등록된 순으로 게시물이 출력되는 것을 확인한 수 있다.

 

 

 

 

 

 

728x90
반응형