신입 개발자 면접 기초

웹 개발 페이지 처리(Paging) 방법 - 성능을 고려해보자

JEONG_AMATEUR 2017. 11. 4. 23:58
반응형

웹 개발 페이지 처리(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

반응형