본문으로 바로가기

이전에 만든 CRUD 게시판은 100개의 게시글이 있다면 한번에 100개의 게시글이 모두 보여진다. 정말 비효율적인 게시판이다..(ㅎ) 그래서 게시판 목록 페이징 처리를 하려고 한다. 그러면 좀 더 게시판 스럽지 않을까 싶다! 나는 만들어 놓은 CURD 게시판을 수정해서 작업을 하는 것이기 때문에 본인의 게시판에 맞게 적절히 섞어(?)주면 된다.


1. 페이징 규칙

먼저 페이징이 어떤식으로 처리되는 지 이해해보자. 페이지당 게시글을 10개씩 보여주고 페이징 버튼은 최대 10개까지만 보인다고 하자.


  • 총 게시글 수 77개
[1] [2] [3] [4] [5] [6] [7] [8]
총 게시글 수가 77이면 페이지 번호의 끝 번호가 8이여야 한다.
페이지의 끝 번호를 알기 위해서는 총 게시글의 수가 필요하다.

  • 총 게시글 수 135개
[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [다음]
[이전] [11] [12] [13]
화면에 보여줄 페이지 버튼의 수가 10개 이상이라면 다음 버튼을 이용해 다음 페이지를 이용할 수 있어야 한다.
시작 페이지의 번호가 1이 아니라면 이전 버튼을 이용해 이전 페이지를 이용할 수 있어야 한다.

  • 총 게시글 수 215개

[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [다음]

[이전] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [다음]

[21] [22]

현재 페이지의 번호가 1~10 사이라면 시작 번호는 1이여야 한다.

현재 페이지의 번호가 11~20 사이라면 시작 번호는 11이여야 한다.



2. 페이징 원칙

페이징 처리를 함에 있어서 원칙들이 있다. 

  • 페이징 처리는 반드시 GET 방식만을 이용해야 한다.
  • 게시글 목록 페이지의 하단에 페이지들의 번호를 보여주고 원하는 번호를 선택하면 해당 페이지로 이동해서 목록을 보여줘야 한다.
  • 페이징은 반드시 필요한 페이지 번호만 출력해야 한다. 만약 페이지당 10개의 게시글을 출력하는데 전체 게시글의 수가 42개라면 페이지의 번호는 5페이지까지여야 한다.
  • 이전과 다음 버튼이 존재해야 한다. 게시글이 1000개인데 1페이지당 10개의 게시글을 볼 수 있게 했다면 페이징 버튼은 100개가 필요하다. 그런데 이전과 다음 버튼이 존재하지 않는다면 100개의 버튼을 한번에 보여줘야 한다. 그렇기때문에 하단 페이지에 보여줄 버튼의 갯수를 정하고 더 많은 데이터가 있다면 이전과 다음 버튼으로 표시해줘야 한다.
  • 게시글을 조회하거나 수정, 삭제를 하고 난 뒤, 다시 원래의 목록 페이지로 이동해야 한다. 예를 들면 게시판에 5페이지에 있는 어떠한 게시물을 조회하거나 수정, 삭제를 했다고 한다면, '목록으로'버튼을 이용해 목록으로 돌아갈 땐 5페이지로 이동해야 한다.



3. SQL : LIMIT

페이징 처리를 확인하기 위해서 DB에 100개의 데이터를 넣어뒀다. 그럼 게시판 목록을 조회하는 쿼리를 실행하게 되면 어떻게 될까?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> SELECT ...
    -> FROM board
    -> WHERE DEL_CHK = 'N'
    -> ORDER BY IDX DESC;
+-----+--------------+---------+------------+-------------+
| IDX | TITLE        | HIT_CNT | CREA_DATE  | CREA_ID     |
+-----+--------------+---------+------------+-------------+
| 100 | 타이틀100    |       0 | 2018.11.26 | 테스트100    |
|  99 | 타이틀99A    |       0 | 2018.11.26 | 테스트99     |
|  98 | 타이틀98     |       0 | 2018.11.26 | 테스트98     |
|                      . . . .                            |
|   4 | 타이틀4      |       0 | 2018.11.26 | 테스트4      |
|   3 | 타이틀3      |       0 | 2018.11.26 | 테스트3      |
|   2 | 타이틀2      |       0 | 2018.11.26 | 테스트2      |
|   1 | 타이틀1      |       0 | 2018.11.26 | 테스트1      |
+-----+--------------+---------+------------+-------------+
100 rows in set (0.01 sec)
cs


100개의 게시글이 한번에 나올 것이다. 한 페이지(화면)에 100개 혹은 1000개의 게시글을 한번에 보여주기엔 벅차다. 그렇기에 게시글 목록에는 페이징 처리가 반드시 필요하다. DB에 따라 페이징 처리를 할 수 있는 쿼리가 존재한다고 한다. 그렇기에 각자 사용하는 DB를 확인해서 쿼리를 사용하면 된다. 나는 MySQL을 사용하기 때문에 LIMIT을 이용해서 페이징 처리를 할 것이다.


1
2
3
4
SELECT *
FROM 테이블명
ORDER BY 게시글 번호 DESC
LIMIT 시작 행, 출력할 갯수
cs


LIMIT을 위와 같이 사용하게 되면 특정 페이지의 게시글을 보여줄 수 있다. 게시글 번호로 내림차순 정렬한 데이터를 출력할 행부터 출력할 갯수까지 조회해주는 쿼리이다. 데이터의 행은 0부터 시작한다. 따라서 LIMIT 0, 10 이라고 설정하면 0행부터 10개까지의 게시글이 조회될 것이다. 그러면 페이징 처리를 하기 위해선 어떻게 조회를 해야 할까?


1페이지 -> 00행 ~ 09행 (10개) : LIMIT 0, 10

2페이지 -> 10행 ~ 19행 (10개) : LIMIT 10, 10

3페이지 -> 20행 ~ 29행 (10개) : LIMIT 20, 10



이런식으로 페이징 처리가 이루어져야 한다. 그래서 특정 페이지의 첫번째 게시글의 행번호와 출력할 갯수만 전달 받는다면 특정 페이지의 게시글들을 조호할 수 있을 것이다. 나는 전달받을 파라미터들을 각각 pageStart와 perPageNum으로 정했다. 그러면 아래와 같은 쿼리로 페이징 처리를 하면 된다.


1
2
3
4
5
SELECT ...
FROM board
WHERE DEL_CHK='N'
ORDER BY IDX DESC
LIMIT #{pageStart}, #{perPageNum}
cs
  • pageStart : 특정 페이지의 첫번째 게시글의 행이다.
  • perPageNum : 한 페이지당 보여줄 게시글의 개수이다.

특정 페이지를 조회하기 위해 두 개의 파라미터를 담는 클래스를 만들 것이다. 이렇게 클래스를 만드는 이유는 뭘까? 게시판에도 여러 종류의 게시판이 존재한다. 그러면 게시판 마다 페이징 조회 로직을 생성해야 한다. 하지만 페이징 관련된 기능들을 별도의 클래스로 분리해놓는다면 조금 더 효율적인 작업을 할 수 있다. 



4. 특정 페이지 조회를 위한 Criteria 클래스

게시글 조회 쿼리에 전달될 파라미터를 담게 될 클래스이다. VO라고 생각하면 쉬울 것이다.


Criteria

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Criteria {
    
    private int page;
    private int perPageNum;
    
    public int getPageStart() {
        return (this.page-1)*perPageNum;
    }
    
    public Criteria() {
        this.page = 1;
        this.perPageNum = 10;
    }
    
    public int getPage() {
        return page;
    }
    public void setPage(int page) {
        if(page <= 0) {
            this.page = 1;
        } else {
            this.page = page;
        }
    }
    public int getPerPageNum() {
        return perPageNum;
    }
    public void setPerPageNum(int pageCount) {
        int cnt = this.perPageNum;
        if(pageCount != cnt) {
            this.perPageNum = cnt;
        } else {
            this.perPageNum = pageCount;
        }
    }
    
}
cs
  • int page : 현재 페이지 번호
  • int perPageNum : 한 페이지당 보여줄 게시글의 갯수
  • int getPageStart() : 특정 페이지의 게시글 시작 번호, 게시글 시작 행 번호

1
2
//현재 페이지의 게시글 시작 번호 = (현재 페이지 번호 - 1) * 페이지당 보여줄 게시글 갯수
getPerPageNum = (this.page - 1* perPageNum
cs


 현재 페이지 번호

 페이지당 보여줄 게시글 수

 계산식

 게시글 시작 행 번호

 5

 10

 (5-1)*10

 40

 3

 5

 (3-1)*5

 10

 15

 10

 (15-1)*10

 140

  • Criteria() : 기본 생성자
1
2
this.page = 1;
this.perPageNum = 10;
cs


최초로 게시판 목록에 들어 왔을 때를 위해서 기본 셋팅을 해야 한다. 왜냐하면 페이징을 처리하기 위해선 현재 페이지 번호와 페이지당 게시글 수가 필요한데 처음 게시판에 들어오게 되면 두 개의 정보를 가져올 방법이 없기 때문에 기본 생성자를 통해 기본 값을 셋팅하도록 하자. 현재 페이지를 1페이지로, 페이지당 보여줄 게시글의 수를 10개로 기본 셋팅해두었다.

  • setter : 잘 못된 값들이 셋팅되지 않게 적절하게 set 메서드 셋팅
- setPage() : 페이지가 음수값이 되지 않게 설정. 음수가 되면 1페이지를 나타낸다.
- setPerPageNum() : 페이지당 보여줄 게시글 수가 변하지 않게 설정했다.
  • getter : get 메서드를 셋팅



5. 게시판 페이징을 처리하는 PageMaker 클래스

페이징에 관련된 버튼들을 만드는 기능을 하는 클래스이다. 페이지에 페이징 버튼들을 만들기 위한 계산 클래스라고 생각하면 된다. 그래서 계산이 필요한 부분이 많다.(ㅠㅠ) 하지만 그냥 공식이라 생각하고 이렇게 쓰는구나, 정도의 이해만 하고 넘어가도 된다.


PageMaker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class PageMaker {
    
    private Criteria cri;
    private int totalCount;
    private int startPage;
    private int endPage;
    private boolean prev;
    private boolean next;
    private int displayPageNum = 5;
    
    public Criteria getCri() {
        return cri;
    }
    public void setCri(Criteria cri) {
        this.cri = cri;
    }
    public int getTotalCount() {
        return totalCount;
    }
    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
        calcData();
    }
    
    private void calcData() {
        
        endPage = (int) (Math.ceil(cri.getPage() / (double) displayPageNum) * displayPageNum);
 
        startPage = (endPage - displayPageNum) + 1;
        if(startPage <= 0) startPage = 1;
        
        int tempEndPage = (int) (Math.ceil(totalCount / (double) cri.getPerPageNum()));
        if (endPage > tempEndPage) {
            endPage = tempEndPage;
        }
 
        prev = startPage == 1 ? false : true;
        next = endPage * cri.getPerPageNum() < totalCount ? true : false;
        
    }
    
    public int getStartPage() {
        return startPage;
    }
    public void setStartPage(int startPage) {
        this.startPage = startPage;
    }
    public int getEndPage() {
        return endPage;
    }
    public void setEndPage(int endPage) {
        this.endPage = endPage;
    }
    public boolean isPrev() {
        return prev;
    }
    public void setPrev(boolean prev) {
        this.prev = prev;
    }
    public boolean isNext() {
        return next;
    }
    public void setNext(boolean next) {
        this.next = next;
    }
    public int getDisplayPageNum() {
        return displayPageNum;
    }
    public void setDisplayPageNum(int displayPageNum) {
        this.displayPageNum = displayPageNum;
    }
 
}
cs

- 22행: 총 게시글 수를 셋팅할때 calcData() 메서드를 호출하여 페이징 관련 버튼 계산을 한다.

- 25행: 페이징의 버튼들을 생성하는 계산식을 만들었다. 끝 페이지 번호, 시작 페이지 번호, 이전, 다음 버튼들을 구한다.

  • Criteria cri.getPage() : 현재 페이지 번호
  • Criteria cri.getPerPageNum() : 한 페이지당 보여줄 게시글의 갯수
  • int totalCount : 총 게시글 수
1
2
SELECT count(*)
FROM board
cs

조회 쿼리에서 count(*)를 이용해서 구해야 한다. 총 게시글 수 쿼리는 조금 있다가 설정할 것이다.

  • int endPage : 화면에 보여질 마지막 페이지 번호, 끝 페이지 번호
1
2
//끝 페이지 번호 = (현재 페이지 번호 / 화면에 보여질 페이지 번호의 갯수) * 화면에 보여질 페이지 번호의 갯수
endPage = (int) (Math.ceil(cri.getPage() / (double) displayPageNum) * displayPageNum);
cs


 현재 페이지 번호

 페이지 번호의 갯수

 계산식

 끝 페이지 번호

 1

 10

 Math.ceil(1/10) * 10

 10

 3

 10

 Math.ceil(3/10) * 10

 10

 13

 10

 Math.ceil(13/10) * 10

 20

 23

 20

 Math.ceil(23/20) * 20

 40


끝 페이지 번호는 총 게시글 수와 연관이 있다. 근데 100개의 게시글을 20개씩 보여준다고 하면 끝 페이지의 번호가 5여야 하는데 위의 계산식으로 계산을 하게 되면 끝 페이지 번호가 20이 나오게 된다. ( Math.ceil(1/20)*20 = 20 ) 그래서 총 게시글 수와 페이지당 보여줄 게시글의 갯수로 마지막 페이지 번호를 구해 서로 비교해서 화면에 보여질 끝 페이지 번호를 구해야 한다.


1
2
3
4
5
//마지막 페이지 번호 = 총 게시글 수 / 한 페이지당 보여줄 게시글의 갯수
int tempEndPage = (int) (Math.ceil(totalCount / (double) cri.getPerPageNum()));
if (endPage > tempEndPage) {
    endPage = tempEndPage;
}
cs


마지막 페이지의 번호를 구한 뒤, 끝 페이지 번호보다 작은 경우에 마지막 페이지의 번호를 끝 페이지 번호로 저장해준다. 화면에 보여질 끝 페이지 번호는 마지막 페이지의 번호보다 클 수는 없다. 그렇기 때문에 위와 같은 조건을 넣어줘야 한다. 이 부분은 시작 페이지 번호까지 구한 뒤에 처리해줘야 한다.


  • int startPage : 화면에 보여질 첫번째 페이지 번호, 시작 페이지 번호
1
2
3
//시작 페이지 번호 = (끝 페이지 번호 - 화면에 보여질 페이지 번호의 갯수) + 1
startPage = (endPage - displayPageNum) + 1
if(startPage <= 0) startPage = 1
cs


 끝 페이지 번호

 페이지 번호의 갯수

 계산식

 시작 페이지 번호

 10

 10

 (10-10) + 1

 1

 30

 10

 (30-20) + 1

 11

 40

 20

 (40-20) + 1

 21

 20

 5

 (20-5) + 1

 16


시작 페이지 번호를 구할 때, 마지막 페이지 번호가 화면에 보여질 페이징 버튼의 갯수보다 작으면 문제가 생긴다. 시작 페이지 번호가 음수가 되어버리는 상황이 발생한다. 예를들면 끝 페이지의 번호가 3이고, 보여줄 페이지 갯수가 5라면, 시작 페이지 번호는 -1이 된다. 따라서, 구한 시작페이지 번호가 0보다 작으면(음수) 시작 페이지를 1로 해주는 로직을 추가해야 한다.

  • boolean prev : 이전 버튼 생성 여부
1
2
//이전 버튼 생성 여부 = 시작 페이지 번호 == 1 ? false : true
prev = startPage == 1 ? false : true
cs

이전 버튼은 시작 페이지 번호가 1이 아니면 생기면 된다.


  • boolean next : 다음 버튼 생성 여부
1
2
//다음 버튼 생성 여부 = 끝 페이지 번호 * 한 페이지당 보여줄 게시글의 갯수 < 총 게시글의 수 ? true: false
next = endPage * cri.getPerPageNum() < totalCount ? truefalse
cs


 끝 페이지 번호

 페이지당 게시글 수

 총 게시글 수

 계산식

 다음 버튼 생성 여부

 7

 10

 65

 7 * 10 < 65

 false

 10

 10

 100

 10 * 10 < 100

 false

 10

 10

 127

 10 * 10 < 107

 true

 20

 10

 260

 20 * 10 < 260

 true


  • int displayPageNum : 화면 하단에 보여지는 페이지 버튼의 수
보여지는 버튼의 수를 정하면 된다. 나는 5개만 보이게 할 것이다.

PageMaker 객체를 사용하려면 setCri() 와 setTotalCount()를 먼저 호출해서 값을 셋팅해야 한다. 페이징 버튼들의 값을 구하려면 제일 먼저 총 게시글 수가 있어야 위의 계산식대로 차례로 구할 수 있다. 그렇기에 총 게시글을 셋팅할 때 계산식 메소드를 호출하게 한 것이다. 또한 Criteria 객체에서 필요한 page 와 perPageNum을 사용하기 위해서 setCri()를 먼저 셋팅해야 한다. 컨트롤러에서 객체로 값을 셋팅할 때 유의하자.



6. 목록 조회 수정

** 이전에 만들어 둔 CURD 게시판을 수정해서 사용한다.


6-1. Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping(value="/board/boardList")
public ModelAndView openBoardList(Criteria cri) throws Exception {
        
    ModelAndView mav = new ModelAndView("/board/boardList");
        
    PageMaker pageMaker = new PageMaker();
    pageMaker.setCri(cri);
    pageMaker.setTotalCount(100);
        
    List<Map<String,Object>> list = boardServcie.selectBoardList(cri);
    mav.addObject("list", list);
    mav.addObject("pageMaker", pageMaker);
        
    return mav;
        
}
cs

- 02행: 현재 페이지 번호와 페이지당 보여줄 게시글 수가 담긴 Criteria 객체를 사용한다.

- 06행: PageMaker() 객체를 생성한다.

- 07행: page와 perPageNum을 셋팅해준다.

- 08행: 총 게시글의 수를 셋팅해준다. 아직 총 게시글 수를 조회하는 로직은 구현하지 않았기 때문에 현재 DB에 있는 총 게시글의 수인 100을 넣어줬다.

- 10행: 원래의 목록 조회 로직에서 Criteria 파라미터를 사용하기 위해 수정했다.

- 12행: 셋팅된 pageMaker에는 페이징을 위한 버튼의 값들이 들어있고 ModelAndView를 이용해 jsp에 넘겨준다.


6-2. Service 영역 및 DAO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//BoardService
List<Map<String, Object>> selectBoardList(Criteria cri);
 
//BoardServiceImpl
@Override
public List<Map<String, Object>> selectBoardList(Criteria cri) {
    return boardDAO.selectBoardList(cri);
}
 
//BoardDAO
@SuppressWarnings("unchecked")
public List<Map<String, Object>> selectBoardList(Criteria cri) {
    return (List<Map<String,Object>>)selectList("board.selectBoardList", cri);
}
cs

게시글 목록 조회하는 Service, DAO 영역에 Criteria 객체에 담긴 파라마터를 보내기 위해 수정했다. 각자의 목록 조회에 맞게 수정하면 된다. Criteria 객체에 담아서 SQL 매핑에 보낼 파라미터는 특정 페이지 게시글의 행(pageStart)과 페이지당 보여줄 게시글의 갯수(perPageNum)이다.


6-3. SQL

1
2
3
4
5
6
7
8
9
10
11
12
<select id="selectBoardList" resultType="hashmap" parameterType="hashmap">
    <![CDATA[
        SELECT
            *
        FROM
            board
        WHERE
            DEL_CHK = 'N'
        ORDER BY IDX DESC
        LIMIT #{pageStart}, #{perPageNum}
    ]]>
</select>
cs

- 10행: 페이징 조회를 위해 LIMIT를 이용해서 원래의 목록 조회 쿼리를 수정했다.


6-4. JSP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ul class="btn-group pagination">
    <c:if test="${pageMaker.prev }">
    <li>
        <a href='<c:url value="/board/boardList?page=${pageMaker.startPage-1 }"/>'><i class="fa fa-chevron-left"></i></a>
    </li>
    </c:if>
    <c:forEach begin="${pageMaker.startPage }" end="${pageMaker.endPage }" var="pageNum">
    <li>
        <a href='<c:url value="/board/boardList?page=${pageNum }"/>'><i class="fa">${pageNum }</i></a>
    </li>
    </c:forEach>
    <c:if test="${pageMaker.next && pageMaker.endPage >0 }">
    <li>
        <a href='<c:url value="/board/boardList?page=${pageMaker.endPage+1 }"/>'><i class="fa fa-chevron-right"></i></a>
    </li>
    </c:if>
</ul>
cs

- 02행: 이전 버튼의 생성 여부를 확인하여 버튼을 보여줄 것이다.

- 06행: 현재 페이지가 어디인 지 알기 위해 추가했다.

- 07행: 페이지의 시작 번호와 끝 번호를 이용해 페이지 버튼들을 뿌려줄 것이다.

- 12행: 다음 버튼의 생성 여부를 확인하여 버튼을 보여줄 것이다.



7. 총 게시글 갯수 구하기 (totalCount)

그럼 마지막으로 총 게시글 갯수를 구하는 로직을 만들자. 


7-1. SQL

1
2
3
4
5
6
7
8
9
10
<select id="countBoardList" resultType="Integer">
    <![CDATA[
        SELECT
            count(*)
        FROM
            board
        WHERE
            DEL_CHK='N'
    ]]>
</select>
cs

- 01행: 결과는 int로 리턴되어야 한다.

- 04행: 조회한 결과의 갯수를 뿌려주는 쿼리이다.

- 08행: 총 게시글 중에 삭제된 건 빼야 한다.


7-2. DAO

1
2
3
public int countBoardList(){
    return (Integer) selectOne("board.countBoardList");
}
cs


7-3. Service 영역

1
2
3
4
5
6
7
8
//Service
int countBoardListTotal();
 
//ServiceImpl
@Override
public int countBoardListTotal() {
    return boardDAO.countBoardList();
}
cs


7-4. Controller

1
 pageMaker.setTotalCount(boardServcie.countBoardListTotal());
cs

100으로 셋팅한 부분에 총 게시글 수를 구하는 로직을 넣었다.



8. 확인

밑에 페이징 버튼들이 나오고 10개의 게시글만 나왔다! 그럼 다른 페이지의 버튼를 눌러보자.

페이지에 따라 목록이 바뀐다. 이전, 다음 버튼도 알맞게 구동된다!


페이징 처리에 성공했지만 아직 개선해야 될 부분이 남아있다. 페이징 원칙에서도 언급했는데, 특정 페이지의 한 게시글을 조회, 수정, 삭제를 하고 난 후에 다시 목록으로 돌아가게 될 경우 그 특정 페이지로 돌아가야 한다. 그 부분만 해결된다면 페이징 처리는 완성된것이다.