본문 바로가기

Backend/JSP & SERVLET

[JSP/SERVLET] 게시판 페이징(Paging) 처리 & 구현

 


게시판 페이징(Paging) 처리 & 구현


 

게시판 기능에 필수적인 페이징을 구현해보고자 한다. 해당 예시는 쇼핑몰 제작 실습에서 관리자가 상품을 관리하는 페이지다.

 

 

0. 과정


관리자 상품 관리 페이지를 작성하는 과정을 정리해보면..

 

관리자가 상품을 관리하는 과정을 담은 servlet에서 (AdminProductAction.java)

  1. Paging 클래스 객체 paging 생성
  2. displayPage, displayRow 보여질 페이지 개수 10개로 재설정 (초기 설정: 5)
  3. session 혹은 getParameter로 전달 받은 page 값이 있다면 해당 값으로 page 설정, 그렇지 않다면 1
    1. paging.setPage(page) 페이지 설정
  4. 총 상품이 몇개인지 세고 count 변수에 넣기 (AdminDao에 getAllCount 메소드)
  5. count 변수를 beginPage, endPage, startNum, endNum 을 계산하는 paging.setTotalCount(count); 에 넣어 계산
  6. startNum 과 endNum을 바탕으로 한 페이지에 표시할 상품들을 계산하는 쿼리를 이용하여 ArrayList로 가져오기 (AdminDao에 selectProduct 메소드)
  7. 표시할 상품이 담긴 list를 request 객체에 담기 (productList)
  8. 페이지 정보를 담은 paging 객체를 request 객체에 담기 (paging)

정도가 되겠다.

 

여기서 핵심은 페이징 알고리즘를 담당하는 Paging 클래스한 페이지에 표시할 상품들을 계산하는 쿼리를 이용하여 list 로 가져오는 작업이 되겠다.

 

 

1. 게시판 페이징 알고리즘


Paging.java

private int page = 1; // 현재 화면에 보여질 페이지 번호
private int totalCount; // 전체 게시물 개수
private int displayRow = 5; // 한 화면에 보여줄 게시물 수
private int displayPage = 5; // 한 화면에 보여줄 페이지 수

private int beginPage; // 한 화면에 보여줄 시작 페이지
private int endPage; // 한 화면에 보여줄 끝 페이지

private boolean prev; // 이전 페이지
private boolean next; // 다음 페이지

private int startNum; // 해당 페이지에서 보여줄 게시물의 시작 번호
private int endNum; // 해당 페이지에서 보여줄 게시물의 끝 번호

 

Paging 클래스에 들어가는 변수들이다.

 

▶ page

현재 페이지 번호다

page = 11
page = 4

 

▶ displayRow

한 화면에 보여줄 게시물 수다

displayRow = 10

 

▶ displayPage

한 화면에 보여줄 페이지 수다

displayPage = 5
displayPage = 10

 

▶ beginPage

한 화면에 보여줄 페이지 목록의 시작 번호다

beginPage = 1

 

▶ endPage

한 화면에 보여줄 페이지 목록의 마지막 번호다

endPage = 10

 

▶ prev

현재 보여지는 것 외의 이전 목록 페이지(들)을 나타낼 이전 버튼이다

prev = true

 

▶ next

현재 보여지는 것 외의 이후 목록 페이지(들)을 나타낼 다음 버튼이다

next = true

 

▶ startNum

현재 페이지에서 나타낼 게시물의 시작 번호를 나타낸다

startNum = 1

해당 예시의 번호는 임의로 부여된 값(sql sequence로 구현)이며 내림차순이다

 

▶ endNum

현재 페이지에서 나타낼 게시물의 끝 번호를 나타낸다

endNum = 10

해당 예시의 번호는 임의로 부여된 값(sql sequence로 구현)이며 내림차순이다

 

 

변수들의 getter setter 를 모두 작성하되, 게시물의 총 개수 저장하는 setter 에는 calPaging() 메소드를 추가한다.

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

 

calPaging() 은 화면에 표시될 게시물 수(displayRow)를 바탕으로 한 페이지에 어떤 게시물이 출력되어야 하는지 나타내는 startNum, endNum 와 화면에 표시될 페이지 수(displayPage)를 바탕으로 한 화면에 표시되어야 하는 페이지가 몇 번인지를 나타내는 beginPage, endPage 구할 것이다.

 

private void calPaging() {
    // 한 화면에 표시될 페이지 번호 (시작, 끝)
    endPage = ((int)(Math.ceil(page / (double)displayPage))) * displayPage;
    beginPage = endPage - (displayPage - 1);
	
    // 총 페이지 개수
    int totalPage = (int)Math.ceil(totalCount/(double)displayRow);
    
    // 이전, 다음 페이지를 나타내는 prev, next 의 등장여부
    if(totalPage < endPage) {
        endPage = totalPage;
        next = false;
    }else {
        next = true;
    }
    prev = (beginPage == 1) ? false : true;
	
    // 한 페이지에 표시될 게시물의 시작 번호와 끝 번호
    startNum = (page-1) * displayRow + 1;
    endNum = page * displayRow;
	
    // 결과 확인용
    System.out.println(beginPage + " " + endPage + " " + startNum + " " + endNum);
}

 

보기 편하게 한 줄로 작성한 코드다. 조금 더 세부적으로 들여다보자.

 

 

▶ endPage, beginPage 구하기

// 1. beginPage 와 endPage 계산 : page(현재페이지)와 displayPage 값을 이용해서 계산
double temp = page / (double)displayPage;
// 1/10 -> 0.1    5/10 -> 0.5    15/10 -> 1.5    25/10 -> 2.5

temp = Math.ceil(temp); // 소수점 올림으로 정수 계산
// 0.1 -> 1.0    0.5 -> 1.0    1.5 -> 2.0    2.5 -> 3.0

// 위 계산결과에 displayPage를 곱하면 endPage 가 계산됨
endPage = (int)(temp * displayPage);
// 1.0 -> 10    1.0 -> 10    2.0 -> 20    3.0 -> 30

beginPage = endPage - (displayPage - 1);
// 10 -> 1    10 -> 1    20 -> 11    30 -> 21

 

double temp = page / (double)displayPage;

 

임시로 double 형의 temp 변수를 만든다.

temp 에는 (현재 페이지 / 표시될 페이지 수) 를 넣는다.

 

현재 페이지 수가 15 이고, 표시될 페이지 수가 10이라면 1.5

현재 페이지 수가 10 이고, 표시될 페이지 수가 10이라면 1

현재 페이지 수가 3 이고, 표시될 페이지 수가 10이라면 0.3

 

 

temp = Math.ceil(temp); // 소수점 올림으로 정수 계산

 

temp 에 저장한 값을 소수점 올림으로 정수 계산한다

 

1.5 는 2

1은 1

0.3 은 1

 

값이 1이라면 현재 페이지 값을 보기 위해서는 표시되는 페이지의 첫번째 목록이고,

값이 2라면 현재 페이지 값을 보기 위해서는 표시되는 페이지의 두번째 목록을 확인해야 한다는 뜻이다.

(현재 페이지 수(page)가 15라면 한 화면에 10개씩 표시되는 페이지 목록의 경우 두번째인 11 12 13 14 15 16 17 18 19 20 으로 가야 한다는 뜻)

 

 

// 위 계산결과에 displayPage를 곱하면 endPage 가 계산됨
endPage = (int)(temp * displayPage);

 

temp 에 displayPage 값을 곱하면 endPage 가 나온다.

 

2는 20

1은 10

 

한 화면에 표시되는 페이지 수(displayPage)는 앞서 10개로 지정했다.

그렇다면 우리가 볼 페이지 목록은 1 2 3 4 5 6 7 8 9 10 과 같을 것이다.

temp 에 담긴 값은 현재 페이지 값(page)을 보여주기 위해 몇 번째 목록으로 가야하는가? 에 대한 값인데,

(현재 페이지가 25라면 페이지를 한 번에 10개씩 보여줄 경우 3번째 목록을 봐야 한다)

공교롭게(!) 해당 값에 displayPage 를 곱하면 보여지는 페이지 목록의 마지막 번호를 알 수 있는 것이다.

(displayPage(10개) 만큼 보여줄 때, page가 12인 상태라면 temp는 2이며, 11 12 13 14 15 16 17 18 19 20이다. temp * displayPage = 20, 이때 20이 endPage)

 

다만, 이때 구해진 endPage 는 totalPage 개수와 관련없이 '무조건 displayPage 만큼 보여진다면' 을 가정으로 한다.

이후에 totalPage를 구하면서 재조정할 것이다.

 

 

beginPage = endPage - (displayPage - 1);

 

앞서 구한 endPage 에서 표시할 페이지 수(display)의 -1 값을 빼면 현재 화면에 표시할 시작페이지 값(beginPage)가 나온다.

 

표시되는 마지막 페이지(endPage)가 20 이고, 한 번에 표시되는 페이지 개수가 10개라고 했을 때, 표시되는 첫번째 페이지(beginPage)는 20-(10-1) = 11 이 된다.

11(beginPage) 12 13 14 15 16 17 18 19 20(endPage)

 

 

endPage = ((int)(Math.ceil(page/(double)displayPage))) * displayPage;
beginPage = endPage - (displayPage - 1);

 

이를 한 번에 작성한 게 위의 코드다

 

 

▶ totalPage 구하기

int totalPage = (int)Math.ceil(totalCount/(double)displayRow);

 

멤버변수는 아니지만 중간 연산에 필요해서 총 페이지의 개수도 구했다.

구하는 방식은 위에서 다룬 방식과 매우 유사하다

 

위에서 temp 를 통해 구한 값이 ' 현재 페이지(page)로 가려면 몇 번째 목록으로 가야하는가? ' 였다면

totalPage는 ' 모든 게시물(totalCount)을 displayRow개씩 나타내면 총 몇 개의 페이지가 나오는가? ' 이다.

 

108 개의 게시물을 한 페이지에서 10개씩 보여준다고 했을 때, 처리 과정은 다음과 같다.

1. totalCount/(double)displayRow : 108 / 10 -> 10.8
2. Math.ceil : 11.0
3. (int) : 11

 

필요한 페이지의 개수는 총 11개다.

1 2 3 4 5 6 7 8 9 10 
◀ 11

 

위와 같이 표시할 수 있을 것이다.

 

 

▶ totalPage 를 이용해 next 표시 결정

if(totalPage < endPage) {
    endPage = totalPage;
    next = false;
}else {
    next = true;
}

 

현재 화면에서 총 페이지 개수(totalPage)가 끝 페이지 값(endPage)보다 작다면 endPage를 totalPage 값으로 바꾼다.

이때, 다음 목록으로 넘어가는 next 버튼을 비활성화(false) 한다.

 

 

▶ beginPage 를 이용해 prev 표시 결정

prev = (beginPage == 1) ? false : true;

 

이전 목록으로 넘어가는 prev 버튼은 간단하다.

시작 페이지(beginPage)가 1일 경우에 비활성화 되도록 삼항연산자를 통해 표현했다.

 

beginPage == 1 이라면 false

beginPage != 1 이라면 true 다.

 

 

▶ startNum, endNum 구하기

startNum = (page-1) * displayRow + 1;
endNum = page * displayRow;

 

한 페이지에서 보여지는 게시물의 시작 번호와 끝 번호다.

게시물이 오름차순 정렬일 경우 게시판 필드값이 아닌 순수한 행번호다.

 

현재 페이지(page)값이 3이고, 한 페이지에 보여지는 게시물 수(displayRow)가 10개라면 21번부터 30번 게시물이 보여지게 된다.

 

1페이지 : 1~10번 게시물

2페이지 : 11~20번 게시물

3페이지 : 21~30번 게시물

 

 

 

2. 현재 페이지에 따른 게시물 목록 불러오기


AdminProductListAction.java

public class AdminProductListAction implements Action {
    @Override
    public void execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        HttpSession session = request.getSession();
        AdminVO adminUser = (AdminVO)session.getAttribute("adminUser");
        if(adminUser == null) {
            response.sendRedirect("shop.do?command=admin");
        }else {

            if(request.getParameter("first") != null) {
                session.removeAttribute("page");
                session.removeAttribute("key");
            }

            Paging paging = new Paging();
            // 10개씩 출력
            paging.setDisplayPage(10);
            paging.setDisplayRow(10);

            // paging 객체 내의 page 변수값 설정
            int page = 1;
            if(request.getParameter("page") != null) {
                page = Integer.parseInt(request.getParameter("page"));
                session.setAttribute("page", page);
            }else if(session.getAttribute("page") != null) {
                page = (Integer)session.getAttribute("page");
            }
            paging.setPage(page); // 페이지 설정

            String key = "";
            if(request.getParameter("key") != null) {
                key = request.getParameter("key");
                session.setAttribute("key", key); // 검색을 했다면 유지되도록 저장
            }else if(session.getAttribute("key") != null) {
                key = (String)session.getAttribute("key");
            }

            AdminDao adao = AdminDao.getInstance();
            // product 테이블에 레코드 개수(상품 총 개수)
            int count = adao.getAllCount("product", "name", key);
            // 추출된 레코드 갯수는 paging 객체의 totalCount 변수에 저장
            paging.setTotalCount(count); // beginPage, endPage, startNum, endNum 계산
			
            // startNum, endNum에 맞춰 표시할 product 선택
            ArrayList<ProductVO> productList = adao.selectAdminProductList(paging, key);

            request.setAttribute("productList", productList);
            request.setAttribute("paging", paging);
            request.setAttribute("key", key);

            request.getRequestDispatcher("admin/product/productList.jsp").forward(request, response);
        }
    }

}

 

냅다 다 긁어왔다.

현재 ShoesShop 프로젝트는 주소과 전달값을 노출하지 않기 위해 command 값을 Servlet을 통해 ActionFactory 로 전달해 페이지를 이동하고 있다.

 

구조를 간단히 살펴보자.

 

 

▶ adminUser 로그인 확인

HttpSession session = request.getSession();
AdminVO adminUser = (AdminVO)session.getAttribute("adminUser");
if(adminUser == null) {
    response.sendRedirect("shop.do?command=admin");
}else {
    // admin 로그인 상태라면 실행
    ...
}

 

admin 로그인 유무를 확인하는 코드다.

로그인 당시 session 에 adminUser 값을 저장해놨다.

session 에 저장된 adminUser 를 불러와서 값이 없다면 로그인하는 창으로 가도록 했다.

 

 

▶ 검색값과 페이지값 저장 확인

if(request.getParameter("first") != null) {
    session.removeAttribute("page");
    session.removeAttribute("key");
}

 

프론트 form 에서 현재 page값과 검색한 key값을 session 에 저장해두었다.

이는 상품목록, 회원목록, Q&A 등 다양한 곳에서 공통적인 이름으로 쓰이기 때문에

해당 카테고리를 눌러 들어간 상태! (first) 라면 page와 key값을 초기화하기로 했다.

(삭제하지 않았을 때 - 회원목록에서 "김" 을 입력 후 5페이지를 누른 상태에서 상품목록으로 들어가면, 검색창에 "김"이 남아있는 상태에서 5페이지 값을 가지고 오려할 것이다.)

 

 

▶ 현재 페이지(page) 받아오기

Paging paging = new Paging();
// 10개씩 출력
paging.setDisplayPage(10);
paging.setDisplayRow(10);

// paging 객체 내의 page 변수값 설정
int page = 1;
if(request.getParameter("page") != null) {
    page = Integer.parseInt(request.getParameter("page"));
    session.setAttribute("page", page);
}else if(session.getAttribute("page") != null) {
    // 이미 저장된 값이 있다면
    page = (Integer)session.getAttribute("page");
}
paging.setPage(page); // 페이지 설정

 

현재 페이지(page)값은 항상 1로 지정해둔다.

다만, 새로 page값을 눌러 전달됐거나 session 에 저장된 값이 있다면 (ex 해당 페이지에서 게시물 상세보기를 눌렀다가 목록으로 나옴) 해당 값을 받아 page로 설정한다.

 

setPage 로 page 설정해주는 것을 잊지 말자.

(이거 안 해서 삽질을 좀 했었다;)

 

 

▶ 현재 검색한 값(key) 받아오기

String key = "";
if(request.getParameter("key") != null) {
    key = request.getParameter("key");
    session.setAttribute("key", key); // 검색을 했다면 유지되도록 저장
}else if(session.getAttribute("key") != null) {
    key = (String)session.getAttribute("key");
}

 

검색할 때 작성한 값(key) 도 page 설정과 마찬가지다.

 

 

▶ 현재 페이지에 보여줄 상품 리스트 받아오기

AdminDao adao = AdminDao.getInstance();

// product 테이블에 레코드 개수
int count = adao.getAllCount("product", "name", key);
// 추출된 레코드 갯수는 paging 객체의 totalCount 변수에 저장
paging.setTotalCount(count); // beginPage, endPage, startNum, endNum 계산

// startNum, endNum에 맞춰 표시할 product 선택
ArrayList<ProductVO> productList = adao.selectAdminProductList(paging, key);

 

AdminDao 에서 테이블의 모든 레코드 '개수'를 가져오는 getAllCount 메소드

현재 페이지에서 가져와야할 상품 정보들을 가져올 selectAdminProductList 메소드를 만들었다.

해당 메소드에 사용되는 쿼리문은 챕터에서 자세히 다뤄보려 한다.

 

 

▶ request 에 저장

request.setAttribute("productList", productList);
request.setAttribute("paging", paging);
request.setAttribute("key", key);

 

프론트에 가져갈 정보들을 request.setAttribute 로 담는다.

 

 

▶ request 값 들고 forward

request.getRequestDispatcher("admin/product/productList.jsp").forward(request, response);

 

이후 productList.jsp 로 전달한다.

 

 

 

3. 페이지에 따른 게시물 받아오기 (쿼리문)


AdminDao.java

public int getAllCount(String tableName, String fieldName, String key) {
    int count = 0;
    con = Db.getConnection();
    String sql = "SELECT COUNT(*) AS cnt FROM " + tableName + " WHERE " + fieldName + " like '%'||?||'%'";
    try {
        pstmt = con.prepareStatement(sql);
        pstmt.setString(1, key);
        rs = pstmt.executeQuery();
        if (rs.next())
            count = rs.getInt("cnt");
    } catch (SQLException e) { e.printStackTrace();
    } finally { Db.close(con, pstmt, rs); }

    return count;
}

 

검색한 값을 바탕으로 테이블 레코드(게시물)의 개수를 구하는 메소드다.

 

SELECT COUNT(*) AS cnt
FROM product
WHERE name like %||key||%;

 

sql 문으로 나타내면 위와 같다

product 테이블에서 name 필드에 ' key ' 가 포함된 값이 총 몇개인지! 를 cnt 라는 이름으로 select 했다.

 

 

public ArrayList<ProductVO> selectAdminProductList(Paging paging, String key) {
    ArrayList<ProductVO> list = new ArrayList<ProductVO>();
    con = Db.getConnection();
    String sql = "SELECT * FROM (" + "SELECT * FROM (" + "SELECT rownum AS rn, p.* FROM "
            + "((SELECT * FROM product WHERE name LIKE '%'||?||'%' ORDER BY pseq DESC) p)" + ") WHERE rn >= ?"
            + ") WHERE rn <= ?";
    try {
        pstmt = con.prepareStatement(sql);
        pstmt.setString(1, key);
        pstmt.setInt(2, paging.getStartNum());
        pstmt.setInt(3, paging.getEndNum());
        rs = pstmt.executeQuery();
        while (rs.next()) {
            // 해당하는 product 필드 값 전부 받아오기
            ...
            list.add(pvo);
        }
    } catch (SQLException e) { e.printStackTrace();
    } finally { Db.close(con, pstmt, rs); }

    return list;
}

 

이게 진짜 헬이다.

현재 페이지에서 가져올 상품 정보들을 list 에 담아오는 메소드다.

 

SELECT * FROM
(SELECT * FROM
(SELECT rownum AS rn, p.* FROM(
(SELECT * FROM product WHERE name LIKE '%'||key||'%' ORDER BY pseq DESC) p)
)WHERE rn >= startNum
)WHERE rn <= endNum

 

1. product 테이블에서 name 필드에 key값이 포함된 레코드를 pseq 기준 내림차순으로 선택

2. 한 값을 가지고! 행번호(rownum) 이 해당 페이지의 시작 게시물 번호(startNum) 인 값만 선택

3. 한 값을 가지고! 행번호(rownum) 이 해당 페이지의 마지막 게시물 번호(endNum) 인 값만 선택

 

하는.. 외워서 써도 될 쿼리문이다.

봐도봐도 이해하기 힘든데.. 이렇게 괄호괄호괄호를 쳐줘야 속도가 빠르다고 한다.

where rn >= startNum and rn <= endNum 보다 나누는 게 더 빠르다는데..

쿼리가 from 검사 -> 첫번째 where 조건 검사 -> 두번째 where 조건 검사 로 이뤄지는 듯 하나.. 정확한 이유를 모르겠다. 근데 느낌상 빠를 것 같다(?)

 

 

4. 결과


<form method="post" name="frm">
    <div class="tb">
        <div class="row">
            <div class="col" style="display: flex; aling-items: center;">
                상품명 &nbsp; <input type="text" name="key" value="${key}"/>&nbsp;&nbsp;&nbsp;
                <input class="btn" type="button" name="btn_search" value="검색" onclick="go_search('adminProductList')"/>
                &nbsp;&nbsp;&nbsp;
                <input type="button" style="margin-left: auto" value="상품등록" onclick="go_write()"/>
            </div>
        </div>
        <div class="row">
            <div class="coltitle">번호</div>
            <div class="coltitle">상품명</div>
            <div class="coltitle">원가</div>
            <div class="coltitle">판매가</div>
            <div class="coltitle">등록일</div>
            <div class="coltitle">사용유무</div>
        </div>
        <c:forEach items="${productList}" var="productVO">
            <div class="row">
                <div class="col">${productVO.pseq}</div>
                <div class="col" onclick="go_detail('${productVO.pseq}')">${productVO.name}</div>
                <div class="col"><fmt:formatNumber value="${productVO.price1}"/></div>
                <div class="col"><fmt:formatNumber value="${productVO.price2}"/></div>
                <div class="col"><fmt:formatDate value="${productVO.indate}"/></div>
                <div class="col">
                    <c:choose>
                        <c:when test='${productVO.useyn=="N"}'>미사용</c:when>
                        <c:otherwise>사용</c:otherwise>
                    </c:choose>
                </div>
            </div>
        </c:forEach>
        <div class="row"> <!-- 페이지의 시작 -->
            <div class="coltitle" style="font-size: 120%; font-weight: bold;">
                <c:if test="${paging.prev}">
                    <a href="shop.do?command=adminProductList&page=${paging.beginPage-1}">◀</a>
                </c:if>
                &nbsp;
                <c:forEach begin="${paging.beginPage}" end="${paging.endPage}" var="index">
                    <!-- 현재 보고 있지 않은 페이지라면 -->
                    <c:if test="${index!=paging.page}">
                        <a href="shop.do?command=adminProductList&page=${index}">${index}&nbsp;</a>
                    </c:if>
                    <!-- 현재 보고 있는 페이지라면 -->
                    <c:if test="${index==paging.page}">
                        <span style="color: red">${index}&nbsp;</span>
                    </c:if>
                </c:forEach>
                &nbsp;
                <c:if test="${paging.next}">
                    <a href="shop.do?command=adminProductList&page=${paging.endPage+1}">▶</a>
                </c:if>
            </div>
        </div> <!-- 페이지의 끝 -->
    </div>
</form>

 

JSTL 과 EL 문법을 사용했다.

 

 

구현된 화면은 다음과 같다.

 

 

 

검색창에 원하는 키워드를 입력하면, 일치하는 상품들이 뜬다.

 

 

 

key 를 session 에 저장해둔 덕분에 다른 페이지로 넘어갔을 때도 key 가 포함된 상품이 뜬다.