2015년 6월 22일 월요일

페이징(Paging)에 대한 이해 - (1) 페이지 번호를 생성하자.


흔히 웹 개발을 처음 하는 개발자가 처음 겪게 되는 난관은 바로 게시판의 페이징(Paging)이 아닐까 합니다. 특히 DBMS 와 연동해서 개발하는 경우가 많은데, 각 DBMS 마다 페이징 방법도 많고 성능 등 신경써야 할 부분이 많이 때문에 어떤 자료를 보고 따라해야 문제가 없을지 판단하기 막막할 때가 있습니다.

그래서 페이징에 대한 원리를 파악해서 가장 기본적인 페이징 구현을 할 수 있는 방법을 알려드리고자 합니다.

이 글은 초보자를 위한 글이므로, 페이징 정도는 우습다는 분은 패스해주시길...^^;;;

------------------------------------------------------------------------------------------------------




먼저, 페이징에 대한 이해를 하기 위해서는 페이징을 하기 위해 각 DBMS 가 어떤 정보를 제공해줘야 하는지 알아야 합니다. DBMS 마다 페이징을 위한 방법이 다른데, 제공하는 정보가 다르기 때문입니다.

모든 DBMS 는 내가 원하는 정보의 총 갯수를 가져오는 기능을 제공합니다. 바로 select count(*) 을 이용하는 것입니다. 이것을 이용해서 게시판 아래 출력되는 페이지 번호를 생성할 수 있습니다.
그렇다면, 해당 페이지의 게시물만 가져오는 방법은 무엇일까요?
오라클은 전통적으로 ROWNUM 을 제공합니다. 출력되는 정보에 순차적인 번호를 부여함으로써 원하는 게시물만 가져오는 방식입니다.
MS SQL Server 는 예전에는 TOP 을 이용했습니다. 출력될 정보 중 상위 몇 개를 가져온 뒤, 그 중 원하는 시작점까지 프로그램 상에서 이동을 해서 그 뒤의 것들을 가져오는 방식입니다. 하지만, 최근에는 오라클과 마찬가지로 ROW_NUMBER() 을 제공함으로써 좀 더 편한 페이징이 가능해졌습니다.
MySQL 이나 PostgreSQL 은 전통적으로 LIMIT 를 이용한 페이징을 제공했습니다. 페이징 할 때 크게 고민할 것 없이 사용이 가능하므로 매우 개발자 친화적이긴 하지만, 두 DBMS 의 LIMIT 는 의미가 약간 다르므로 주의해서 사용해야 합니다.
참고로, 제가 주로 사용하는 NoSQL 제품인 Couchbase 에서는 limit 와 skip 을 통해서 페이징을 가능합니다. 사실 내부적인 동작 원리는 비슷하지만, 어떻게 페이징된 정보를 가져오냐의 차이인 것 같습니다.



이번에는 게시판 하단에 표시되는 페이지 번호에 대해서 알아보겠습니다.

예를 들어 25 개의 게시물이 등록된 게시판(board)이 있다고 하겠습니다. 그럼 다음과 같은 Query 을 통해 총 게시물의 수(totalCount)를 가져올 수 있을 겁니다.

select count(*) as totalCount from board

그럼 프로그램에선 몇 가지 정보를 이용해서 하단의 페이지 번호를 생성할 수 있습니다.

1. 한 페이지에 출력될 게시물 수 (countList)
2. 한 화면에 출력될 페이지 수 (countPage)
3. 현재 페이지 번호 (이하 page)

처음 해야할 것은 총 몇 페이지가 존재하는지 알아내는 것입니다(총 페이지, 이하 totalPage). 이 때 필요한 것은 countList 인데, 이해를 돕기 위해 countList = 10 혹은 countList = 5 라고 가정하고 계산해보겠습니다.

25 / countList
25 / 10 = 2 나머지 5
25 / 5 = 5 나머지 0

우리가 기대하는 수는? 바로 3 와 5 입니다. Java 와 같은 언어에서는 int 형으로 보통 계산을 하기 때문에 25 /10 을 해버리면 2 만 나오게 됩니다. 그렇다고 무조건 1 을 더하게 되면 아래의 countList = 5 와 같은 경우 때문에 정확한 총 페이지 수(totalPage)을 계산할 때 무조건 1 을 더해선 안됩니다(의외로 이렇게 계산해서 마지막 페이지에 게시물 안나오는 곳이 많더군요). 위에서 보듯이 가장 정확하면서 단순한 방법은 나머지가 있을 경우에만 1 을 더해줘야 합니다. Java 로 된 최종 코드는 아래와 같이 되겠죠.

int totalCount = 25; // 물론 실제론 여긴 DBMS 에서 조회해서 들어가야 합니다.
int countList = 10;

int totalPage = totalCount / countList;

if (totalCount % countList > 0) {

    totalPage++;

}

혹은 아래와 같이 처리할 수도 있겠죠.

int totalCount = 25;
int countList = 10;

int totalPage = totalCount / countList;

if (totalCount > countList * totalPage) {

    totalPage++;

}

이렇게 하면 총 페이지 수를 알 수 있습니다. 이 값을 통해서 한 화면에 출력될 페이지 수가 10 개라고 해도, 3 페이지까지만 존재할 경우 1 에서 3 페이지까지만 하단에 출력해줄 수 있습니다(이런 처리 안해서 없는 페이지를 출력하는 곳도 많더군요).
또한, 잘못된 현재 페이지에 대한 보정도 가능합니다.
현재 페이지 번호가 총 페이지 번호보다 크다면 어떻게 해야 할까요? 현재 페이지를 강제로 총 페이지 번호로 치환하는 것도 방법이 될 것입니다.

int page = 2;

int totalCount = 25;
int countList = 10;

int totalPage = totalCount / countList;

if (totalCount % countList > 0) {

    totalPage++;

}

if (totalPage < page) {

    page = totalPage;

}



이번에는, 하단에 표시될 페이지 번호들을 어떻게 알아낼 수 있는지 알아보겠습니다. 이번 예제에는 totalCount = 255, countList = 10, countPage = 10 으로 하겠습니다. 그리고 현재 페이지인 page 는 5 와 22 의 두 가지 경우를 생각해보겠습니다. 또한, 페이지 표시 방식은 대한민국에서 가장 많이 쓰는 방식인 10 개의 페이지를 보이는 방식으로 하겠습니다. 이 방식은 현재 페이지 기준으로 앞 뒤 몇 개의 페이지를 보여주는 외국의 방식보다 대한민국에서 더 많이 쓰는 방식입니다.

먼저 page = 5 일 경우입니다. 이 경우 우리가 예상하는 결과는 하단에 1 ~ 10 까지의 페이지 번호가 표시되는 것입니다(countPage = 10 이니까요).

이런 페이지 번호 계산을 하는 방법은 몇 가지가 있습니다만, 가장 간단하고 쉬운 방법은 바로 시작 페이지 번호를 계산해내는 것입니다. page = 5 일 경우 시작 페이지(startPage)는 1, 마지막 페이지(endPage)는 시작 페이지에서 10 페이지(countPage 가 10 이니까요)까지라는 건 금방 이해하시리라 생각합니다. 그럼 page = 5 에서 어떻게 1 페이지를 찾아낼 수 있을까요? 아주 쉽습니다. 그냥 countPage 로 나눠버리면 됩니다(그리고 1 을 더해줘야 해요!). 그럼 마지막 페이지는요? countPage 을 더하면 됩니다. 단, 더한 뒤 1 을 빼주는 작업은 잊으시면 안됩니다. 왜 그런지는 생각해보시면 금방 깨닫게 되실꺼구요.

int page = 5;
int countPage = 10;

int startPage = page / 10 + 1;  // 왜 1 을 더할까요?
int endPage = startPage + countPage - 1;  // 왜 1 을 뺄까요?

실제 보이는 페이지는 1 ~ 10 까지이지만, 실제 페이지 번호는 0 ~9 까지로 처리하는 경우도 있습니다. 그게 Java 와 같은 언어에선 처리가 더 간단하기 때문인데요...저 역시 시작 페이지는 0 으로 해서 처리한 클래스를 만들어서 씁니다만, 이해를 돕기 위해 여기서는 1 페이지는 1 의 page 번호를 가지도록 하겠습니다.

어쨌든, 저렇게 계산을 하면 startPage = 1, endPage = 10 이라는 결과가 나옵니다.

그럼 화면에 출력할 때에는요?

int page = 5;
int countPage = 10;

int startPage = page / 10 + 1;
int endPage = startPage + countPage - 1;

for (int iCount = startPage; iCount <= endPage; iCount++) {
 
    System.out.print(" " + iCount + " ");
 
}

이런 식으로 출력하면 페이지 번호가 연달아서 출력이 되겠죠.

그렇다면 page 가 22 인 경우에는요?

먼저 단순히 산수 계산을 해보면 255 개의 게시물이 있을 경우 총 26 페이지가 존재할 것이고, 22 페이지가 있는 곳에는 21 에서 30 페이지 영역일 것입니다. 하지만, 26 페이지까지이기 때문에 단순히 21 페이지에서 countPage 을 더하면 안된다는 것을 대번에 눈치 채셨을 겁니다. 그래서 이 경우에도 마지막 페이지는 총 페이지 수로 대체를 해줘야 합니다.

int page = 22;
int countList = 10;
int countPage = 10;

int totalCount = 255;

int totalPage = totalCount / countList;

if (totalCount % countList > 0) {

    totalPage++;

}

if (totalPage < page) {

    page = totalPage;

}

int startPage = page / 10 + 1;
int endPage = startPage + countPage - 1;

//  여기서 마지막 페이지를 보정해줍니다.
if (endPage > totalPage) {
 
    endPage = totalPage;
 
}

// [paging]
// 이 부분은 아래에서 추가로 설명합니다.
for (int iCount = startPage; iCount <= endPage; iCount++) {
 
    System.out.print(" " + iCount + " ");
 
}

대충 끝이 보이네요. 위에서 [paging] 이라고 표시된 부분에서 페이지 번호를 출력하는데, 출력을 할 때 css 코드를 넣거나 <a> 태그를 이용해서 연결을 하면 페이지 이동이 가능합니다. 이 때 현재 페이지는 굵은 글씨체로 표시하고 <a> 태그를 빼기 위해 아래와 같이 처리도 가능하겠죠.

// [paging]
// 이렇게 개선됩니다.
for (int iCount = startPage; iCount <= endPage; iCount++) {
 
    if (iCount == page) {
     
        System.out.print(" <b>" + iCount + "</b>");
     
    } else {
     

        System.out.print(" " + iCount + " ");
     
    }
 
}

보통 첫페이지 이동이나 이전 페이지, 다음페이지, 끝페이지 이동 버튼도 추가로 달아줍니다. 그래야 해당 페이지 리스트에 없는 곳으로도 이동이 될테니까요.
첫 페이지는 현재 페이지가 1 페이지가 아닐 때 표시되게 하는 경우도 있고, 시작 페이지가 1페이지가 아닐 때 표시하는 경우도 있습니다. 취향 문제죠. 어쨌든 이동하는 페이지는 항상 page = 1 이 되겠죠. 이전 페이지도 마찬가지입니다. 1 페이지가 아닐 경우 현재 페이지보다 1 페이지 앞으로 이동하도록 page - 1 값을 가지고 이동하게 하면 되죠. 다음페이지는 totalPage 와 비교해서 마찬가지로 표시해주면 됩니다.

int page = 22;
int countList = 10;
int countPage = 10;

int totalCount = 255;

int totalPage = totalCount / countList;

if (totalCount % countList > 0) {

    totalPage++;

}

if (totalPage < page) {

    page = totalPage;

}

int startPage = page / 10 + 1;
int endPage = startPage + countPage - 1;


if (endPage > totalPage) {
 
    endPage = totalPage;
 
}

if (startPage > 1) {
 
    System.out.print("<a href=\"?page=1\">처음</a>");
 
}

if (page > 1) {
 
    System.out.println("<a href=\"?page=" + (page - 1)  + "\">이전</a>");

}

for (int iCount = startPage; iCount <= endPage; iCount++) {
 
    if (iCount == page) {
     
        System.out.print(" <b>" + iCount + "</b>");
     
    } else {
     
        System.out.print(" " + iCount + " ");
     
    }
 
}

if (page < totalPage) {
 
    System.out.println("<a href=\"?page=" + (page + 1)  + "\">다음</a>");

}

if (endPage < totalPage) {
 
    System.out.print("<a href=\"?page=" + totalPage + "\">끝</a>");
 
}


이렇게 처리하면 페이징 표시를 할 수 있게 됩니다. 참 쉽죠~~~잉? (고 밥 로스 아저씨 따라하기)



다음에는 실제 각 DBMS 에서 페이지에 해당하는 글을 읽어오는 방법에 대해서 알아보겠습니다.
그리고, 이 글을 읽으시는 분 중에, DBMS 에서 조회할 때 총 페이지 번호와 게시물을 같이 읽어오면 되지, 왜 두 번에 나눠서 요청을 하느냐...그건 비효율적이다...라고 생각하시는 분은 곧 작성해서 올릴 다음 글을 꼭 읽어주세요. 왜냐하면 잘못된 습관이기 때문입니다.



이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;

위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.

댓글 4개:

  1. 참고 잘 하였습니다.
    다만 int startPage = page / 10 + 1 이부분이 조금 이해가 안됩니다.
    혹시 ((page / 10)*10) + 1; 아닌지...
    page 22/10+1 이면 3인데
    22의 start페이지는 21부터 아닌지요..

    답글삭제
    답글
    1. 죄송합니다. 글을 두 군데 올리다보니...

      https://okky.kr/article/282819

      오키의 페이지에는 정상적으로 수정을 했던 내용입니다. 아래 댓글에 해당 내용이 있습니다.

      삭제
  2. int startPage = nowPage / 10 + 1;

    int startPage = nowPage % 10 + 1;

    / -> % 아닌가요?

    답글삭제
    답글
    1. 예를 들어, 현재 페이지가 2페이지, 12페이지 두 가지가 있다고 하면 각각 1~10 페이지, 11~20 페이지가 하단에 페이징 목록으로 표시될 겁니다.

      이 때 2 / 10 을 하게 되면 0 입니다. 0~9 까지 모두 0 입니다. 10 ~ 19 까지는 모두 1 입니다. 이걸 이용하는 방식이기 때문에 / 가 맞습니다. % 의 경우는 나머지를 가져오는 것입니다.

      삭제