티스토리 뷰
페이징에 대한 이해를 하기 위해서는 페이징을 하기 위해 각 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 입니다. 왜냐하면 25 개의 게시물이 있을 경우 한 페이지에 10 개씩 글을 보여주면 3 번째 페이지에서 5 개의 게시물만 표시될 것이고, 한 페이지에 5 개씩 보여줄 경우 5 번째 페이지에서 마지막 글까지 다 보여주기 때문입니다. Java 와 같은 언어에서는 int 형으로 보통 계산을 하기 때문에 25 / 10 을 해버리면 소수점 혹은 나머지가 사라지고 정수값인 2 만 남게 됩니다. 그러므로 여기에 1 을 더하면 3 총 페이지는 3 이 될 것이라 생각하고 1 을 더하는 경우가 있습니다. 하지만, 무조건 1 을 더하게 되면 countList = 5 와 같은 경우 때문에 부정확한 결과가 나오게 됩니다. 의외로 이렇게 계산해서 마지막 페이지에 게시물 안나오는 곳이 많습니다. 결국 가장 정확하면서 단순한 방법은 나머지가 있을 경우에만 1 을 더해줘야 합니다. Java 로 된 최종 코드는 아래와 같이 될 것입니다. 기본 원리는 countList 로 나눈 뒤, 다시 전체 게시물 수를 countList 로 나누어서 나머지 값을 얻은 뒤(Java 에서 % 는 나머지만을 구하는 연산자입니다) 이 값이 0 보다 크면 1 을 더해주는 것입니다.
int totalCount = 25; // 물론 실제론 여긴 DBMS 에서 조회해서 들어가야 합니다. int countList = 10; int totalPage = totalCount / countList; if (totalCount % countList > 0) { totalPage++; }
혹은 아래와 같이 처리할 수도 있겠죠. 25 / 5 을 한 결과가 5 일 때, 역으로 5 * 5 을 하면 원래의 총 게시물 수와 일치하게 되지만, 25 / 10 을 해서 정수 부분만 취할 경우 2 가 총 페이지 수가 되어 2 * 10 으로 다시 역산을 하면 총 게시물 수가 20 이 되어서 일치하지 않게 되므로, 이러한 원리를 이용해서 총 페이지 수를 한 개 늘려주는 것입니다.
int totalCount = 25; int countList = 10; int totalPage = totalCount / countList; if (totalCount > countList * totalPage) { totalPage++; }
이렇게 하면 총 페이지 수를 알 수 있습니다. 이 값을 통해서 한 화면에 출력될 페이지 수가 10 개라고 해도, 3 페이지까지만 존재할 경우 1 에서 3 페이지까지만 하단에 출력해줄 수 있습니다. 다시 한 번 강조하지만, 실제로 이런 처리 안해서 마지막 페이지가 빈 페이지로 나오는 사이트가 생각보다 많습니다.
또한, 잘못된 현재 페이지에 대한 보정도 가능합니다.
현재 페이지 번호가 총 페이지 번호보다 크다면 어떻게 해야 할까요? 현재 페이지를 강제로 총 페이지 번호로 치환하는 것도 방법이 될 것입니다.
int page = 4; int totalCount = 25; int countList = 10; int totalPage = totalCount / countList; if (totalCount % countList > 0) { totalPage++; } // 여기까지 계산하면 totalPage 는 3 이 됩니다. // 4 페이지가 현재 페이지라고 했으나, 실제로 3 페이지가 총 페이지이므로 강제로 현재 페이지는 3 페이지라고 보정할 수 있습니다. if (totalPage < page) { page = totalPage; }
이번에는, 하단에 표시될 페이지 번호들을 어떻게 알아낼 수 있는지 알아보겠습니다.
이번 예제에는 totalCount = 255, countList = 10, countPage = 10 으로 하겠습니다. 그리고 현재 페이지인 page 는 5 와 22 의 두 가지 경우를 생각해보겠습니다. 또한, 페이지 표시 방식은 대한민국에서 가장 많이 쓰는 방식인 현재 페이지가 속한 countPage 개의 페이지를 보이는 방식으로 하겠습니다. 이 방식은 현재 페이지 기준으로 앞 뒤 몇 개의 페이지를 보여주는 외국의 방식보다 대한민국에서 더 많이 쓰는 방식입니다.
보통 한국에선 5 페이지를 보고 있다면 다음과 같은 형태로 하단 페이징 부분을 보여주는 경우가 많습니다. 빨간색으로 칠해진 5 가 현재 페이지입니다. 일반적으로 현재 페이지가 포함된 한 블럭이 모두 표시가 되고 그 블럭을 벗어나지 않는다면 어떤 페이지라도 동일한 페이지 번호들이 출력되는게 특징입니다. 아래 예제에서는 1~10 페이지 중 어떤 페이지에 존재하더라도 현재 페이지 표시를 제외하고 다른 모든 것들이 동일하게 보이고 동작한다는 것입니다.
[처음 페이지] [이전 페이지] 1 2 3 4 5 6 7 8 9 10 [다음 페이지] [마지막 페이지]
하지만, 서양 문화권에서는 아래와 같이 출력하는 경우가 더 많습니다. 보통 서양에선 현재 페이지를 기준으로 앞과 뒤의 몇 페이지를 보여주는 형태로 많이 구현됩니다. 그래서 현재 페이지는 페이징 문자열의 중앙에 위치하는 것이 보통입니다.
1 ... 4 5 6 ... 26
장단점이 있겠지만 이 글에서는 위의 방식을 가지고 설명할 것이고, 3 부에 걸친 문서를 모두 숙지하신다면 아래의 방식도 충분히 혼자서 구현하실 수 있으리라 믿습니다. 원리가 크게 다르지 않을 것이니까요.
이제 실제 구현에 대해서 설명해 보겠습니다.
먼저 page = 5 일 경우입니다. 이 경우 우리가 예상하는 결과는 하단에 1 ~ 10 까지의 페이지 번호가 표시되는 것입니다. 그 이유는 countPage 가 10 이기 때문입니다. countPage 가 10 일 경우 처음에는 1~10, 그 다음은 11~20, ... 이런 식으로 10 개씩 표시되는 것을 예상할 수 있습니다. countPage 가 만약 7 이라면 1~7, 8~14, ... 이런 식으로 표시되겠죠.
이런 페이지 번호 계산을 하는 방법은 몇 가지가 있습니다만, 가장 간단하고 쉬운 방법은 바로 시작 페이지 번호를 먼저 계산해내는 것입니다. page = 5 일 경우 시작 페이지(startPage)는 1, 마지막 페이지(endPage)는 시작 페이지에서 10 페이지까지라는 건 금방 이해하시리라 생각합니다. 그럼 page = 5 에서 어떻게 1 페이지를 찾아낼 수 있을까요? 아주 쉽습니다. 그냥 countPage 로 나눠버리고 1 을 더해주면 됩니다. 그럼 마지막 페이지는 어떨까요? startPage 에 countPage 을 더한 뒤 1 을 빼주면 됩니다.
아래 예제 코드에는 startPage 와 endPage 을 구하는 방법이 기술되어 있습니다. 그런데, 위에서는 간단하게 시작 페이지를 1 이라고 했는데, 실제 산술식에서는 조금 복잡한 형태를 띄고 있습니다. 역시나 위와 같이 Java 언어의 int 는 나머지를 지워버리는 원리를 이용하는 것입니다.
첫번째 표시될 페이지 리스트는 단번에 1~10 이라는 숫자라는 걸 알 수 있습니다. 이 숫자들은 10 으로 나눌 경우 10 을 제외하고는 모두 0 이 몫이라는 것도 알 수 있습니다. 다음 페이지 리스트인 11~20 에서는 마찬가지로 20 을 제외한 모든 수의 몫이 1 이라는 것 역시 조금만 생각해보면 알 수 있을 것입니다. 여기서 공식이 나오는데, 결국 리스트에 표시될 숫자가 1~10 일 경우 혹은 11~20 일 경우 현재 페이지가 무엇이든 1 을 뺀 상태에서 나누기를 하여 몫을 구하면 몇 번째 페이지 리스트인지 알 수 있다는 것입니다. 여기에 다시 countPage 을 구하고 계산을 위해 빼주었던 1 을 더하면 시작 페이지를 구할 수 있게 됩니다.
int page = 5; int countPage = 10; int startPage = ((page - 1) / countPage) * countPage + 1; int endPage = startPage + countPage - 1;
실제 보이는 페이지는 1 ~ 10 까지이지만, 실제 페이지 번호는 프로그램 내에서 0 ~9 까지로 처리하는 경우도 있습니다. 그게 Java 와 같은 언어에선 위에서 처럼 +1 와 -1 처리를 생략할 수 있어 내부 구현이 더 간단하기 때문인데, 저 역시 시작 페이지는 0 으로 해서 처리한 클래스를 만들어서 사용하는 경우가 많습니다만, 이해를 돕기 위해 여기서는 1 페이지는 1 의 page 번호를 가지도록 하겠습니다.
어쨌든, 이러한 공식에 의해 현재 페이지가 5 일 경우를 상정해서 계산을 하면 startPage = 1, endPage = 10 이라는 결과가 나옵니다.
그럼 화면에 출력할 때에는 어떻게 해야 할까요? 실제 화면에 출력될 때에는 시작 페이지와 마지막 페이지 사이의 번호를 모두 출력하면 됩니다. 그래서 loop 을 이용해서 숫자를 그대로 출력하면 됩니다.
int page = 5; int countPage = 10; int startPage = ((page - 1) / 10) * 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 - 1) / countPage) * countPage + 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 - 1) / countPage) * countPage + 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>"); }
이렇게 처리하면 페이징 표시를 할 수 있게 됩니다. 참 쉽죠~~~잉? (고 밥 로스 아저씨 따라하기)
2019/01/21 - [Programming/기타] - 페이징(Paging)에 대한 이해 - (2) ROW NUMBER 을 이용한 게시물 가져오기
'Programming > 기타' 카테고리의 다른 글
페이징(Paging)에 대한 이해 - (3) LIMIT 와 TOP 을 이용한 게시물 가져오기 (0) | 2019.01.21 |
---|---|
페이징(Paging)에 대한 이해 - (2) ROW NUMBER 을 이용한 게시물 가져오기 (0) | 2019.01.21 |
시니어 프로그래머로 넘어가는 길 (3) (2) | 2019.01.21 |
시니어 프로그래머로 넘어가는 길 (2) (4) | 2019.01.20 |
시니어 프로그래머로 넘어가는 길 (1) (0) | 2019.01.19 |
- Total
- Today
- Yesterday
- 내장 WAS
- Nas
- couchbase
- RestTemplate
- messages.properties
- paging
- 워드프레스
- boot
- Spring MVC
- 외장 WAS
- Phabricator
- 클라우드플레어
- 도입기
- KDE
- jooq
- SI
- manjaro
- docker
- 페이징
- 엘지
- Redmine
- OracleJDK
- proxmox
- java config
- 시니어 프로그래머
- Spring Boot
- git
- Spring
- NoSQL
- 프로젝트 규모
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |