스프링 간단한 페이징 - seupeuling gandanhan peijing


해당 포스팅에서 화면 처리는 JSP가 아닌 타임리프(Thymeleaf) 템플릿 엔진(이하 타임리프)을 사용합니다.

본 포스팅은 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.

본 포스팅은 롬복(Lombok) 라이브러리를 이용하니
롬복이 설치되어 있지 않으시다면, 링크를 통해 롬복을 우선적으로 설치해 주세요 :)


이전 글에서는 프로젝트에 AOP를 적용하여, 컨트롤러, 서비스, 매퍼
각 영역별로 실행되는 메서드를 로그로 출력해 보았고,
트랜잭션의 개념에 대해 간단히 공부해 보았습니다.

이번 글에서는 게시판(게시글)에 페이지네이션(Pagination) 기능을 적용해볼 건데요.

게시글, 댓글 등 페이징 기능이 필요한 어느 곳에서나 공통으로 사용할 수 있는 클래스를 정의하고,
어떠한 방식으로 페이징을 적용할 수 있는지 알아보도록 하겠습니다.


이번 글은 구멍가게 코딩단 코드로 배우는 스프링 웹 프로젝트 서적을 참고하여 포스팅하였습니다.


1. 페이징(Paging)이란?

페이징은 사용자가 어떠한 데이터를 필요로 할 때, 전체 데이터 중의 일부를 보여주는 방식입니다.

예를 들어, 게시판에 등록된 게시글이 100,000개라고 가정해 보겠습니다.

하나의 페이지에서 100,000개의 데이터를 출력한다면 어떻게 될까요...?

페이지의 로딩 속도가 느려질뿐더러, 혹시라도 어떤 사용자가 필요로 하는 데이터가
30,000번째로 등록된 데이터라면,
스크롤을 밑도 끝도 없이 내려서
한참을 찾아야 하는 불편함을 겪게 될 것입니다.

이러한 문제점을 페이징과 검색 기능을 통해 해결할 수 있습니다.

 

2. 페이징 파라미터 처리용 클래스 만들기

페이징과 검색 처리는 다음의 표와 같이 몇 가지 데이터(파라미터)를 필요로 합니다.

파라미터설명page현재 페이지 번호를 의미합니다.recordSize페이지당 출력할 데이터 개수를 의미합니다.pageSize화면 하단에 출력할 페이지의 사이즈를 의미합니다.

ex:) 1~5, 1~10, 1~20 등
keyword검색 키워드를 의미합니다.searchType검색 유형을 의미합니다.

ex:) 전체, 제목, 내용, 작성자 등

 

표에 명시된 파라미터들을 컨트롤러에서 @RequestParam으로 일일이 전달받는 방법은
효율적이지 않을뿐더러, 파라미터의 개수가 늘어난다거나 했을 때 파라미터의 관리와 수집이
까다로워지기 때문에 공통으로 사용할 수 있는 하나의 클래스로 관리하는 게 좋을 듯합니다.

우선 com.study.common.dto 클래스에 SearchDto 클래스를 추가하고, 소스 코드를 작성해 주세요.

스프링 간단한 페이징 - seupeuling gandanhan peijing
클래스가 추가된 패키지 구조

 

package com.study.common.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class SearchDto {

    private int page;             // 현재 페이지 번호
    private int recordSize;       // 페이지당 출력할 데이터 개수
    private int pageSize;         // 화면 하단에 출력할 페이지 사이즈
    private String keyword;       // 검색 키워드
    private String searchType;    // 검색 유형

    public SearchDto() {
        this.page = 1;
        this.recordSize = 10;
        this.pageSize = 10;
    }

    public int getOffset() {
        return (page - 1) * recordSize;
    }

}

 

구성 요소설명page현재 페이지 번호를 의미합니다.

화면을 처리할 때 페이징 정보를 계산하는 용도로 사용됩니다.
recordSize페이지마다 출력할 데이터의 개수를 의미합니다.

마찬가지로 화면 처리에서 페이징 정보 계산에 사용됩니다.
pageSize화면 하단에 출력할 페이지의 크기를 지정합니다.

5로 지정하면 1~5까지의 페이지가, 10으로 지정하면 1~10까지의 페이지가 보이게 됩니다.
keyword페이징에서 검색은 빠질 수 없는 부분입니다.

원하는 데이터를 찾기 위해 페이지를 일일이 넘어 다니며 찾을 수는 없으니까요!

다음 글에서 동적(Dynamic) SQL을 처리하면서 사용됩니다.
searchTypekeyword와 함께 사용됩니다.

게시글의 제목, 내용, 작성자 중 하나 또는 전체로 LIKE 검색을 할 수 있습니다.
생성자객체가 생성되는 시점에 기본값으로 현재 페이지 번호는 1로,

페이지당 출력할 데이터 개수와 하단에 출력할 페이지 개수를 10으로 초기화합니다.
getOffset( )MySQL에서 LIMIT 구문의 시작 부분에 사용되는 메서드입니다.

뒤에서 SQL 쿼리를 추가하고 해당 메서드가 어떻게 사용되는지 알아보도록 하겠습니다.

 

3. Mapper 인터페이스와 XML 변경하기

우선 PostMapper 인터페이스입니다.

기존에 만들어둔 findAll( )과 count( ) 메서드가 SearchDto 객체를 파라미터로 전달받도록
다음과 같이 변경해 주세요.

    /**
     * 게시글 리스트 조회
     * @param params - search conditions
     * @return 게시글 리스트
     */
    List<PostResponse> findAll(SearchDto params);

    /**
     * 게시글 수 카운팅
     * @param params - search conditions
     * @return 게시글 수
     */
    int count(SearchDto params);

 

다음은 PostMapper.xml입니다.

findAll 쿼리의 parameterType에 SearchDto를 선언하고, count 쿼리를 작성해 주세요.

    <!-- 게시글 리스트 조회 -->
    <select id="findAll" parameterType="com.study.common.dto.SearchDto" resultType="com.study.domain.post.PostResponse">
        SELECT
            <include refid="postColumns" />
        FROM
            tb_post
        WHERE
            delete_yn = 0
        ORDER BY
            id DESC
        LIMIT #{offset}, #{recordSize}
    </select>
    
    
    <!-- 게시글 수 카운팅 -->
    <select id="count" parameterType="com.study.common.dto.SearchDto" resultType="int">
        SELECT
            COUNT(*)
        FROM
            tb_post
        WHERE
            delete_yn = 0
    </select>

 

LIMIT

MySQL에서 LIMIT 구문은 데이터를 원하는 만큼 가져오고 싶을 때 사용합니다.

LIMIT의 첫 번째 파라미터에는 시작 위치(몇 번째 데이터부터 가지고 올 것인지)를 지정하고,

두 번째 파라미터에는 시작 위치를 기준으로 가지고 올 데이터의 개수를 지정합니다.

파라미터설명offsetMyBatis에서 #{파라미터}는 여러 멤버를 가진 객체의 경우 getter에 해당됩니다.

offset은
SearchDto 클래스의 getOffset( )의 리턴 값을 의미하는데요.

getOffset( ) 메서드가 리턴하는 값은 (page - 1) * recordSize를 계산한 값입니다.

예를 들어, 현재 페이지 번호(page)를 3으로,

페이지당 출력할 데이터의 개수(recordSize)를 10으로 가정해보면 (3 - 1) * 10 = 20입니다.

즉, LIMIT 20, 10으로 쿼리가 실행됩니다.
recordSize페이지당 출력할 데이터의 개수를 의미합니다.

 

LIMIT 구문의 예시는 다음과 같습니다.

(과거 데이터라 테이블명과 컬럼명이 다르니 참고해 주세요!)

스프링 간단한 페이징 - seupeuling gandanhan peijing
LIMIT 1
스프링 간단한 페이징 - seupeuling gandanhan peijing
LIMIT 2
스프링 간단한 페이징 - seupeuling gandanhan peijing
LIMIT 3
스프링 간단한 페이징 - seupeuling gandanhan peijing
LIMIT 4

 

LIMIT 구문에서 1, 2번 이미지와 같이 두 번째 인자가 비어있으면,
첫 번째로 등록된 데이터부터 N(입력한 값)개까지의 데이터를 가지고 옵니다.

그리고 3, 4번 이미지와 같이 두 번째 파라미터가 비어있지 않으면,
N개부터 페이지당 출력할 데이터 개수(recordsSize)까지의 데이터를 가지고 옵니다.

예를 들어, LIMIT 0, 15로 지정한다면 1페이지의 첫 번째 게시글부터 15개까지를 조회합니다.

추가적으로 LIMIT 구문의 결과에는 ORDER BY를 적용할 수도 있습니다.

DBMS 툴에서 직접 쿼리를 작성해서 실행해 보시는 것이 여러분의 빠른 이해에 도움이 될 듯합니다 :)

 

count( )

게시글 테이블에서 검색 조건에 해당되는 데이터 개수를 조회하기 위한 용도로 사용됩니다.

검색 처리에서 사용되는 기능으로, 지금 당장은 사용되지 않습니다.

 

4. 서비스(Service) 변경하기

Mapper 인터페이스와 XML이 변경되었으니, 서비스와 컨트롤러도 마찬가지로 변경되어야 합니다.

PostService의 findAllPost( )를 다음과 같이 변경해 주세요.

    /**
     * 게시글 리스트 조회
     * @param params - search conditions
     * @return 게시글 리스트
     */
    public List<PostResponse> findAllPost(final SearchDto params) {
        return postMapper.findAll(params);
    }

 

5. 컨트롤러(Controller) 변경하기

마지막으로 PostController의 openPostList( )를 다음과 같이 변경해 주세요.

    // 게시글 리스트 페이지
    @GetMapping("/post/list.do")
    public String openPostList(@ModelAttribute("params") final SearchDto params, Model model) {
        List<PostResponse> posts = postService.findAllPost(params);
        model.addAttribute("posts", posts);
        return "post/list";
    }

 

@ModelAttribute 

해당 어노테이션을 이용하면, 파라미터로 전달받은 객체를 자동으로 뷰(HTML)까지 전달할 수 있습니다.

예를 들어, 1:1로 매핑되는 단일 파라미터는 @RequestParam으로 넘겨받은 후
Model 인터페이스의 addAttribute( ) 메서드를 이용해서 일일이 화면(View)으로 전달하는데요.

@ModelAttribute는 별다른 처리 없이 화면(View)으로 파라미터를 전달해 줍니다.

@ModelAttribute 괄호 안의 "params"는 화면(View)에서 사용할 별칭(Alias)입니다.

만약, @ModelAttribute("a") SearchDto params로 선언했다면,
뷰에서는 ${a.page}와 같은 방식으로 객체에 접근할 수 있습니다.

 

6. 자가 복사(Self Copy) 이용하기

페이징 테스트를 위해서는 대량의 데이터가 필요합니다.

제 tb_post 테이블에는 삭제되지 않은 데이터를 기준으로 총 100개의 게시글이 있습니다.

자가 복사(Self Copy)를 이용하면 (테이블에 저장된 데이터 * 2) 만큼의 데이터가 INSERT 됩니다.

100개에서 한 번 실행하면 200개, 한 번 더 실행하면 400개, 또 한 번 실행하면 800개의 데이터가 추가됩니다.

 

혹시라도 테이블에 데이터가 하나도 없는 경우에는 몇 건의 게시글을 추가한 다음에 진행해 주세요.

(테스트 클래스에서 for 문을 돌려 postService의 savePost( ) 메서드를 반복 실행하시면 편리합니다!)

 

 

자가 복사(Self Copy) 작업의 순서는 다음과 같습니다.

DBMS 툴에서 DESC tb_post 명령어를 실행하면 테이블의 구조를 확인할 수 있습니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
tb_post 테이블 구조

 

여기서 값을 필수적으로 입력해 주어야 하는 컬럼은 id와 날짜를 제외한 전체 컬럼입니다.

이제, 다음의 SQL 쿼리를 작성하고 쿼리를 실행합니다.

INSERT INTO tb_post (title, content, writer, view_cnt, notice_yn, delete_yn)
(SELECT title, content, writer, view_cnt, notice_yn, delete_yn FROM tb_post WHERE delete_yn = 0);

 

앞의 쿼리를 여러 번 실행해보시면, 이미지와 같이 게시글 테이블의 레코드가
2배씩 증가하는 것을 확인하실 수 있습니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
2배씩 증가하는 레코드

 

7. 애플리케이션 실행하기

애플리케이션을 실행하고, 게시글 리스트 페이지로 이동해 보도록 하겠습니다.

URI에는 파라미터가 없기 때문에 SearchDto 클래스의 생성자에 의해
현재 페이지 번호(page)와 페이지당 출력할 데이터 개수(recordSize)는 각각 1, 10이 되며,
실행된 쿼리의 LIMIT 구문은 getOffset( )과 getRecordSize( )의 결괏값인 0, 10으로 처리됩니다.

ORDER BY가 게시글 번호(id) DESC으로 지정되었기에
가장 마지막에 등록된 데이터를 기준으로 10개가 출력됩니다.

(이미지 깨짐 문제를 방지하기 위하여 헤더, 푸터, 좌측 메뉴는 제외시켰습니다!)

스프링 간단한 페이징 - seupeuling gandanhan peijing
쿼리 스트링이 없는 게시글 리스트 페이지

 

스프링 간단한 페이징 - seupeuling gandanhan peijing
실행된 SQL 쿼리 로그

 

이번에는 URI에 강제로 파라미터를 지정해 보도록 하겠습니다.

이미지와 마찬가지로 현재 페이지 번호를 5, 페이지당 출력할 데이터 개수를 15로 지정한 다음,

콘솔의 쿼리 로그를 보면 LIMIT 구문의 값이 달라졌음을 확인할 수 있습니다.

(이미지를 확대하여 URL을 확인해 주세요!)

스프링 간단한 페이징 - seupeuling gandanhan peijing
쿼리 스트링이 포함된 게시글 리스트 페이지

 

스프링 간단한 페이징 - seupeuling gandanhan peijing
실행된 SQL 쿼리 로그

 

8. 페이지네이션(Pagination) 처리용 클래스 생성하기

브라우저에서 URI에 강제로 파라미터를 지정하여 게시글 리스트를 호출했을 때
데이터가 모두 올바르게 출력되는 것을 확인하였습니다.

웹에서는 화면 하단에 페이지 번호를 출력하는 기능의 이름을 페이지네이션이라고 부르는데요.

페이지네이션 계산을 위해서는 앞에서 생성한 SearchDto의 파라미터들이 필수적으로 필요합니다.

자세한 내용은 코드를 작성한 다음 설명을 드리도록 할게요 :)

com.study 패키지에 paging 패키지와 Pagination 클래스를 추가하고,
다음의 소스 코드를 작성해 주세요.

스프링 간단한 페이징 - seupeuling gandanhan peijing
패키지와 클래스가 추가된 디렉터리 구조

 

package com.study.paging;

import com.study.common.dto.SearchDto;
import lombok.Getter;

@Getter
public class Pagination {

    private int totalRecordCount;   // 전체 데이터 수
    private int totalPageCount;     // 전체 페이지 수
    private int startPage;          // 첫 페이지 번호
    private int endPage;            // 끝 페이지 번호
    private int limitStart;         // LIMIT 시작 위치
    private boolean existPrevPage;  // 이전 페이지 존재 여부
    private boolean existNextPage;  // 다음 페이지 존재 여부

    public Pagination(int totalRecordCount, SearchDto params) {
        if (totalRecordCount > 0) {
            this.totalRecordCount = totalRecordCount;
            this.calculation(params);
        }
    }

    private void calculation(SearchDto params) {

        // 전체 페이지 수 계산
        totalPageCount = ((totalRecordCount - 1) / params.getRecordSize()) + 1;

        // 현재 페이지 번호가 전체 페이지 수보다 큰 경우, 현재 페이지 번호에 전체 페이지 수 저장
        if (params.getPage() > totalPageCount) {
            params.setPage(totalPageCount);
        }

        // 첫 페이지 번호 계산
        startPage = ((params.getPage() - 1) / params.getPageSize()) * params.getPageSize() + 1;

        // 끝 페이지 번호 계산
        endPage = startPage + params.getPageSize() - 1;

        // 끝 페이지가 전체 페이지 수보다 큰 경우, 끝 페이지 전체 페이지 수 저장
        if (endPage > totalPageCount) {
            endPage = totalPageCount;
        }

        // LIMIT 시작 위치 계산
        limitStart = (params.getPage() - 1) * params.getRecordSize();

        // 이전 페이지 존재 여부 확인
        existPrevPage = startPage != 1;

        // 다음 페이지 존재 여부 확인
        existNextPage = (endPage * params.getRecordSize()) < totalRecordCount;
    }

}

 

totalRecordCount

전체 게시글의 개수를 의미합니다.

예를 들어, 테이블에 1,000개의 레코드가 있다고 가정했을 때

검색 조건이 없는 경우에는 전체 데이터 개수가 되고,

검색 조건이 있는 경우에는 조건에 해당되는 데이터 개수가 됩니다.

 

totalPageCount

페이지 하단에 출력할 전체 페이지 개수를 의미합니다.

테이블에 1,000개의 레코드가 있고, 페이지당 출력할 데이터 개수가 10개라고 가정했을 때

(1,000 / 10)의 결과인 100이 됩니다.

 

startPage

현재 페이지네이션의 첫 페이지를 의미합니다.

페이지 하단에 출력할 페이지 수(pageSize)가 10이고,

현재 페이지 번호(page)가 5라고 가정했을 때 1을 의미합니다.

다른 예로,  페이지 번호가 15라면, startPage는 11이 됩니다.

 

endPage

현재 페이지네이션의 끝 페이지를 의미합니다.

페이지 하단에 출력할 페이지 수(pageSize)가 10이고,

현재 페이지 번호(page)가 5라고 가정했을 때 10을 의미합니다.

다른 예로,  페이지 번호가 15라면, endPage는 20이 됩니다.

 

limitStart

MySQL의 LIMIT 구문에 사용되는 멤버 변수입니다.

LIMIT의 첫 번째 파라미터에는 시작 위치, 즉 몇 번째 데이터부터 조회할지를 지정하고,

두 번째 파라미터에는 시작 limitStart를 기준으로 조회할 데이터의 개수를 지정합니다.

예를 들어, 현재 페이지 번호가 1이고, 페이지당 출력할 데이터 개수가 10이라고 가정했을 때

(1 - 1) * 10 = 0이라는 결과가 나오게 되고, LIMIT 0, 10으로 쿼리가 실행됩니다.

다른 예로, 페이지 번호가 5라면, LIMIT 40, 10으로 쿼리가 실행됩니다.

 

existPrevPage

이전 페이지의 존재 여부를 확인하는 데 사용되는 멤버 변수입니다.

현재 첫 페이지(startPage)가 1이 아니라면, 이전 페이지는 무조건적으로 존재하게 됩니다.

 

existNextPage

다음 페이지의 존재 여부를 확인하는 데 사용되는 멤버 변수입니다.

예를 들어, 페이지당 출력할 데이터 개수가 10개, 끝 페이지 번호가 10이라고 가정했을 때

(10 * 10) = 100이라는 결과가 나오게 되는데요.

만약, 전체 데이터 개수가 105개라면, 다음 페이지 존재 여부는 true가 됩니다.

 

9. XML Mapper 수정하기

페이지네이션 계산용 클래스의 처리가 완료되었으니,

findAll 쿼리의 LIMIT 구문에서 계산된 페이지네이션 정보를 파라미터로 사용할 수 있도록

PostMapper.xml을 수정해 주어야 합니다.

findAll 쿼리를 다음과 같이 변경해 주세요.

    <!-- 게시글 리스트 조회 -->
    <select id="findAll" parameterType="com.study.common.dto.SearchDto" resultType="com.study.domain.post.PostResponse">
        SELECT
            <include refid="postColumns" />
        FROM
            tb_post
        WHERE
            delete_yn = 0
        ORDER BY
            id DESC
        LIMIT #{pagination.limitStart}, #{recordSize}
    </select>

 

10. SearchDto 수정하기

그런데, 뭔가 이상하죠? SearchDto에는 Pagination 타입의 멤버가 존재하지 않는데,

PostMapper의 findAll 쿼리에서는 pagination 객체의 limitStart를 LIMIT 시작 구문에 사용하고 있습니다.

네, 당연히 SearchDto가 Pagination 타입의 멤버를 갖도록 선언해 주는 게 맞습니다.

여러분이 SQL 쿼리를 먼저 보시는 게 이해가 쉬우시지 않을까 싶어 순서를 조금 뒤바꿔 보았습니다 :)

 

이제 SearchDto를 다음과 같이 변경해 주시면 되는데요.

Pagination 타입의 멤버가 추가되었고,

LIMIT 구문에서는 pagination의 limitStart를 이용하므로 getOffset( ) 메서드가 제거되었습니다.

package com.study.common.dto;

import com.study.paging.Pagination;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class SearchDto {

    private int page;                 // 현재 페이지 번호
    private int recordSize;           // 페이지당 출력할 데이터 개수
    private int pageSize;             // 화면 하단에 출력할 페이지 사이즈
    private String keyword;           // 검색 키워드
    private String searchType;        // 검색 유형
    private Pagination pagination;    // 페이지네이션 정보

    public SearchDto() {
        this.page = 1;
        this.recordSize = 10;
        this.pageSize = 10;
    }

}

 

11. 페이징(Paging) 전용 응답 클래스 생성하기

현재 PostService의 findAllPost( )는 구조적으로 문제가 있는데요.

findAllPost( )의 리턴 타입은 List<PostResponse>로 선언되어 있습니다.

 

Pagination 객체를 생성하여 정보를 계산하는 것까지는 문제가 되지 않지만,

페이지 하단에 번호를 그리는 작업은 뷰(HTML) 단에서 이루어져야 합니다.

즉, list.html에서 Pagination 타입의 객체를 필요로 한다는 의미인 거죠.

 

이를  해결하기 위해 Key, Value 구조로 이루어진 Map을 이용해도 되지만,

좀 더 직관적인 처리를 위해 페이징 전용 응답 클래스를 생성해 보도록 하겠습니다.

com.study.paging 패키지에 PagingResponse 클래스를 추가하고, 다음의 코드를 작성해 주세요.

package com.study.paging;

import lombok.Getter;

import java.util.ArrayList;
import java.util.List;

@Getter
public class PagingResponse<T> {

    private List<T> list = new ArrayList<>();
    private Pagination pagination;

    public PagingResponse(List<T> list, Pagination pagination) {
        this.list = list;
        this.pagination = pagination;
    }

}

 

list

자바의 제네릭(Generic)을 활용해 보았습니다.

T는 Type을 의미하며, 어떤 타입의 객체던 데이터로 받겠다는 의미입니다.

 

pagination

앞에서 생성한 페이지네이션 객체입니다.

계산된 페이지 정보를 담아 화면(HTML)으로 전달하는 용도로 사용됩니다.

 

12. 서비스(Service) 수정하기

이제 PostService의 findAllPost( ) 메서드를 수정할 차례입니다.

단순히 Mapper를 호출하는 게 아니라, 페이지 정보를 계산해주는 로직이 추가되어야 합니다.

계산된 페이지 정보를 이용하여 리스트 조회 쿼리를 실행하고,

앞에서 생성한 응답 객체를 이용해 리스트 데이터와 계산된 페이지 정보를 리턴합니다.

    /**
     * 게시글 리스트 조회
     * @param params - search conditions
     * @return 게시글 리스트
     */
    List<PostResponse> findAll(SearchDto params);

    /**
     * 게시글 수 카운팅
     * @param params - search conditions
     * @return 게시글 수
     */
    int count(SearchDto params);
0

 

전체 로직

전체 로직에 대해 가볍게 설명드리도록 하겠습니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
68~71번 라인

먼저, postMapper의 count( ) 메서드를 호출하여 데이터 개수를 카운팅합니다.

데이터가 없는 경우, 즉 테이블이 비어있거나, 검색 조건에 해당되는 데이터가 없는 경우에는
PagingResponse로 비어있는 리스트와 null을 전달하고 로직을 종료합니다.

 

스프링 간단한 페이징 - seupeuling gandanhan peijing
73~77번 라인

 

73번 로직이 실행된다는 건 테이블에 데이터가 존재한다는 것을 의미합니다.

먼저, Pagination 객체를 생성하여 페이지네이션 처리에 필요한 데이터를 계산하고,

SearchDto 타입의 객체인 params의 pagination에 계산된 페이지 정보를 저장합니다.

 

스프링 간단한 페이징 - seupeuling gandanhan peijing
PostMapper의 findAll 쿼리 구조

PostMapper의 findAll 쿼리는 pagination 객체의 limitStart를 이용해서 LIMIT 구문을 실행하는데요.

SearchDto 타입의 객체인 params의 pagination에 계산된 페이지 정보를 가진 객체를 저장해 주어야만
LIMIT 구문이 정상적으로 실행됩니다.

마지막으로 PagingResponse로 리스트 데이터와 계산된 페이지 정보를 전달하여
객체를 리턴하고 로직을 종료합니다.

 

13. 컨트롤러(Controller) 수정하기

마지막으로 PostController의 openPostList( ) 메서드입니다.

PostService의 findAllPost( ) 메서드의 리턴 타입이 변경되었으니,
변경된 데이터 타입을 뷰(HTML)로 전달해 주어야 합니다.

    /**
     * 게시글 리스트 조회
     * @param params - search conditions
     * @return 게시글 리스트
     */
    List<PostResponse> findAll(SearchDto params);

    /**
     * 게시글 수 카운팅
     * @param params - search conditions
     * @return 게시글 수
     */
    int count(SearchDto params);
1

 

14. HTML 처리하기

이제, 계산된 페이지네이션 정보를 기준으로 화면 하단에 페이지 번호를 출력하고,

페이지 번호를 클릭했을 때, 원하는 페이지로 이동하도록 처리해주면 되는데요.

Thymeleaf를 이용해서 HTML 자체에서 처리해주어도 되지만, 좀 더 클린한 소스 코드를 위해

JS를 이용하여 리스트와 페이지네이션 HTML을 그려보도록 하겠습니다.

우선 list.html의 content 프래그먼트(fragment)를 다음과 같이 변경해 주세요.

(Diffchecker에서 기존 코드와 변경된 코드를 비교해 보실 수 있습니다!)

    /**
     * 게시글 리스트 조회
     * @param params - search conditions
     * @return 게시글 리스트
     */
    List<PostResponse> findAll(SearchDto params);

    /**
     * 게시글 수 카운팅
     * @param params - search conditions
     * @return 게시글 수
     */
    int count(SearchDto params);
2

 

<tbody id="list"></tbody>

게시글 리스트 HTML을 렌더링 할 영역입니다.

기존에는 Thymeleaf의 th:each를 이용하여 HTML을 그렸으나,
이번에는 자바스크립트 코드로 HTML을 그리게 됩니다.

 

<div class="paging"></div>

페이지네이션 HTML을 렌더링 할 영역입니다.

리스트 HTML을 그린 후, 페이지 번호 HTML을 화면에 그리게 됩니다.

 

15. 자바스크립트 함수 정의하기

다음은 자바스크립트 영역입니다. list.html에 다음의 코드를 작성해 주세요.

각 함수는 코드 작성이 끝난 후, 순서대로 설명해 드리도록 하겠습니다.

    /**
     * 게시글 리스트 조회
     * @param params - search conditions
     * @return 게시글 리스트
     */
    List<PostResponse> findAll(SearchDto params);

    /**
     * 게시글 수 카운팅
     * @param params - search conditions
     * @return 게시글 수
     */
    int count(SearchDto params);
3

 

findAllPost( )

스프링 간단한 페이징 - seupeuling gandanhan peijing
findAllPost( ) 함수 구조

onload( ) 함수에서 호출하는 해당 함수는 페이지가 로드되었을 때, 딱 한 번만 실행되는 함수입니다.

76번 라인의 list는 PagingResponse의 멤버인 List<T> 타입의 list를 의미합니다.

77~80번 라인의 코드를 통해 리스트가 비어있는 경우, "검색 결과가 없다"는 메시지를 행에 출력하고,

페이지네이션 HTML을 제거(초기화)한 후 로직을 종료합니다.

82번 라인의 pagination은 PagingResponse의 멤버인 pagination을 의미하며,

83번 라인의 params는 컨트롤러에서 @ModelAttribute를 이용하여 뷰로 전달한

SearchDto 타입의 객체인 params를 의미합니다.

84번 라인의 num은 리스트에 출력되는 게시글 번호를 처리하기 위해 사용되는 변수입니다.

 

추가적으로 리스트에 보여지는 데이터 번호를 PK로 출력하는 경우를 몇 번 보았는데요.

절대 절대 절대로 안됩니다... (선배님들에게 혼나실 가능성이 굉장히 높아요...)

 

drawList( )

스프링 간단한 페이징 - seupeuling gandanhan peijing
drawList( ) 함수 구조

findAllPost( )에서 호출하는 함수로, list와 num을 파라미터로 전달받습니다.

기존에 Thymyleaf를 이용하여 리스트 데이터를 그리던 것과 동일한 로직입니다.

차이점이 있다면, 기존에는 게시글 번호를 (전체 데이터 수 - loop의 인덱스 번호)로 처리했다면,

현재는 전체 데이터 수 - ((현재 페이지 번호 - 1) * 페이지당 출력할 데이터 개수)로

정밀하게 페이지 번호를 계산한다는 점입니다.

(dayjs는 날짜 관련 라이브러리로 이전에 설명드렸기에 자세한 설명은 생략하겠습니다!)

 

drawPage( )

스프링 간단한 페이징 - seupeuling gandanhan peijing
drawPage( ) 함수 구조

해당 함수는 화면 하단에 페이지 번호를 그리는 역할을 하는 함수입니다.

여기서 모든 로직은 서비스(Service)에서 계산된 Pagination 객체를 기준으로 처리됩니다.

 

스프링 간단한 페이징 - seupeuling gandanhan peijing
124~129번 라인

먼저 124~129번 라인은 이전 페이지가 있는 경우, 즉 시작 페이지(startPage)가 1이 아닌 경우에
첫 페이지 버튼과 이전 페이지 버튼을 HTML에 추가합니다.

 

스프링 간단한 페이징 - seupeuling gandanhan peijing
132~138번 라인

다음으로 132~138번 라인은 시작 페이지(startPage)와 끝 페이지(endPage) 사이의 페이지 번호를
넘버링하는 역할을 합니다.

만약, 현재 페이지 번호( params.page )와 그려야 할 페이지 번호( i )가 같으면,
해당 페이지 번호를 활성화(on) 처리합니다.

 

스프링 간단한 페이징 - seupeuling gandanhan peijing
141~148번 라인

다음으로 141~146번 라인의 로직은 현재 위치한 페이지 뒤에 데이터가 있는 경우,
다음 페이지 버튼과 끝 페이지 버튼을 HTML에 추가합니다.

그리고, 마지막 148번 라인의 코드를 통해 그려진 HTML을 페이지네이션 영역에 렌더링 합니다.

여기서 포인트는 movePage( ) 함수입니다. 각 태그에 선언된 onclick 이벤트를 통해 페이지를 이동하는데,
movePage( )의 인자로 전달하는 페이지 번호만 확실하게 이해하고 넘어가 주시면 되겠습니다.

 

movePage( )

스프링 간단한 페이징 - seupeuling gandanhan peijing
moePage( ) 함수 구조

설명이 필요 없을 정도로 심플한 함수입니다.

페이지 번호(page)를 전달받아 페이지를 이동하는 것이 전부입니다.

여기서 location.href에 사용된 location.pathname은 URI("/post/list.do")를 의미하며,

new URLSearchParams( ).toString( )은 queryParams에 담긴 프로퍼티(Key - Value)를
쿼리 스트링으로 변환해주는 역할을 합니다.
(해당 함수가 리턴해주는 값을 브라우저 콘솔(console)에 찍어보시면 이해가 쉬우실 거에요!)

 

16. 애플리케이션 실행하기

드디어! 대망의 페이지네이션 처리가 마무리되었습니다.

애플리케이션을 실행해서 페이징이 정상적으로 작동하는지 확인해 보도록 하겠습니다.

먼저, 게시글 리스트 페이지에서 5페이지를 클릭했을 때의 결과입니다.

이미지를 클릭해서 URI를 확인해보면 파라미터가 올바르게 연결되어 있음을 확인할 수 있습니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
게시글 리스트 5페이지

 

이번에는 다음 페이지로 이동하는 > 버튼을 세 번 클릭했을 때의 결과입니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
게시글 리스트 31페이지

 

이번에는 가장 마지막 페이지인 끝 페이지로 이동하는 >> 버튼을 클릭했을 때의 결과입니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
게시글 리스트 마지막 페이지

 

이번에는 끝 페이지에서 이전 페이지로 이동하는 < 버튼을 세 번 클릭했을 때의 결과입니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
게시글 리스트 610페이지

 

마지막으로 가장 앞 페이지인 1페이지로 이동하는 << 버튼을 클릭했을 때의 결과입니다.

스프링 간단한 페이징 - seupeuling gandanhan peijing
게시글 리스트 1페이지

 

마무리

이렇게 해서, 게시판 페이징 기능이 완성되었습니다.

하지만, 아직 개선해야 할 부분이 몇 가지 남아있습니다.


1. 검색 유형(searchType), 검색 키워드(keyword)를 이용한 게시글 검색 처리

2. SearchDto 객체의 멤버를 이용하여 이전 페이지 정보 기억하기


2번의 내용은 다음과 같습니다.

예를 들어, 15페이지에 있는 45번 게시글의 상세 페이지로 이동하면

45번 게시글 상세 페이지에서 15페이지에 대한 값을 파라미터로 받아두고,

수정 페이지로 이동하거나, 수정/삭제 또는 뒤로 가기를 클릭했을 때

받아둔 파라미터 값을 이용해서 다시 기존에 머무르던 15페이지로 이동해야 합니다.

 

이번 포스팅에서 모두 마무리 하기에는 너무 길어지는 감이 있기에

두 번으로 나누어 다음 글에 1, 2번과 관련된 내용을 포스팅하려고 합니다.

넓은 마음으로 이해해 주시면 감사하겠습니다. ( - 꾸벅 - )

오늘도 방문해 주신 여러분께 진심으로 감사드립니다!

좋은 하루 보내세요 :)


프로젝트를 import 해서 사용하신다면, application.properties에서 DB 정보만 올바르게 변경해서 사용해 주세요!


Board.zip

0.62MB

공유하기

게시글 관리

구독하기Let's Develop

'Spring Boot' 카테고리의 다른 글

스프링 부트(Spring Boot ) - REST API 알아보기 [개발을 시작해봐요!]  (32)2022.09.23스프링 부트(Spring Boot ) - 검색(페이징) 처리하기 [개발을 시작해봐요!]  (64)2022.08.24스프링 부트(Spring Boot) - AOP와 트랜잭션(Transaction) [개발을 시작해봐요!]  (46)2022.08.18스프링 부트(Spring Boot) - 인터셉터(Interceptor) 적용하기 [개발을 시작해봐요!]  (16)2022.08.18스프링 부트(Spring Boot) - Logback을 사용하여 쿼리 로그 출력하기 [개발을 시작해봐요!]  (31)2022.08.18