IT/development

[springBoot] 페이지네이션 처리(feat. MariaDB)

알 수 없는 사용자 2022. 11. 19. 06:07
반응형

목차

    페이지네이션 처리 관련해서 미래의 내가 보기 위해 기록 😃

    웹개발의 기초 중 기초라고 할 수 있는 페이지네이션(페이징) 처리에 대해서 간략하게 정리

    그동안은 원리를 제대로 이해하지 못하고 썼다면 이제 핵심위주로 잘 정리해 보자.

    시간 상 페이징 처리는 디자인은 위처럼 하나도 안 이쁘지만 데이터가 잘 나오는지 위주로 작성함

    개발환경

    back-end: springBoot 2.6.13(jdk 1.8)/mybatis 3.5.9/h2 database H2 2.1.214 (2022-06-13)(mode는 mySQL로 설정)

    front-end: thymeleaf/javascript/jQuery

     

    예시를 위한 테이블 

    CREATE TABLE t_board(
                        board_id bigint auto_increment,
                        title varchar (30),
                        content varchar (30),
                        name varchar (30),
                        reg_date timestamp,
                        update_date timestamp,
                        primary key(board_id)
    );

     

    테스트 데이터 넣는 것은 생략

    DBMS는 mysql 기준으로 작성

    쿼리에서 데이터를 제한 걸어서 가져와야 함

    limit절 설명은 생략함(시작값, 가져올 데이터 건수)

     

    쿼리는 limit만 걸어서 가져오면 된다.

    다만 limit절 뒤의 parameter가 화면에서 사용자가 클릭해서 넘어온 값들로 변수 처리되어야 한다는 것

    1페이지는 limit 0, 10

    2페이지는 limit 10,10

    3페이지는 limit 20,10 이렇게 처리가 되어야 한다.

    mapper xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!-- column과 vo의 field명이 상이해서 resultMap 선언 -->        
    <mapper namespace="paging.study.mapper.BoardMapper">
        <resultMap id="boardMap" type="paging.study.domain.vo.BoardVO">
            <id column="board_id" property="boardId"/>
            <result column="title" property="title"/>
            <result column="content" property="content"/>
            <result column="name" property="name"/>
            <result column="reg_date" property="regDate"/>
            <result column="update_date" property="updateDate"/>
        </resultMap>
        <!-- 현재 페이지 번호와 가져올 데이터 개수가 있는 Criteria를 parameter로 받아 limit절에 활용 -->
        <select id="findBoardListPaging" parameterType="paging.study.domain.Criteria" resultMap="boardMap">
            SELECT * FROM t_board
            ORDER BY reg_date DESC
            limit #{limitStart}, #{amount}
        </select>
    </mapper>

    Criteria(쿼리의 paramer에 활용)

    // 프론트에서 페이지 번호와 페이지 출력 개수를 넘기기 위한 클래스
    package paging.study.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    @Getter @Setter
    @ToString
    public class Criteria {
    
        private int pageNum;        // 페이지 번호(현재 페이지가 몇 페이지인지)
        private int amount;         // 한 화면에 출력한 페이지 개수
        private int limitStart;             // 쿼리에서 (pageNum -1) * amount 사용하기 위한 변수
    	
        // 기본 값 세팅(현재 페이지는 1, 가져올 데이터는 10건인 경우)
        public Criteria() {
            this.pageNum = 1;
            this.amount = 10;
        }
    
        public Criteria(int pageNum, int amount) {
            this.pageNum = pageNum;
            this.amount = amount;
        }
        
        // 쿼리에서 limit 1번 째 parameter로 쓸 값(mybatis에서 getter로 이 값을 사용)
        public int getLimitStart() {
            return this.limitStart = (pageNum - 1) * this.amount;
        }
    }

    BoardVO

    package paging.study.domain.vo;
    
    import lombok.*;
    
    import java.time.LocalDateTime;
    
    @Getter @Setter
    @ToString
    @NoArgsConstructor
    public class BoardVO {
    
        private Long boardId;
        private String title;
        private String content;
        private String name;
        private LocalDateTime regDate;
        private LocalDateTime updateDate;
    
        public BoardVO(String title, String content, String name) {
            this.title = title;
            this.content = content;
            this.name = name;
        }
    }

     

    mapper interface

    package paging.study.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    import paging.study.domain.Criteria;
    import paging.study.domain.vo.BoardVO;
    
    import java.util.List;
    
    @Mapper
    public interface BoardMapper {
    	
        List<BoardVO> findBoardListPaging(Criteria cri);
        // 전체 카운트
        @Select("SELECT COUNT(*) FROM t_board")
        int findBoardCount();
    }

    service

    package paging.study.service;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import paging.study.domain.Criteria;
    import paging.study.domain.vo.BoardVO;
    import paging.study.mapper.BoardMapper;
    
    import java.util.List;
    
    @Service
    @Slf4j
    @RequiredArgsConstructor
    public class BoardService {
    
        private final BoardMapper boardMapper;
    
        public List<BoardVO> findBoardListPaging(Criteria cri) {
            return boardMapper.findBoardListPaging(cri);
        }
        
        public int findBoardCount() {
            return boardMapper.findBoardCount();
        }
    }

    controller

    package paging.study.controller;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    import paging.study.domain.Criteria;
    import paging.study.domain.paging.PageMaker;
    import paging.study.domain.vo.BoardVO;
    import paging.study.service.BoardService;
    import paging.study.service.ReplyService;
    
    import java.util.List;
    
    @Controller
    @Slf4j
    @RequiredArgsConstructor
    public class BoardController {
    
        private final BoardService boardService;
    
        /**
         * content list
         * @param cri
         * @param model
         * @return
         */
        @GetMapping("/board/list")
        public String listPaging(@ModelAttribute("cri") Criteria cri, Model model) {
        	// 화면에서 cri를 받아서 목록 조회 서비스 호출시 parameter로 같이 넘김
            List<BoardVO> list = boardService.findBoardListPaging(cri);
            // 페이지네이션 화면 처리에 필요한 전체 목록 개수 구함
            int boardCount = boardService.findBoardCount();
            // model에 2 종류의 데이터(리스트, 페이지네이션 데이터)를 담아서 view에 전달
            model.addAttribute("list", list);
            // pageMaker에 2개의 parameter를 넘김
            model.addAttribute("pageMaker", new PageMaker(boardCount, cri));
            return "board/boardList";
        }
    }

     


    페이지네이션 화면 처리를 위한 클래스 😎

    // 프론트에서 넘긴 Criteria를 참조해서 실제 화면에서 페이지네이션 처리를 하기 위한 계산을 하는 클래스
    package paging.study.domain.paging;
    
    import lombok.Getter;
    import lombok.ToString;
    import paging.study.domain.Criteria;
    
    @Getter	// 외부에서 값 변경 불가하도록 setter는 선언하지 않음
    @ToString
    // 페이지네이션 화면에서 필요한 데이터 계산하는 클래스
    public class PageMaker {
        private int startPage;  //페이징 화면 하단의 시작 번호(5페이지라고 하면 [1][2][3][4][5] 여기서 제일 첫 번 째 시작번호)
        private int endPage;    //페이징 화면 하단의 끝 번호(5페이지라고 하면 [1][2][3][4][5] 여기서 제일 끝 번호)
        private boolean prev, next;     // 이전, 다음 존재 여부
    
        private int totalCount;         // 전체 게시글 수
        private Criteria cri;      // 프론트에서 전달하는 pageNum(현재 페이지), amount(출력 페이지) 전달 역할
    	
        // 게산식에 필요한 게 전체 카운트, 그리고 현재 페이지와 페이지 개수
        // 2개 parameter를 받아서 아래에서 계산 처리(공식이니 외울 필요는 없고 이런식으로 구하는 구나라고 이해하면 됨)
        public PageMaker(int totalCount, Criteria cri) {
            this.totalCount = totalCount;
            this.cri = cri;
    
            this.endPage = (int)(Math.ceil(cri.getPageNum()/10.0)) * 10;
            this.startPage = this.endPage - 9;
            // 실제 끝 번호
            int realEnd = (int)(Math.ceil((totalCount * 1.0) / cri.getAmount()));
    		// 만일 80개의 데이터가 있을 때 끝번호는 10이 아니라 8이 되어야 하기에 실제 끝번호와 비교해서 실제 끝 번호로 치환
            if(realEnd < this.endPage) {
                this.endPage = realEnd;
            }
    		// 이전, 다음
            this.prev = this.startPage > 1;
            this.next = this.endPage < realEn
        }
    }

    view(thymeleaf)

    <!-- 게시글 목록 영역 start -->
    <div>
            <table class="table table-striped">
                <thead>
                <tr>
                    <th>아이디</th>
                    <th>제목</th>
                    <th>내용</th>
                    <th>이름</th>
                </tr>
                </thead>
                <tbody>
                <!-- 게시글 목록 루프 돌며 표시 -->
                <tr th:each="item : ${list}">
                    <td th:text="${item.boardId}"></td>
                    <td th:text="${item.title}"></td>
                    <td th:text="${item.content}"></td>
                    <td th:text="${item.name}"></td>
                </tr>
                </tbody>
            </table>
        </div>
    <!-- // 게시글 목록 영역 end -->    
    
    <!-- 게시판 하단 페이지네이션 영역 start -->
            <nav aria-label="Page navigation">
                <ul class="pagination">
                    <!-- prev -->
                    <li class="page-item" th:if="${pageMaker.prev} == true">
                        <a class="page-link" th:href="@{/board/list(pageNum=${pageMaker.startPage}-1)}">Prev</a>
                    </li>
                    <!-- pageMaker의 startPage부터 endPage까지 루프, a태그의 href에 idx를 링크(get방식으로 pageNum을 붙여서) -->
                    <li class="page-item" id="paginate_btn" th:each="idx: ${#numbers.sequence(pageMaker.startPage, pageMaker.endPage)}" th:classappend="${pageMaker.cri.pageNum} == ${idx} ? active : null">
                        <a class="page-link" th:href="@{/board/list(pageNum=${idx})}" th:text="${idx}"></a>
                    </li>
                    <!-- next -->
                    <li class="page-item" th:if="${pageMaker.next} == true and ${pageMaker.endPage > 0}">
                        <a class="page-link" th:href="@{/board/list(pageNum=${pageMaker.endPage}+1)}">Next</a>
                    </li>
                </ul>
            </nav>
            <!-- // 게시판 하단의 페이지네이션 영역 end -->

    이렇게 하면 아래처럼 못생긴 페이지네이션 처리가 된 게시판 목록이 표시된다.

    데이터가 잘 나오는지 확인 후 아래처럼 디자인을 입히면 된다.

    테스트를 위해 급하게 만든거라 오탈자가 있거나 오류가 있을 수 있으니 알려주시면 감사드리겠습니다.

     

    반응형