웹 개발 페이지 처리(Paging) 방법 - 성능을 고려해보자
웹 개발 페이지 처리(Paging)
웹 개발을 하면서 당연하게(?) 고민하게 되는 것은 게시판의 페이징 처리다.
실제로 면접에서 질문을 받아봤다.
"페이징처리할 때 어떻게 하셨어요?"
대답은 게시물의 총 개수와 한 페이지당 보여줄 게시물의 개수를 기준으로 이렇게 저렇게 해서 DB는 Mysql을 썼었으니까 LIMIT로 가져왔다고 대답했다.
그런데 그런건 당연한 얘기고 본인이 듣고 싶었던 키워드는 "커서"였다고 했다.
흠.. 여전히 잘 모르겠으나 페이지 처리를 정리해보고자 한다.
MySQL 에는 LIMIT, MS-SQL 2012에서는 OFFSET Fetch로 페이징 쿼리를 조금 더 쉽게 작성할 수 있으나 현업에서 해당 버전, 해당 DBMS를 사용한다는 보장이 없으니 기본적인 것을 알고 가도록 한다.
게시판 페이지 처리 기본 과정 (Oracle)
1. 전체 게시물의 개수를 가져온다. (totalCount)
1 | select count(*) as totalCount from board | cs |
2. 한 페이지에 몇 개의 게시글을 보여줄지를 정한다. (listCount) ex) 10개, 20개씩 보여준다!
3. 이제 게시판이 몇 개의 페이지를 가지는지 구할 수 있다. (총 페이지 수, totalCount)
totalPage = totalCount / listCount ;
나눠떨어지지 않는 경우에 추가로 페이지가 하나 더 있어야 하므로 +1을 해준다.
1 2 3 4 5 6 7 8 9 | int totalCount = /*DB접속후 쿼리를 통해 얻은 값*/; int listCount = 10; int totalPage = totalCount / listCount; if (totalCount % listCount > 0) { totalPage++; } | cs |
이것으로 1에서 총 페이지 수(totalPage)까지 출력 가능
또한 url에서 임의로 page수를 바꿀 수 있으므로 총 페이지 수 보다 높은 페이지를 접근 못하게 예방한다.
1 2 3 4 | //page = 현재 보고있는 페이지 if (totalPage < page){ page = totalPage; } | cs |
4. 하단에 페이지 번호들을 몇 개 보여줄지 정한다. (pageCount 10개=> 1~10페이지, 11~20페이지, ...)
5. 시작페이지와 끝페이지를 계산한다.
1 2 3 4 5 | int startPage = ((page - 1) / pageCount) * pageCount + 1; //현재 페이지가 pageCount와 같을 때를 유의하며 (page-1)을 하고 // +1은 첫페이지가 0이나 10이 아니라 1이나 11로 하기 위함임 int endPage = startPage + pageCount - 1; // -1은 첫페이지가 1이나 11 등과 같을때 1~10, 11~20으로 지정하기 위함임 | cs |
6. 대신 5와 같이 끝페이지를 계산해버리면 totalPage(총 페이지 수)보다 크게 잡힐 위험이 있으니 그것을 처리함.
1 2 3 | if (endPage > totalPage) { endPage = totalPage; } | cs |
7. 이제 현재 페이지(page)를 가지고 시작 페이지와 끝 페이지를 동적 계산하도록 변경되었으므로 현재페이지를 자유롭게 옮겨다닐 수 있도록 [처음][이전] /*1,2,3,4,...,10*/ [다음][끝] 링크를 만듦.
=> 단순히 처음, 끝은 page=1,page=totalPage로 링크시키면되고 이전, 다음은 page-1, page+1로 링크시키면 됨.
//이전, 다음으로 다음 startpage, endpage 구조로 넘어가게 할 수 있음 > page=startpage-1, endpage+1 하면됨.
끝.
이걸 실제로 spring, jsp에 적용하려면 구조는 조금 더 복잡하겠지만 메커니즘은 정리가 되었다.
그런데 좀더 나아가서 성능이 좋은 게시판, 페이지 처리를 고민해야한다.
Oracle에서 페이징 생각하기
1단계 - oracle에서 rownum이라는 가상 칼럼을 만들어서 번호를 붙여주기
1 2 3 4 5 | select rownum as rnum, A.id, A.name, A.content, A.regdate from ( select id, name, content, regdate from board order by regdate desc ) A |
* order by절과 rownum을 한번에 적용하려고 하면 order by 의 정렬보다 rownum이 먼저 적용되어 엉뚱한 순서로 번호가 붙게 된다.
따라서 인라인뷰로 먼저 정렬을 한 후 가상으로 번호를 붙인 칼럼을 만드는 rownum을 사용한다.
2단계 - between대신 rownum의 범위를 나눠서 정하기 [key point]
1 2 3 4 5 6 | select rownum as rnum, A.id, A.name, A.content, A.regdate from ( select id, name, content, regdate from board order by regdate desc ) A where rownum between 11 and 20 | cs |
이렇게하면 잘 가져올 수는 있으나 데이터가 수 백, 수 천만건이면 엄청나게 느려진다
왜냐하면 rownum을 수백만 데이터에 붙이고 그 중에서 11~20에 있는 값을 가져오기 때문이다.
이것을 나눠서 표현하면 옵티마이저의 힘으로 성능을 좋게 만들 수 있다.
(게시판의 페이지를 볼 때 대부분의 사람들이 5페이지 이상 잘 안보는 특성을 고려함)
1 2 3 4 5 6 7 8 9 | select B.rum, B.id, B.name, B.content, B.regdate from (select rownum as rnum, A.id, A.name, A.content, A.regdate from ( select id, name, content, regdate from board order by regdate desc ) A where rownum <=20 ) B where B.rnum >= 11 | cs |
이렇게 하면 DBMS 옵티마이저가 rownum을 20번까지 붙이다가 그 이상을 필요없다고 판단하고 수행하지 않는다.
그런 후에 rownum의 11번부터 20번을 가져오게 되니 엄청난 성능 향상이 된다. (데이터가 많을 수록 눈에 띔)
이정도만 해도 100만건인 데이터에서 제일 뒤인 999991~1000000인 부분의 수행시간은 약 0.8초가 된다.
3단계 - 인덱스를 이용하여 한 발 더 나아가기.
1 2 3 4 5 6 7 8 9 | select B.rum, B.id, B.name, B.content, B.regdate from (select /*+index_desc(A 인덱스명)*/ rownum as rnum, A.id, A.name, A.content, A.regdate from ( select id, name, content, regdate from board order by regdate desc ) A where rownum <=20 ) B where B.rnum >= 11 | cs |
select문에서 옵티마이저에게 인덱스 힌트를 주는 방법을 사용하면 조금 더 성능이 좋은 페이징 처리를 할 수 있다.
기본키는 자동으로 유니크인덱스가 설정되므로 적용하는 것이 좋겠다.
이것도 다른 블로그의 실험을 보면 999991~1000000인 부분의 수행시간은 약 0.6초가 된다.
* 인덱스를 잘 지정하는 것이 포인트같다.
+ MySQL의 LIMIT는 위의 오라클의 rownum+인덱스를 조합한 것보다도 성능이 월등히 좋다...
참고 사이트 :
http://kdarkdev.tistory.com/272
http://fruitdev.tistory.com/45
http://egloos.zum.com/cubenuri/v/2273888