JSP 와 Oracle 조합에서 페이징을 처리하기 위한 작업을 진행해 보았다.
기존에 ASP 작업을 할때만 해도, 누군가가 만들어 놓은 소스를 약간만 변형해서 썼기 때문에 페이징을 별로 신경쓰지 않았는데,
JSP 의 경우에 rs.absolute() 나 rs.first() 같은 명령이 기본 설정으로는 사용할 수 없게 되어 있기 때문에,
그런 명령 없이 자체적으로 페이지를 분석해서 페이지 블럭을 만들어 보기 위해 분석을 시작.
생각보다 훨씬 어렵고 난해해서 이틀을 꼬박 분석을 했다.
물론, JSP 에서도 rs.absolute() 를 쓸 수 있는데, 그러려면 최초에 createStatement 를 설정할때 조금 다르게 해줘야 한다.
rs.absolute() 나 rs.first() 처럼 레코드의 커서를 이리저리 전후방으로 움직이게 하려면 아래와 같이,
stmt = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
로 선언을 해주면 된다.
이렇게 선언을 해두면, 커서를 전후방(양방향,역방향) 으로 모두 이동할 수 있다.
하지만, 보통 JSP 코드를 사용할때 위와같이 선언을 하지 않고, 단순하게
stmt = conn.createStatement();
로만 선언을 해서 쓰는데, 이렇게 되면, 일부 JDBC 명령어를 쓸 수 없어 오류가 발생한다.
따라서, 기본선언만 해놓고 페이징을 할 수 있도록 코드를 처리해봤다.
다른 포스팅에서도 언급했듯이, 페이징을 하는 것은 어떤 원리와 방식을 따르느냐에 따라 알고리즘이 달라지게 되고,
사용자에게 어떤 식으로 페이지 블럭을 보여줄 것인가에 따라서도 코드 구조가 바뀌게 된다.
여기서는, Oracle 의 rownum 을 이용해 레코드를 필요한 만큼만 잘라서 가져오고,
페이지 블럭을 보여주는 부분은 순수하게 JSP 코드만을 이용해서 분석하여 처리했다.
설명을 위해 기존에 작성한 소스를 잘라와서 옮겨놨기 때문에, 실제 적용하면 오류가 나는 부분이 있을 수 있다.
rownum 이라는 컬럼은 가상의 컬럼이다.
실제로는 존재하지 않지만, 오라클은 레코드를 불러오면 가상으로 rownum 이라는 컬럼에 번호를 매겨서 반환한다.
따라서, rownum 에 지정된 번호를 기준으로 레코드를 잘라서 가져올 수 있다.
하지만, Oracle 10g Express Edition 에서는 rownum 의 최고한도를 지정할 수 있지만 범위를 지정해서 가져오지는 못했다.
즉, rownum<11 같은 쿼리는 되지만,
rownum>2 and rownum<11
또는
between rownum>2 and rownum<11
같은 쿼리를 이용하면, 아무 레코드도 가져오지 못했다.
Oracle 10g Express Edition 의 버그인지 오류인지는 모르겠지만, 아무튼 범위를 지정하지 못하기 때문에 무용지물이다.
그러나, 어떤 개발자가 편법을 이용해 범위를 지정한것처럼 잘라오는 코드를 소개하고 있어,
그 방법에 따라 범위를 지정해서 잘라올수는 있다.
하지만, 어디까지나 편법이기 때문에, 데이타베이스의 성능에는 문제가 있을 수 있다.
따라서, 데이타베이스의 성능 향상을 원한다면, 테이블을 만들때 인덱스 컬럼과 키를 생성해서 그에 따라 처리하는 것이 훨씬 효과적이다.
다만, 여기서는 인덱스를 만들수 있는 상황이 아니거나 인덱스 없이 쓰고 싶을때 사용한다고 가정하고 코드를 작성했다.
String sql = "select * from \"테이블명\" ";
beginrow = 2;
endrow = 11;
String cutrowsql = "select * from (select A.*, rownum rownum_ from (" + sql.toString() + ") A where rownum<= " + endrow + " ) where rownum_>= " + beginrow + "";
위의 코드와 같이, 시작할 beginrow 와 종료할 endrow 를 변수로 지정한후,
rownum 분석을 위해 쿼리를 이중으로 처리하고, 안에서 한번 뒤집어서 처리하는 방식이다.
페이징의 방식은 아래의 그림과 같다.
일단, 테스트를 위해 페이지의 블럭은 3개 까지만 표시했다.
3개 블럭 이상의 레코드가 있는 경우, 우측에 '다음목록' 을 표시.
여기서 말하는 '블럭' 이란, '페이지' 와 다른 개념이다.
'페이지' 는 페이지 번호를 눌렀을때 나오는 목록으로, '한 페이지에 몇개의 레코드를 보여주는가' 와 관련된 개념이다.
블럭은 그런 페이지들이 몇개가 있느냐 하는 개념이다.
위에서는 표시할 블럭수를 3개로 지정했기 때문에, 3개의 페이지만 나온다.
여기서 약간 개념의 혼동이 발생한다.
난데없이 '블럭' 이 뭔가?
사실, 이 부분이 좀 쌩뚱맞고 헷갈리게 하는 부분이기는 하다.
여기서 말하는 '블럭' 은, 페이지를 몇개를 표시하느냐 하는 의미다.
표시블럭수를 3 으로 지정하면 3페이지 까지만 표시되고, 나머지는 '이전목록' 이나 '다음목록' 이라는 글씨로 표시한다.
페이지가 엄청나게 많을 경우 모두 화면에 표시할수는 없기 때문이다.
개념상 혼동을 주는 명칭이기는 하지만 아무튼 그렇다.
위의 화면에서 보듯이 1,2,3 페이지가 한 묶음이고, 한 블럭이 된다.
만약, 그 이전 단계의 그림에서 '다음목록' 을 눌렀다면, 위 그림처럼 '4' 페이지가 표시되고, 그 이전의 페이지들은 '이전목록' 으로 표시된다.
'이전목록' 을 누르면 그 이전의 블럭인 '1,2,3' 페이지가 표시되는 것이다.
다시 '이전목록' 을 눌러보니, 위 그림처럼 '3' 페이지가 표시되고, 3페이지 이후의 목록은 다시 '다음목록' 으로 표시되어 숨겨진다.
명칭이 좀 모호해서 개념에 혼동이 올 수 있다.
일단, 페이징을 처리하기 전에 해야할 것은,
1. totalrecord : 전체 레코드수
2. pagesize : 한 페이지에 표시하는 레코드의 수
3. pagenum: 이전 페이지에서 넘어온 '연결할 페이지 번호'
4. totalpage : 레코드수와 한페지에 표시되는 목록수를 기준으로 계산한 전체 페이지의 갯수
5. pageea : 화면에 표시할 페이지 블럭의 갯수(한 블럭에 표시할 페이지의 갯수)
6. pos : 이전 페이지에서 넘어온 '좌우측 정렬값'.
이 값은, '이전목록' 이나 '다음목록' 을 누를경우, 해당 블럭의 첫 페이지를 보여줄 것인지, 끝 페이지를 보여줄 것인지를 지정하는 값이다.
이 값들을 기준으로 페이징이 처리되며,
아래의 코드에서는 사용상 편의를 위해, 그리고 재사용이 용이하도록 함수 형식으로 만들었다.
함수로 만들어 두면, 나중에 여기저기서 불러다 쓸 수 있기 때문에, 같은 코딩을 중복해서 할 필요가 없다.
해당 함수에서는, 넘겨진 변수값을 기준으로 페이징을 처리하기 위한 계산을 해야 한다.
실제로 분석된 표시할 수 있는 블럭의 갯수가 몇개인지, '이전목록' 과 '다음목록' 을 표시할 것인지,
현재가 몇번재 블럭이고, 현재 블럭의 시작페이지는 몇 페이지이며 마지막 페이지는 몇 페이지 인지 등등.
사실, 이 부분이 마치 수학문제 같았다.
개념이 좀 혼동되어서 이 부분을 해결하는데 시간이 걸렸는데,
여기에 기재한 코드에서는 페이지를 블럭 단위로 표시한다.
즉,
블럭에 표시한 페이지의 수를 3 으로 지정했다면,
(1 2 3 ) (4 5 6) (7 8 9) (10 11 )
과 같은 식으로 블럭을 나누는 방식이다.
여기서 (1,2,3) 이 한 블럭이 되고, (4,5,6) 이 한 블럭이 되는 셈이다.
따라서, 페이지가 11개라면, 4개의 실제 블럭이 만들어진다.
그리고, (1,2,3) 페이지를 누를때는 1 블럭에서만 이동하고, '다음목록' 을 누르면 두번째 블럭으로 이동해서 (4,5,6) 페이지를 표시하는 방식이다.
현재가 두번째 블럭이어서 화면에 (4,5,6) 페이지가 표시되었다면, 좌측에는 '이전목록' 링크가 있고, 우측에는 '다음목록' 링크가 생겨난다.
즉, 여기서 '이전' 과 '다음' 은 블럭을 이동하는 개념이다.
그러므로, '이전목록' 링크를 누르면, 현재 블럭의 첫번째 페이지에 -1 을 계산한 페이지로 이동하되, 이전의 블럭을 표시해준다.
현재가 두번째 블럭의 첫번째 페이지인 4 페이지인데, '이전목록' 을 누르면,
이전 블럭인 (1,2,3) 이 표시되고, 현재 페이지는 3 페이지가 되는 방식이다.
반대로, 현재 페이지가 (4,5,6) 인데, 4,5,6 중 어느 페이지에 있건 간에 우측의 '다음목록' 링크를 누른다면, 다음 블럭으로 이동하겠다는 뜻이므로, 다음 블럭인 (7,8,9) 를 화면에 보여준다.
그리고, 페이지는 7 페이지로 이동한다.
이러한 알고리즘을 처리하기 위해서는 꽤나 복잡한 공식이 필요하다.
실제로는 존재하지 않는 '블럭' 개념이 들어가 있기 때문이다.
기존에 ASP 에서는 rs.absolute() 나 rs.pagesize() 같은 명령을 통해 꽤나 쉽게 처리가 되었지만,
그런 명령을 사용하지 않고 처리하려니 훨씬 복잡해진것 같다.
(JSP JDBC 에는 rs.absolute() 는 있지만, rs.pagesize() 는 없다)
물론, 이미 숙련된 개발자들이 각종 라이브러리를 많이 만들어서 공개하고 있기 때문에 페이징을 괜히 어렵게 할 필요없이 가져다 쓸 수 있지만, 페이징의 개념을 이해하고 있는 것이 중요하며, 원리를 이해하고 있을때 새로운 페이징 개념을 만들수도 있다.
블럭단위 이동 개념에서 가장 중요한 변수는 한 블럭의 레코드 갯수이다.
즉, 변수로 지정한 한블럭의 페이지수를 기준으로, 한 블럭에 들어갈 수 있는 총 레코드수를 계산한다.
그리고, 현재 페이지를 기준으로 현재가 몇번째 블럭에 속하는지 계산하고, '이전목록' 을 누르거나 '다음목록' 을 누를경우, 몇번째의 가상 블럭으로 이동할 것인지를 지정하는 것이다.
이때, 계산식에서 보면, Math.ceil 을 이용해 소숫점을 무조건 올림 하기도 하고, Math.floor 을 이용해 무조건 버리기도 한다.
그리고, 레코드수를 기준으로 나눗셈을 해서 나머지와 몫을 구하기 때문에, 0 이 될때가 있다.
이때는 현재 블럭에 속한 것으로 가정하고, 0 이 아니면 + 1 이나 -1 을 해서 블럭을 이동한다.
물론, 경우의 수를 예측해서 에러처리는 모두 해줘야 한다.
아래의 코드에서는 일부 에러처리가 안되어 있는 부분이 있으므로 주의.
==================================================================================================
<%@ page contentType="text/html;charset=euc-kr" %>
<%@ page import="java.sql.*" %>
<%//DB 연결 및 객체 생성------------------------------
String dburl = "DB 연결주소";
String dbcid = "DB 연결 CID 이름";
String dbcpw = "DB 연결 CID 이름의 비밀번호";
Class.forName( "oracle.jdbc.driver.OracleDriver" );
Statement stmt=null;
Connection conn=null;
conn = DriverManager.getConnection(dburl,dbcid,dbcpw);
stmt = conn.createStatement(); // 전방향만 가능
%>
<%//-------페이징을 위한 기본 분석 및 변수 설정-------------------
String str_pagenum = request.getParameter("pagenum"); //연결할 페이지 정보(이전 페이지에서 넘겨진 값)
int pagenum = Integer.parseInt(str_pagenum); //계산에 사용되기 때문에 숫자형으로 변환
int totalrecord = 0;
int totalpage = 0; //전체 페이지수 (전체목록수 / 페이지당 목록수)
int pagesize = 3; //한 페이지당 목록수(기본값 10)
int beginrow = 1; //리스트의 시작위치 기본값(시작 row 번호)
int endrow = 1; //리스트의 종료위치 기본값(마지막 row 번호)
//전체 레코드수를 얻기 위한 쿼리 처리---------------
String countsql= "select count(*) from \"테이블명\" ";
try{
ResultSet countrs = stmt.executeQuery(countsql);
countrs.next(); //반드시 최초에 .next() 를 해줘야 처리됨(처음 접속시 커서가 0 위치에 있기 때문
int listcount = countrs.getInt(1); //첫번째 컬럼의 값을 숫자로 리턴
if(listcount==0){
totalrecord = 0;
}else{
totalrecord = listcount;
}
countrs.close();
}catch(Exception e){
totalrecord = 0;
out.print(e.toString());
}
//레코드가 있는 경우, 지정한 pagesize(한 페이지당 출력할 목록의 갯수) 값에 따라, 시작 rownum 와 끝 rownum 를 분석
if (totalrecord>0){
//전체 페이지수 (전체목록수 / 페이지당 목록수) (Math.ceil 이용해 소숫점 무조건 올림)
totalpage = (int)Math.ceil((double)totalrecord/pagesize);
//시작 row 찾기(row 는 1 부터 시작)
if (pagenum<=1){ //현재페이지가 1 이면
beginrow = 1;
}else{
beginrow = pagesize*pagenum-pagesize+1; //선택한 블럭의 첫번째 rownum 값을 계산.
//계산된 시작 rownum 의 번호가 전체레코드수 보다 크다면, 해당 블럭에는 레코드가 없는것.
}
endrow = beginrow + pagesize - 1;
//계산된 마지막 rownum 의 번호가 전체레코드수 보다 크다면, 해당 블럭에는 불러올 레코드가 없는것.
}else{
totalpage = 0;
beginrow = 0;
endrow = 0;
}
%>
<%
//기본 쿼리 지정--------------------------
String sql = "select * from \"테이블명\" ";
//rownum 분석에 따라 잘라오기 위한 별도의 쿼리 지정------
String cutrowsql = "select * from (select A.*, rownum rownum_ from (" + sql.toString() + ") A where rownum<= " + endrow + " ) where rownum_>= " + beginrow + "";
try{
ResultSet rs = stmt.executeQuery(cutrowsql.toString());
int viewlistcount = 0;
int reverserow = 0;
String val1 = "";
//--------레코드 불러오기 및 출력-------------
if (totalrecord>0){
while (rs.next()){
//목록 좌측에 순번을 표시하기 위해 역순의 번호를 만든다
reverserow = pagesize * (totalpage-pagenum+1) - ((totalpage*pagesize)-totalrecord) - viewlistcount;
++viewlistcount;
//아래의 out.flush() 부분은 없어도 상관없음
if (viewlistcount>pagesize){ out.flush(); break; } //한페이지당 출력수를 넘어설 경우
if (viewlistcount>endrow){ out.flush(); break; } //최종row 를 넘어설 경우
val1 = rs.getString("컬럼1");
} //end while
} //end totalrecord check if
rs.close();
}catch(Exception e){
out.println(e.toString());
out.close();
} //end rs try
if (totalrecord==0){
out.println("레코드가 없습니다");
}
%>
<!--페이징 정보를 표시하기 위한 div 태그-->
<div id="pagelistbox" name="pagelistbox" style="height:26px;background-color:#F9F9F9;">page list</div>
<%
String pos = request.getParameter("pos"); //페이지 블럭의 좌우측 정렬 결정
String listpagebox = ""; //페이징을 처리한 함수를 불러온다
listpagebox = temp_pagelist("pagesend2",pagenum,totalpage,totalrecord,pagesize,3,pos);
%>
<script language="javascript">
function pagesend2(act,pos){
window.location.href="연결파일.jsp?pagenum="+act+"&pos="+pos;
}
</script>
<%
stmt.close();
conn.close();
%>
==================================================================================================
<%!
//아래의 부분은, 페이징 부분을 별도로 호출해서 쓸수 있도록 만든 함수---------
//이 함수부분은 리스트를 출력하는 페이지에 넣어도 되고, 별도의 페이지에 넣은후 include 로 불러다 쓸 수 있다
public static String temp_pagelist(String btnname, int pagenum, int totalpage, int totalrecord, int pagesize, int pageea, String pos) {
//위에서 지정되어 내려온 변수:
//pagenum(쿼리로 넘겨진 현재페이지), totalpage(레코드수를 기준으로 분석한 전체 페이지수)
//totalrecord (총 레코드수),
//pagesize(한 페이지당 레코드수),
//pageea (페이지 블럭의 갯수)
//pos (현재 페이지를 좌측 끝으로 정렬할지, 우측 끝으로 결정할지 선택) //좌측정렬이면 값이 2, 우측정렬이면 1, 정렬아닌 경우 값 없음.
String errmsg = "";
String pagetag = "";
if (pos==null){ pos=""; } //null 처리 안하면, pos 값이 없는 경우 에러발생.
try{
if (totalrecord>0){
int beginpage = 1;
int endpage = 1;
//지정한 블럭당 페이지 갯수 기준으로 총 블럭의 수 (소숫점 이하 무조건 올림)
int blockcount = (int)Math.ceil((double)totalpage/pageea);
int nowblock = 1;
int thispagerecord = pagenum * pagesize; //현재 페이지의 총 레코드수
int oneblockrecord = pagesize * pageea; //한 블럭단위의 총 레코드수
int blockcomp1 = (int)Math.floor((double)thispagerecord/oneblockrecord);
float blockcomp2 = (float)((double)thispagerecord/oneblockrecord);
//나머지값이 0 인 경우를 체크 (해당 블럭의 마지막 페이지인 경우 0 값이 나옴. 이때는 해당 블럭으로 계산)
int blocktouch = (thispagerecord%oneblockrecord);
//나머지값이 0 이 아닌 경우에는 계산된 블럭값에+1 을 해주면 실제 블럭이 됨
if ((int)blocktouch==0){ nowblock = blockcomp1; }else{ nowblock = blockcomp1 + 1; }
//현재 블럭의 시작페이지 번호
beginpage = (nowblock * pageea) - pageea + 1;
//현재 블럭의 끝페이지 번호
endpage = nowblock * pageea;
//errmsg = Integer.toString(blockcount);
boolean toss1 = false; //왼쪽 '다음목록' 설정
boolean toss2 = false; //오른쪽 '다음목록' 설정
if ((nowblock>1)&&(blockcount>1)){ toss1 = true; }
//끝페이지가 지정한 블럭의 갯수에 도달하지 않는다면
if ((pagenum<=totalpage)&&(nowblock<blockcount)){ toss2 = true; }
//레코드수가 한 블럭당 레코드수보다 작거나 같으면, 보여줄 블럭의 첫페이지는 1, 마지막 페이지는 페이지 총 갯수
if(totalrecord<=(pagesize*pageea)){
beginpage = 1;
endpage = (int)Math.ceil((double)totalrecord/pagesize);
}
int toss1page = 1;
int toss2page = 1;
toss1page = beginpage - 1;
toss2page = endpage + 1;
if (toss1page<=0){ toss1page = 1; }
if (toss2page<=0){ toss2page = 1; }
//해당 블럭의 끝페이지가 전체 페이지보다 크다면, 끝페이지는 전체페이지 수
if (endpage>totalpage){ endpage = totalpage; }
pagetag = "<div style='font-size:12pt;letter-spacing:2;padding-top:3;background:;border-bottom:0 solid #FFFFFF;color:#000000;text-align:center;'>";
String thislight = "font-weight:bold;color:#FF0000;";
String thislight2 = "";
if (toss1==true){
pagetag = pagetag + "<span style='cursor:hand;' onclick=javas-ript:" + btnname + "('"+toss1page+"','1');>이전목록◀</span>";
}
//현재 블럭의 시작페이지 부터 끝페이지까지 루프하여 리스트를 만든다.
for (int i=beginpage; i<=endpage; i++){
if (i==pagenum){ thislight2 = thislight; }else{ thislight2 = ""; }
pagetag = pagetag + " <span style='cursor:hand;"+thislight2+"' onclick=javas-ript:" + btnname + "('"+ i +"','0');>" + i + "</span>";
}
if (toss2==true){
pagetag = pagetag + "<span style='cursor:hand;' onclick=javas-ript:" + btnname + "('"+toss2page+"','2');>▶다음목록</span>";
}
pagetag = pagetag + "</div>";
}else{
//레코드가 0 인 경우
pagetag = "";
}
}catch(Exception e){
pagetag = e.toString();
//pagetag = "error";
}
if (!errmsg.equals("")){
pagetag = errmsg;
}else{
pagetag = pagetag.toString();
}
return pagetag;
}
%>