1. 페이징할때의 규칙
만약 100개의 게시글이 있는데 한번에 100개의 게시글이 보여진다면 매우 비효율적일 것이다.
그러므로 페이징 처리를 해야 한다.
만약 총 게시글의 수가 55개라면 한페이지당 10개씩 보여져야 할 때 페이지 수는 6개여야 할 것이다.
페이지의 끝 번호를 알기 위해선 총 게시글의 수를 알아야 한다.
먄악 총 게시글의 수가 155개라면 다음 버튼으로 다음 페이지를 이용해야 할 것이다.
시작 페이지의 번호가 1이 아니라면 이전 버튼이 필요할 것이다.
만약 총 게시글의 수가 255개라면
현재 페이지의 번호가 1~10이면 시작 번호는 1이어야 할 것이고
현재 페이지의 번호가 11~20이면 시작 번호는 11이어야 할 것이고
현재 페이지의 번호가 21~26이면 시작 번호는 21이어야 할 것이다.
페이징 처리를 하면서 알아두어야 할 것이 있다.
- 페이징 처리는 반드시 Get 방식만을 이용해야 한다.
- 게시글 목록 페이지 하단에 페이지들의 번호를 보여주고 원하는 번호를 선택하면 해당 페이지로 이동해서 목록을 보여주어야 한다.
- 페이징은 반드시 필요한 페이지 번호만 출력해야 할 것이다.
페이지당 10개의 글을 출력하는데 전체 게시글의 수가 55개라면 페이지 번호는 6까지여야 할 것이다. - 이전과 다음 버튼이 필요하다. 페이지당 버튼을 하나하나 만들면 100개의 페이지면 100개의 버튼이 생길 것이다.
- 게시글을 조회하거나 수정이나 삭제할 때 원래의 목록 페이지로 이동해야 할 것이다.
6페이지에 있는 게시글을 수정할 경우 수정완료시 다시 6페이지로 와야 할 것이다.
2. SQL에서의 처리 방법 ( Limit )
select * from board_table;
만약 100개의 글을 select 문으로 그냥 조회할 경우 목록이 100개가 될 것이다.
그러므로 반드시 페이징 처리를 해야 하는데 MySQL에서는 Limit을 사용해서 페이징 처리를 해야 한다.
Database에 따라 페이징 처리를 할 수 있는 쿼리문이 다르기 때문에 조심해야 할 것이다.
select * from 테이블명 order by 게시글번호 desc limit 시작 행, 출력할 갯수;
Limit을 사용해서 특정 페이지의 게시글을 보여줄 수 있다.
게시글 번호가 먼저 써진것이 위로 나오게 해야 하기 때문에 내림차순으로 정렬 후 출력할 행부터 출력할 갯수를 입력한다.
Limit 0, 10이라고 설정하면 0행부터 10개까지의 글이 조회될것이다.
적용 쿼리 보기
1페이지 = 0행 ~ 10행 ( 10개 ) Limit 0,10
select * from board_table order by id desc limit 0,10;

2페이지 = 10행 ~ 20행 ( 10개 ) Limit 10,10
select * from board_table order by id desc limit 10,10;

이렇게 페이징 처리가 되어야 할 것이다.
특정 페이지의 첫번째 게시글의 행 번호와 출력할 갯수만 전달한다면 특정 페이지의 게시글을 조회할 수 있을 것이다.
전달받을 파라미터를 각각 pageStart와 perPageNum으로 정하면 아래와 같은 쿼리문으로 페이징 처리를 하면 될 것이다.
select * from board_table order by id desc limit #{pageStart},#{perPageNum};
- pageStart = 특정 페이지의 첫번째 게시글의 행
- perPageNum = 한 페이지당 보여줄 게시글의 개수
특정 페이지를 조회하기 위해 두개의 파라미터를 담는 클래스를 만들어야 한다.
클래스를 만드는 이유는 게시판에도 여러 종류의 게시판이 존재할텐데 이렇게 페이징과 관련된 기능들을 별도의 클래스로 분리해놓는다면 재활용이 가능할 것이다.
3. 특정 페이지 조회를 위한 PageDTO 클래스
게시글 조회 쿼리에 전달될 파라미터를 담게되는 클래스이다.
package com.icia.board.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class PageDTO {
// page = 기본 페이지 1페이지
// perPageNum = 페이지당 보여줄 수
private int page;
private int perPageNum;
// 특정 페이지의 게시글 시작 번호 , 게시글 시작 행 번호
public int getPageStart() {
return (this.page-1) * perPageNum;
}
// 기본생성자
public PageDTO() {
this.page = 1;
this.perPageNum = 3;
}
// 페이지 주소 알아내기
public int getPage() {
return page;
}
// 페이지가 음수값 안되게 , 음수되면 1로 리턴
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;
}
}
}
- int page = 현재 페이지 번호
- int perPageNum = 한페이지당 보여줄 게시글의 갯수
- int getPageStart() = 특정 페이지의 게시글 시작 번호 , 게시글 시작 행 번호
- 현재 페이지의 게시글 시작 번호 = ( 현재 페이지 번호 - 1 ) * 페이지당 보여줄 게시글 갯수
public int getPageStart() {
return (this.page-1) * perPageNum;
}
현재 페이지 번호 | 페이지당 보여줄 게시글 수 | 계산식 | 게시글 시작 행 번호 |
6 | 10 | (6-1)*10 | 50 |
3 | 5 | (3-1)*5 | 10 |
17 | 10 | (17-1)*10 | 160 |
- PageDTO의 기본 생성자
public PageDTO() {
this.page = 1;
this.perPageNum = 10;
}
최초로 게시판 목록에 들어왔을 시 기본 세팅을 해야한다.
왜냐하면 페이징을 처리하기 위해선 현재 페이지와 페이지당 게시글 수가 필요한데 처음 게시판에 들어오면 두개의 정보를 가져올 방법이 없기 때문에 기본 생성자를 통해 기본값을 세팅하는 것이다.
현재 페이지를 1페이지 , 페이지당 보여줄 게시글의 수를 10개로 기본 세팅해두었다.
Setter
- 잘못된 값들이 세팅되지 않도록 set 메소드를 세팅한다
- setPage() = 페이지가 음수값이 되지 않게 설정, 음수가 되면 1페이지로 바꾼다.
- setPerPageNum() : 페이지당 보여줄 게시글 수가 변하지 않게 설정한다.
Getter
- get 메소드를 세팅한다.
4. 게시판 페이징을 처리하는 Page 클래스
페이징과 관련된 버튼들을 만드는 기능을 하는 클래스이다.
페이지에 페이징 버튼들을 만들기 위한 계산 클래스라고 생각하면 될것이다.
package com.icia.board.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class Page {
// PageDTO pageDTO.getPage() = 현재 페이지 번호
// PageDTO pageDTO.getPerPageNum() = 한 페이지당 보여줄 게시글의 갯수
// int totalCount = 총 게시글 수
// int endPage = 화면에 보여질 마지막 페이지 번호 , 끝 페이지 번호
private PageDTO pageDTO;
private int totalCount; // 글의 총 개수
private int startPage; // 페이지 가장 왼쪽 숫자
private int endPage; // 페이지 가장 오른쪽 숫자
private boolean prev; // 왼쪽 버튼
private boolean next; // 오른쪽 버튼
private int displayPageNum = 10; // 한번에 몇페이지를 보여줄것인가
public PageDTO getPageDTO() {
return pageDTO;
}
public void setPageDTO(PageDTO pageDTO) {
this.pageDTO = pageDTO;
}
public int getTotalCount() {
return totalCount;
}
// 총 게시글 수를 세팅할때 calcData() 메소드로 페이징 관련 버튼 계산
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
calcData();
}
// 페이징 버튼들을 생성하는 계산식 , 끝 페이지 번호 , 시작 페이지 번호 , 이전 ,다음 버튼
private void calcData() {
// Math.ceil = 소수점 올림처리
endPage = (int) (Math.ceil(pageDTO.getPage() / (double) displayPageNum) * displayPageNum);
startPage = (endPage - displayPageNum + 1);
if (startPage <= 0) startPage = 1;
int tempEndPage = (int) (Math.ceil(totalCount / (double) pageDTO.getPerPageNum()));
//전체 페이지 갯수가 계산한 endPage보다 작을 때는 endPage 값을 tempEndPage 값과 같게 세팅
if (endPage > tempEndPage) {
endPage = tempEndPage;
}
prev = startPage == 1 ? false : true;
next = endPage * pageDTO.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;
}
}
총 게시글 수를 세팅할 때 calsData() 메소드를 호출하여 페이징 관련 버튼 계산을 한다.
calsData() 메소드로 페이징의 버튼들을 생성하는 계산식을 실행한다.
각 끝 페이지 번호 , 시작 페이지 번호 , 이전 , 다음 버튼들을 구한다.
- PageDTO pageDTO.getPage() : 현재 페이지 번호
- PageDTO pageDTO.getPerPageNum() : 한 페이지당 보여줄 게시글의 갯수
- int totalCount : 총 게시글 수
- int endPage : 화면에 보여질 마지막 페이지 번호 , 끝 페이지 번호
endPage = (int) (Math.ceil(pageDTO.getPage() / (double) displayPageNum) * displayPageNum);
끝 페이지 번호 = ( 현재 페이지 번호 / 화면에 보여질 페이지 번호의 갯수 ) * 화면에 보여질 페이지 번호의 갯수
Math.ceil이란 소숫점 올림처리를 말한다.
현재 페이지 번호 | 페이지 번호의 갯수 | 계산식 | 끝 페이지 번호 |
1 | 10 | Math.ceil(1/10)*10 | 10 |
5 | 10 | Math.ceil(5/10)*10 | 10 |
16 | 10 | Math.ceil(16/10)*10 | 20 |
54 | 20 | Math.ceil(54/20)*20 | 60 |
끝 페이지 번호는 총 게시글 수와 연관이 있다.
100개의 게시글을 20개씩 보여준다고 하면 끝 페이지의 번호가 5여야 하는데 위의 계산식으로 계산을 하게 되면 끝 페이지 번호가 20이 나오게 된다 ( Math.ceil(1/20)*20 = 20 ) 그래서 총 게시글 수와 페이지당 보여줄 게시글의 갯수로 마지막 페이지 번호를 구해 서로 비교해서 화면에 보여질 끝 페이지 번호를 구해야 한다.
int tempEndPage = (int) (Math.ceil(totalCount / (double) pageDTO.getPerPageNum()));
//전체 페이지 갯수가 계산한 endPage보다 작을 때는 endPage 값을 tempEndPage 값과 같게 세팅
if (endPage > tempEndPage) {
endPage = tempEndPage;
}
마지막 페이지 번호 = 총 게시글 수 / 한 페이지당 보여줄 게시글의 갯수
마지막 페이지의 번호를 구한 뒤 , 끝 페이지 번호보다 작은 경우에 마지막 페이지의 번호를 끝 페이지 번호로 저장해준다.
화면에 보여질 끝 페이지 번호는 마지막 페이지의 번호보다 클 수는 없다.
그렇기 때문에 위와 같은 조건을 넣어줘야 한다.
이 부분은 시작 페이지 번호까지 구한 뒤에 처리해줘야 한다.
- int startPage : 화면에 보여질 첫번째 페이지 번호 , 시작 페이지 번호
startPage = (endPage - displayPageNum + 1);
if (startPage <= 0) startPage = 1;
시작 페이지 번호 = ( 끝 페이지 번호 - 화면에 보여질 페이지 번호의 갯수 ) + 1
끝 페이지 번호 | 페이지 번호의 갯수 | 계산식 | 시작 페이지 번호 |
10 | 10 | (10-10)+1 | 1 |
30 | 10 | (30-10)+1 | 21 |
40 | 20 | (40-20)+1 | 21 |
75 | 5 | (75-5)+1 | 71 |
시작 페이지 번호를 구할 때 , 마지막 페이지 번호가 화면에 보여질 페이징 버튼의 갯수보다 작으면 문제가 생기므로
시작 페이지 번호가 음수가 되어버리는 상황이 발생할 수 있다.
예를 들면 끝 페이지 번호가 3이고 보여줄 페이지 갯수가 5라면 시작 페이지 번호는 -1이 된다.
따라서 구한 시작페이지 번호가 0보다 작으면 ( 음수 ) 시작 페이지를 1로 해주는 로직을 추가해야 한다.
- boolean prev : 이전 버튼 생성 여부
prev = startPage == 1 ? false : true;
이전 버튼 생성 여부 = 시작 페이지 번호 == 1 ? false : true
이전 버튼은 시작 페이지 번호가 1이 아니면 생기면 된다.
- boolean next : 다음 버튼 생성 여부
next = endPage * pageDTO.getPerPageNum() < totalCount ? true : false;
다음 버튼 생성 여부 = 끝 페이지 번호 * 한 페이지당 보여줄 게시글의 갯수 < 총 게시글의 수 ? true : false
끝 페이지 번호 | 페이지당 게시글 수 | 총 게시글 수 | 계산식 | 다음 버튼 생성 여부 |
7 | 10 | 65 | 7*10<65 | false |
15 | 10 | 100 | 15*10<100 | false |
10 | 10 | 127 | 10*10<127 | true |
20 | 20 | 260 | 20*20<260 | false |
- int displayPageNum : 화면 하단에 보여지는 페이지 버튼의 수
보여지는 버튼의 수를 정한다.
private int displayPageNum = 10;
본인은 10개씩 보여줄것이므로 10 으로 작성했다.
Page 객체를 사용하려면 setpageDTO()와 setTotalCount()를 먼저 호출해서 값을 세팅해야 하므로 페이지 버튼들의 값을 구하려면 제일 먼저 총 게시글 수가 있어야 위의 계산식을 모두 사용할 수 있다.
그러므로 총 게시글을 세팅할 때 계산식 메소드를 호출하게 한 것이다.
또한 PageDTO 객체에서 필요한 page와 perPageNum을 사용하기 위해서 setpageDTO를 먼저 세팅해야 한다.
컨트롤러에서 객체로 값을 세팅할 때 유의해야 한다.
5. 목록 조회
5-1 Controller
@GetMapping("/")
public ModelAndView BoardList(PageDTO pageDTO) throws Exception {
ModelAndView mav = new ModelAndView(("boardPages/boardList"));
int total = boardService.total();
Page page = new Page();
page.setPageDTO(pageDTO);
page.setTotalCount(total);
List<Map<String, Object>> list = boardService.selectBoardList(pageDTO);
mav.addObject("List",list);
mav.addObject("page",page);
System.out.println("page = " + page);
return mav;
}
public ModelAndView BoardList(PageDTO pageDTO) throws Exception
현재 페이지 번호와 페이지당 보여줄 게시글 수가 담긴 PageDTO 타입의 pageDTO 객체를 사용
Controller
int total = boardService.total();
Service
public int total() {
return boardRepository.total();
}
Repository
public int total() {
Long result = sql.selectOne("Board.total");
int re = Integer.parseInt(String.valueOf(result));
return re;
}
Mapper
<select id="total" resultType="Long">
select count(id) from board_table
</select>
총 글의 갯수를 알아내기 위해 메소드를 호출하여 mapper에서 select문으로 count를 이용해 총 글의 수를 알아낸다.
Page page = new Page();
page 객체를 생성한다.
page.setPageDTO(pageDTO);
page.setTotalCount(total);
pageDTO 안의 page와 perPageNum을 세팅한다.
List<Map<String, Object>> list = boardService.selectBoardList(pageDTO);
mav.addObject("List",list);
mav.addObject("page",page);
세팅된 page에는 페이징을 위한 버튼의 값들으 들어있고 ModelAndView를 사용해 jsp로 넘겨준다.
( ModelAttribute를 사용해도 된다 )
5-2 Service
public List<Map<String, Object>> selectBoardList(PageDTO pageDTO) {
return boardRepository.selectBoardList(pageDTO);
}
pageDTO를 매개변수로 Map<String , Object>가 들어있는 리스트를 리턴한다.
5-3 Repository
public List<Map<String, Object>> selectBoardList(PageDTO pageDTO) {
return sql.selectList("Board.selectBoardList",pageDTO);
}
Service와 마찬가지로 같은 매개변수로 리스트를 리턴하게 둔다.
5-4 Mapper ( SQL )
<select id="selectBoardList" parameterType="HashMap" resultType="HashMap">
<![CDATA[
select id,boardWriter,boardPass,boardTitle,boardContents,boardCreatedDate,boardHits,
case when fileAttached=1 then '파일있음'
when fileAttached=0 then '파일없음' else '잘못입력' end as 'fileAttached' from board_table order by id desc limit #{pageStart}, #{perPageNum}
]]>
</select>
페이징 조회를 위해 Limit를 이용해서 쿼리를 수행한다.
Map 리스트의 경우 Key값을 입력해서 Value값을 뽑을 수 있기 때문에 limit #{pageStart}, #{perPageNum}을 사용하였다.
5-5 JSP
<ul class="pagination justify-content-center">
<c:if test="${page.prev}">
<li class="page-item">
<a class="page-link" href='<c:url value="?page=${page.startPage-1}"/>'><i
class="bi bi-caret-left-fill"></i></a>
</li>
</c:if>
만약 page.prev가 false면 if문으로 인해 해당 왼쪽 버튼이 나오지 않는다.
true면 왼쪽 버튼이 나오게 된다.
<c:forEach begin="${page.startPage}" end="${page.endPage}" var="pageNum" step="1">
<c:choose>
<c:when test="${pageNum eq page.pageDTO.page}">
<li class="page-item active">
<a class="page-link">${pageNum}</a>
</li>
</c:when>
<c:otherwise>
<li class="page-item">
<a class="page-link" href='<c:url value="?page=${pageNum}"/>'><i class="fa">${pageNum}</i></a>
</li>
</c:otherwise>
</c:choose>
c:forEach로 startPage가 endPage까지 갈때까지 반복문을 돌리고
c:choose로 만약 pageNum ( 페이지 번호 ) eq ( == ) page.pageDTO.page ( 해당 페이지 번호 ) 라면
bootstrap의 class로 active를 줘서 색을 다르게 하고 만약 위 조건이 false라면 숫자만 호출한다.
<c:if test="${page.next && page.endPage >0}">
<li class="page-item">
<a class="page-link" href='<c:url value="?page=${page.endPage+1}"/>'><i
class="bi bi-caret-right-fill"></i></a>
</li>
</c:if>
page.next가 true고 page.endPage>0가 true 일때 오른쪽 버튼을 활성화시킨다.