4/13/2018

(구버전) 스프링 게시판 만들기 #6. 페이징 기능 구현

'(구버전) 스프링 게시판 만들기'는 내용이 부족하다고 판단하여
스프링 게시판 만들기를 새로 작성하였습니다.

링크 및 참조용으로 현재 게시물은 남겨두겠지만,
가급적이면 새로운 스프링 게시판 만들기를 참조해주시기 바랍니다.

스프링 게시판 만들기 #5까지 진행하여, 게시판의 기본적인 기능이 모두 구현되었지만, 말그대로 기본적인 기능만 구현되었기 때문에 실제로 사용하기엔 어려움 또는 불편함이 있습니다.

insert into myBoard(bno, title, content, writer)
    select myBoard_seq.nextval, title, content, writer from myBoard;

이 쿼리는 테이블에 저장된 데이터 만큼의 데이터를 다시 저장합니다. 현재 데이터가 2개가 있다면, 이 쿼리를 실행한 후엔 4개, 다시 한번 더 실행하면 8개가 됩니다.

이 쿼리를 몇번 실행하면 수백개의 더미용 데이터를 만들 수 있습니다.

약 3000개 가량의 더미 데이터가 만들어졌습니다.

실제로 사용되는 게시판은 데이터가 더 많은 경우가 훨씬 많을텐데, 데이터가 많아질 경우 게시물을 일정한 갯수별로 나누어서 사용하며, 흔히 페이징 또는 페이지네이션이라고 합니다.

먼저, 가장 최근에 작성된 게시물부터 10개까지 출력되는 쿼리를 작성합니다.

select bno, title, content, writer, regDate, viewCnt
    from (
        select bno, title, content, writer, regDate, viewCnt,
            row_number() over(order by bno desc) as rNum
        from myBoard
        ) mb
    where rNum between 1 and 10
        order by bno desc;

이 쿼리는 테이블 myBoard의 데이터를 최근 순서로 출력하되, 가장 최근에 출력된 게시물을 기순으로 내림차순 숫자 row_number를 부여하고 이를 약칭으로 rNum라 설정합니다. 그 후 전체 목록에서 rNum의 값이 1부터 10인 데이터를 출력합니다.

이 쿼리는 오라클용이며, 다른 DBMS(데이터 베이스 관리 시스템)를 사용한다면 쿼리문이 달라질 수 있습니다.

rNum의 제한값을 1~10까지하면 1번째부터 10번째 게시물을, 11~20까지하면 그 다음인 11번째부터 20번째까지 출력합니다.

이 rNum의 제한값을 임의로 바꾸는것으로 페이징을 구현할 수 있습니다.

이 쿼리를 매퍼에 추가합니다.

<select id="listPage" resultType="com.kuzuro.domain.BoardVO"
parameterType="com.kuzuro.domain.Criteria">
    select bno, title, content, writer, regDate, viewCnt
       from (
           select bno, title, content, writer, regDate, viewCnt,
               row_number() over(order by bno desc) as rNum
           from myBoard
     ) mb
    where rNum between #{rowStart} and #{rowEnd}
        order by bno desc
</select>

rNum을 제한하는 값중 시작값은 #{rowStart}, 끝값은 #{rowEnd}로 했으며, 파라미터 타입(parameterType)을 Criteria로 했습니다.

Criteria는 시작값과 끝값을 다루는 클래스이며, 바로 다음에 추가합니다.

domain 패키지에 Criteria 클래스를 추가합니다.

package com.kuzuro.domain;

public class Criteria
{
 private int page;
 private int perPageNum;
 private int rowStart;
 private int rowEnd;
 
 public Criteria()
 {
  this.page = 1;
  this.perPageNum = 10;
 }

 public void setPage(int page)
 {
  if (page <= 0)
  {
   this.page = 1;
   return;
  }
  this.page = page;
 }

 public void setPerPageNum(int perPageNum)
 {
  if (perPageNum <= 0 || perPageNum > 100)
  {
   this.perPageNum = 10;
   return;
  }
  this.perPageNum = perPageNum;
 }

 public int getPage()
 {
  return page;
 }

 public int getPageStart()
 {
  return (this.page - 1) * perPageNum;
 }

 public int getPerPageNum()
 {
  return this.perPageNum;
 }

 @Override
 public String toString() {
  return "Criteria [page=" + page + ", perPageNum=" + perPageNum + ""
    + ", rowStart=" +  getRowStart() + ", rowEnd=" + getRowEnd()
    + "]";
 }

 public int getRowStart() {
  rowStart = ((page - 1) * perPageNum) + 1;
  return rowStart;
 }

 public int getRowEnd() {
  rowEnd = rowStart + perPageNum - 1;
  return rowEnd;
 }
}

Criteria는 rNum의 제한값과 현재 페이지, 페이지에 출력되는 게시물 숫자를 제어합니다.

DAO와 Service에 페이징용 코드를 추가합니다.

다음으로 컨트롤러에 코드를 추가합니다. 기존에 있던 list와 거의 같습니다.

컨트롤러에 추가된 메서드에 맞는 jsp파일을 추가합니다.

시작 페이지에서 바로 리스트 페이지로 넘어갈 수 있도록 링크를 추가합니다.

프로젝트를 실행하고, 리스트 페이지 링크를 클릭합니다.

쿼리대로 10개만 출력됩니다.

page를 임의로 입력하면 출력되는 목록이 바뀌는것을 알 수 있습니다.

perPageNum은 한 화면에 출력되는 게시물의 숫자입니다. 이것 또한 임의로 입력하면 출력되는 결과가 달라집니다.

그런데 매번 페이지를 넘길 때마다, 주소창에 페이지 번호를 입력해야하는건 몹시 불편합니다. 그러므로 페이지 번호를 만들고 출력해주는 기능이 필요합니다.

domain 패키지에 PageMaker 클래스를 추가합니다.

package com.kuzuro.domain;

public class PageMaker {

 private int totalCount;
 private int startPage;
 private int endPage;
 private boolean prev;
 private boolean next;

 private int displayPageNum = 10;

 private Criteria cri;
 
 public void setCri(Criteria cri) {
  this.cri = cri;
 }

 public void setTotalCount(int totalCount) {
  this.totalCount = totalCount;
  calcData();
 }

 public int getTotalCount() {
  return totalCount;
 }

 public int getStartPage() {
  return startPage;
 }

 public int getEndPage() {
  return endPage;
 }

 public boolean isPrev() {
  return prev;
 }

 public boolean isNext() {
  return next;
 }

 public int getDisplayPageNum() {
  return displayPageNum;
 }

 public Criteria getCri() {
  return cri;
 }
 
 private void calcData() {
  endPage = (int) (Math.ceil(cri.getPage() / (double)displayPageNum) * displayPageNum);
  startPage = (endPage - displayPageNum) + 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 ? false : true;
 }
 
}

totalCount : 게시물의 총 갯수
startPage : 현제 페이지의 시작 번호 (1, 11, 21 등등)
endPage : 현제 페이지의 끝 번호 (10, 20, 30 등등)
prev : 이전 페이지로 이동하는 링크의 존재 여부
next : 다음 페이지로 이동하는 링크의 존재 여부

게시물의 총 갯수를 구하는 쿼리를 매퍼에 작성합니다.

이때 기준이 되는 컬럼을 글 번호(bno)가 아니라 다른 컬럼으로해도, *(모든 선택자)로 해도 결과는 무관하지만 속도의 차이가 있습니다. 지금은 갯수가 얼마 없으니 속도면에서 차이가 없지만 이후가 되면 차이가 생깁니다.

또, 지금 구하려는건 '게시물의 갯수'인데 각 게시물을 구분하는 컬럼은 글 번호(bno)이므로 용도와 역할에서도 글 번호(bno)로 하는것이 맞습니다.

DAO와 Service에 게시물 총 갯수를 구하는 코드를 추가합니다.

컨트롤러의 리스트 페이지 메서드에 코드를 추가합니다.

// 글 목록 + 페이징
@RequestMapping(value = "/listPage", method = RequestMethod.GET)
public void listPage(@ModelAttribute("cri") Criteria cri, Model model) throws Exception {
 logger.info("get list page");
 
 List<BoardVO> list = service.listPage(cri);
 model.addAttribute("list", list);
 
 PageMaker pageMaker = new PageMaker();
 pageMaker.setCri(cri);
 pageMaker.setTotalCount(service.listCount());
 model.addAttribute("pageMaker", pageMaker);
 
}

게시물을 출력하는 테이블 밑에 페이지용 HTML코드를 추가합니다.

<div>
 <ul>
  <c:if test="${pageMaker.prev}">
   <li><a href="listPage?page=${pageMaker.startPage - 1}">이전</a></li>
  </c:if> 
  
  <c:forEach begin="${pageMaker.startPage}" end="${pageMaker.endPage}" var="idx">
   <li><a href="listPage?page=${idx}">${idx}</a></li>
  </c:forEach>
    
  <c:if test="${pageMaker.next && pageMaker.endPage > 0}">
   <li><a href="listPage?page=${pageMaker.endPage + 1}">다음</a></li>
  </c:if> 
 </ul>
</div>

이전 페이지와 다음 페이지는 조건문으로 출력을 제어하고, 페이지 숫자는 반복문으로 출력합니다.

여기까지 작업했으면 페이지는 정상적으로 작동하지만, 오직 page 파라미터만 주소창에 표시되며 perPageNum 파라미터는 표시 자체가 되지 않습니다.

이후의 작업을 위해 perPageNum 파라미터와 추가될 파라미터를 위해 코드를 추가합니다.

PageMaker에 makeQuery 메서드를 추가합니다.

public String makeQuery(int page)
{
 UriComponents uriComponents =
   UriComponentsBuilder.newInstance()
   .queryParam("page", page)
   .queryParam("perPageNum", cri.getPerPageNum())
   .build();
   
 return uriComponents.toUriString();
}

UriComponents는 URI를 생성해주는 클래스입니다.

page와 perPageNum의 파라미터와 값을 설정하여 URI를 생성합니다.

makeQuery가 적용되도록, 코드를 수정합니다.

<div>
 <ul>
  <c:if test="${pageMaker.prev}">
   <li><a href="listPage${pageMaker.makeQuery(pageMaker.startPage - 1)}">이전</a></li>
  </c:if> 
  
  <c:forEach begin="${pageMaker.startPage}" end="${pageMaker.endPage}" var="idx">
   <li><a href="listPage${pageMaker.makeQuery(idx)}">${idx}</a></li>
  </c:forEach>
    
  <c:if test="${pageMaker.next && pageMaker.endPage > 0}">
   <li><a href="listPage${pageMaker.makeQuery(pageMaker.endPage + 1)}">다음</a></li>
  </c:if> 
 </ul>
</div>

makeQuery에서 모든 URI를 생성하므로, 주소의 방식이 조금 달라졌습니다.

페이지 기능 이외에, 주소창에 page와 perPageNum이 정상적으로 표시되며 다른 페이지 역시 잘 표시되고 있습니다.

게시물 수정
  1. 강좌 및 예제 감사합니다!
    궁금한 게 있는데요, listPage.jsp 파일은 list.jsp 파일 복사해서 넣는 것이 맞지요?
    500번 에러가 떠서, service의 listPage메소드가 Criteria cri를 받게 되어있는데, 컨트롤러에서 해당 메소드에는 파라미터가 들어가있지 않네요. 파라미터 없이, 파라미터로 cri를 넣어서 두 개 다 작성해봤는데 에러에서 못 벗어나네요.. 어떻게 해결해야 할까요? ㅠㅠ

    답글삭제
    답글
    1. 안녕하세요?

      먼저 컨트롤러의 listPage메서드의 내용에서 List<BoardVO> list = service.listPage(); 는 잘못 표기되었으며 List<BoardVO> list = service.listPage(cri);가 맞습니다. 말씀하신대로 매개변수(파라미터) cri가 들어가있어야 정상적으로 동작합니다.

      그런데 매개변수를 넣고도 에러가 발생한다면.. 어느 에러인지 알려주실 수 있으신지요?

      삭제
    2. 에러 잡았습니다~ listPage 쿼리에서 세미콜론 빼는 걸 깜빡했더니 인식할 수 없는 문자가 있다며 에러가 떴어요. 수정하니 잘 돌아갑니다 ^^ 감사합니다~~

      삭제

  2. select bno, title, content, writer, regDate, viewCnt
    from (
    select bno, title, content, writer, regDate, viewCnt,
    row_number() over(order by bno desc) as rNum
    from myBoard
    ) mb
    where rNum between #{rowStart} and #{rowEnd}
    order by bno desc

    이 쿼리에선 rowstart , rowend만 쓰시고



    public int getPageStart()
    {
    return (this.page - 1) * perPageNum;
    }

    public int getPerPageNum()
    {
    return this.perPageNum;
    }

    @Override
    public String toString() {
    return "Criteria [page=" + page + ", perPageNum=" + perPageNum + ""
    + ", rowStart=" + getRowStart() + ", rowEnd=" + getRowEnd()
    + "]";
    }

    public int getRowStart() {
    rowStart = ((page - 1) * perPageNum) + 1;
    return rowStart;
    }

    public int getRowEnd() {
    rowEnd = rowStart + perPageNum - 1;
    return rowEnd;
    }
    }

    criteria클래스에서는 rowstart rowend 그리고
    getPageStart메소드가 쓰였는데
    getPageStart 메소드는 어디서 사용이 되는건가요?

    답글삭제
    답글
    1. 안녕하세요?

      죄송합니다, 이 부분도 다른 게시물에있는 것 처럼 실수입니다.ㅠㅠ

      게시물에서는 페이징 구현을 게시물 갯수 조절부터 각 페이지별 목록 처리 등.. 처음부터 차근차근 한게 아니라 뜬금없이 완성된게 툭 튀어나왔는데, 원래는 처음~중간 과정이 있었으나 이걸 통채로 잘라버렸습니다.

      그 과정에서 살아남은 메서드입니다;

      제가 설명하는것보다 다른곳에 참고할 곳이 많고, 분량을 줄이기 위해서 그냥 생략해도 되겠다 싶어서 잘라낸건데 흔적을 남겨서 들켰네요-_-;

      삭제
  3. 작성자가 댓글을 삭제했습니다.

    답글삭제
  4. 문득궁금한게 쿼리에서 bno의 역순으로 검색시 중간에 글이 삭제되면 예를들어 30 29 26 25 이런식으로 나오는데 이런문제 해결못하나요?? Rnum으로 다시 select하기엔 글번호가 역순이 아니게 되고..

    답글삭제
  5. 혹시 페이지 1번에서 10번 버튼을 누르면 다음 페이지가 뜨는 건 어떻게 하나요?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      말씀하신 '다음페이지'라는게.. 1번 클릭하면 2번 페이지가 나오는건지, 아니면 1~10번 클릭시 11~20이 나오는건지는 잘 모르겠는데

      1~10번 클릭시 해당 번호에 맞지 않는 페이지가 출력되는 경우라면.. url을 만드는 방식을 확인해야할 것 같습니다.

      삭제
  6. 익명6/10/2021

    감사합니다.

    답글삭제