java-school logo

게시판 프로그램 이해하기

SQL*PLUS으로 scott 계정으로 접속한 후, 다음 스크립트를 사용하여 게시판을 위한 테이블과 시퀀스를 생성한다.

board-schema.sql
-- 게시판 테이블
create table board(
 no number primary key,
 title varchar2(200) not null,
 content varchar2(4000),
 wdate date
)
/
-- 게시판 no 컬럼 값을 위한 시퀀스 
create sequence board_no_seq
start with 1
increment by 1
/

board-schema.sql 파일을 이용하려면, scott 계정으로 SQL*PLUS에 접속한 후 @ 입력하고 이어서 board-schema.sql 파일의 전체경로를 입력한다. C:\에 board-schema.sql 파일이 있다면 다음과 같이 실행한다.

C:\ Command Prompt
SQL>@C:\board-schema.sql

scott 계정으로 SQL*PLUS에 접속한 상태에서 다음 인서트 문을 실행한다. (총 100개의 인서트 문)

board-data.sql
insert into board values (board_no_seq.nextval, '000001','',sysdate);
insert into board values (board_no_seq.nextval, '000002','',sysdate);
insert into board values (board_no_seq.nextval, '000003','',sysdate);
insert into board values (board_no_seq.nextval, '000004','',sysdate);
insert into board values (board_no_seq.nextval, '000005','',sysdate);
insert into board values (board_no_seq.nextval, '000006','',sysdate);
insert into board values (board_no_seq.nextval, '000007','',sysdate);
insert into board values (board_no_seq.nextval, '000008','',sysdate);
insert into board values (board_no_seq.nextval, '000009','',sysdate);
insert into board values (board_no_seq.nextval, '000010','',sysdate);
insert into board values (board_no_seq.nextval, '000011','',sysdate);
insert into board values (board_no_seq.nextval, '000012','',sysdate);
insert into board values (board_no_seq.nextval, '000013','',sysdate);
insert into board values (board_no_seq.nextval, '000014','',sysdate);
insert into board values (board_no_seq.nextval, '000015','',sysdate);
insert into board values (board_no_seq.nextval, '000016','',sysdate);
insert into board values (board_no_seq.nextval, '000017','',sysdate);
insert into board values (board_no_seq.nextval, '000018','',sysdate);
insert into board values (board_no_seq.nextval, '000019','',sysdate);
insert into board values (board_no_seq.nextval, '000020','',sysdate);
insert into board values (board_no_seq.nextval, '000021','',sysdate);
insert into board values (board_no_seq.nextval, '000022','',sysdate);
insert into board values (board_no_seq.nextval, '000023','',sysdate);
insert into board values (board_no_seq.nextval, '000024','',sysdate);
insert into board values (board_no_seq.nextval, '000025','',sysdate);
insert into board values (board_no_seq.nextval, '000026','',sysdate);
insert into board values (board_no_seq.nextval, '000027','',sysdate);
insert into board values (board_no_seq.nextval, '000028','',sysdate);
insert into board values (board_no_seq.nextval, '000029','',sysdate);
insert into board values (board_no_seq.nextval, '000030','',sysdate);
insert into board values (board_no_seq.nextval, '000031','',sysdate);
insert into board values (board_no_seq.nextval, '000032','',sysdate);
insert into board values (board_no_seq.nextval, '000033','',sysdate);
insert into board values (board_no_seq.nextval, '000034','',sysdate);
insert into board values (board_no_seq.nextval, '000035','',sysdate);
insert into board values (board_no_seq.nextval, '000036','',sysdate);
insert into board values (board_no_seq.nextval, '000037','',sysdate);
insert into board values (board_no_seq.nextval, '000038','',sysdate);
insert into board values (board_no_seq.nextval, '000039','',sysdate);
insert into board values (board_no_seq.nextval, '000040','',sysdate);
insert into board values (board_no_seq.nextval, '000041','',sysdate);
insert into board values (board_no_seq.nextval, '000042','',sysdate);
insert into board values (board_no_seq.nextval, '000043','',sysdate);
insert into board values (board_no_seq.nextval, '000044','',sysdate);
insert into board values (board_no_seq.nextval, '000045','',sysdate);
insert into board values (board_no_seq.nextval, '000046','',sysdate);
insert into board values (board_no_seq.nextval, '000047','',sysdate);
insert into board values (board_no_seq.nextval, '000048','',sysdate);
insert into board values (board_no_seq.nextval, '000049','',sysdate);
insert into board values (board_no_seq.nextval, '000050','',sysdate);
insert into board values (board_no_seq.nextval, '000051','',sysdate);
insert into board values (board_no_seq.nextval, '000052','',sysdate);
insert into board values (board_no_seq.nextval, '000053','',sysdate);
insert into board values (board_no_seq.nextval, '000054','',sysdate);
insert into board values (board_no_seq.nextval, '000055','',sysdate);
insert into board values (board_no_seq.nextval, '000056','',sysdate);
insert into board values (board_no_seq.nextval, '000057','',sysdate);
insert into board values (board_no_seq.nextval, '000058','',sysdate);
insert into board values (board_no_seq.nextval, '000059','',sysdate);
insert into board values (board_no_seq.nextval, '000060','',sysdate);
insert into board values (board_no_seq.nextval, '000061','',sysdate);
insert into board values (board_no_seq.nextval, '000062','',sysdate);
insert into board values (board_no_seq.nextval, '000063','',sysdate);
insert into board values (board_no_seq.nextval, '000064','',sysdate);
insert into board values (board_no_seq.nextval, '000065','',sysdate);
insert into board values (board_no_seq.nextval, '000066','',sysdate);
insert into board values (board_no_seq.nextval, '000067','',sysdate);
insert into board values (board_no_seq.nextval, '000068','',sysdate);
insert into board values (board_no_seq.nextval, '000069','',sysdate);
insert into board values (board_no_seq.nextval, '000070','',sysdate);
insert into board values (board_no_seq.nextval, '000071','',sysdate);
insert into board values (board_no_seq.nextval, '000072','',sysdate);
insert into board values (board_no_seq.nextval, '000073','',sysdate);
insert into board values (board_no_seq.nextval, '000074','',sysdate);
insert into board values (board_no_seq.nextval, '000075','',sysdate);
insert into board values (board_no_seq.nextval, '000076','',sysdate);
insert into board values (board_no_seq.nextval, '000077','',sysdate);
insert into board values (board_no_seq.nextval, '000078','',sysdate);
insert into board values (board_no_seq.nextval, '000079','',sysdate);
insert into board values (board_no_seq.nextval, '000080','',sysdate);
insert into board values (board_no_seq.nextval, '000081','',sysdate);
insert into board values (board_no_seq.nextval, '000082','',sysdate);
insert into board values (board_no_seq.nextval, '000083','',sysdate);
insert into board values (board_no_seq.nextval, '000084','',sysdate);
insert into board values (board_no_seq.nextval, '000085','',sysdate);
insert into board values (board_no_seq.nextval, '000086','',sysdate);
insert into board values (board_no_seq.nextval, '000087','',sysdate);
insert into board values (board_no_seq.nextval, '000088','',sysdate);
insert into board values (board_no_seq.nextval, '000089','',sysdate);
insert into board values (board_no_seq.nextval, '000090','',sysdate);
insert into board values (board_no_seq.nextval, '000091','',sysdate);
insert into board values (board_no_seq.nextval, '000092','',sysdate);
insert into board values (board_no_seq.nextval, '000093','',sysdate);
insert into board values (board_no_seq.nextval, '000094','',sysdate);
insert into board values (board_no_seq.nextval, '000095','',sysdate);
insert into board values (board_no_seq.nextval, '000096','',sysdate);
insert into board values (board_no_seq.nextval, '000097','',sysdate);
insert into board values (board_no_seq.nextval, '000098','',sysdate);
insert into board values (board_no_seq.nextval, '000099','',sysdate);
insert into board values (board_no_seq.nextval, '000100','',sysdate);
commit;

또는, 다음 PL/SQL 문을 실행한다.

DECLARE
  counter INTEGER;
BEGIN
  counter := 1;
  FOR counter IN 1..100 LOOP
    insert into board values (board_no_seq.nextval, LPAD(board_no_seq.currval, 6, 0),'',sysdate); 
  END LOOP;
END;
/

아래 표는 게시판을 구현하기 위해 작성할 파일 목록을 보여준다.

작성할 파일 리스트
list.jsp 게시물의 목록을 보여주는 페이지 (단계적으로 페이지 분할, 페이지 이동 링크, 검색 기능을 추가한다)
write_form.jsp 게시글 입력 양식
BoardWriter.java 게시글을 테이블에 삽입하는 서블릿
view.jsp 게시물의 상세 정보를 출력
modify_form.jsp 수정 입력 양식
BoardModifier.java 게시글을 수정하는 서블릿
BoardDeleter.java 게시글을 삭제하는 서블릿

JSP와 서블릿을 복습하기 위해서, 화면이 필요하면 JSP를, 화면이 필요 없으면 서블릿을 채택했다.
다음은 게시판 프로그램의 흐름이다.

list.jsp → write_form.jsp → BoardWriter.java (insert 실행) → list.jsp
  └── view.jsp
        └── modify_form.jsp → BoardModifier.java (update 실행) → view.jsp
        └── BoardDeleter.java (delete 실행) → list.jsp

ROOT 애플리케이션의 최상위 디렉터리 아래 board 서브 디렉터리를 만들고 이곳에 JSP 파일을 만들겠다.
JSP에서 데이터베이스 이용하기를 실습했다면 사용자 정의 커넥션 풀링 바이트 코드가 WEB-INF/classes에 만들어져 있다.
(사용자 정의 커넥션 풀링 바이트 코드가 없다면 아래 게시판을 테스트할 수 없다)

MyServletContextListener.java
서블릿 절에서 OracleConnectionManager 객체를 웹 애플리케이션이 시작될 때 서블릿 컨텍스트에 담도록 하는 예제(MyServletContextListener.java)가 있었다. 이 리슨너가 ROOT 애플리케이션에서 실행되고 있다.

목록과 글쓰기

아래 목록 페이지 (list.jsp)는 모든 레코드를 보여준다.

/board/list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>목록</title>
</head>
<body>
<h1>목록</h1>
<%
Log log = new Log();

Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;

String sql = "SELECT no, title, wdate FROM board ORDER BY no DESC";

try {
	con = dbmgr.getConnection();

	stmt = con.prepareStatement(sql);
	rs = stmt.executeQuery();

	while (rs.next()) {
		int no = rs.getInt("no");
		String title = rs.getString("title");
		Date wdate = rs.getDate("wdate");
%>
<%=no %> <a href="view.jsp?no=<%=no %>"><%=title %></a> <%= wdate.toString() %><br />
<hr />
<%
  }
} catch(SQLException e) {
	log.debug("Error Source: board/list.jsp : SQLException");
	log.debug("SQLState: " + e.getSQLState());
	log.debug("Message: " + e.getMessage());
	log.debug("Oracle Error Code: " + e.getErrorCode());
	log.debug("sql: " + sql);
} finally {
	if (rs != null) {
		try {
			rs.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (stmt != null) {
		try {
			stmt.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (con != null) {
		dbmgr.freeConnection(con);
	}
	log.close();
}
%>
<p>
<a href="write_form.jsp">글쓰기</a>
</p>
</body>
</html>
/board/write_form.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />

<title>글쓰기</title>
</head>
<body>
<h1>글쓰기</h1>
<form action="../servlet/BoardWriter" method="post">
<table>
<tr>
	<td>제목</td>
	<td><input type="text" name="title" size="50"></td>
</tr>
<tr>
	<td colspan="2">
		<textarea name="content" rows="20" cols="100"></textarea>
	</td>
</tr>
<tr>
	<td colspan="2">
		<input type="submit" value="전송">
		<input type="reset" value="취소">
		<a href="list.jsp">목록</a>
	</td>
</tr>
</table>
</form>  
</body>
</html>
BoardWriter.java
package net.java_school.board;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

import java.sql.*;

import net.java_school.db.dbpool.*;
import net.java_school.util.*;

public class BoardWriter extends HttpServlet {
	private static final long serialVersionUID = 5698354994510824246L;
	
	OracleConnectionManager dbmgr = null;

	@Override
	public void init() throws ServletException {
		ServletContext sc = getServletContext();
		dbmgr = (OracleConnectionManager)sc.getAttribute("dbmgr");
	}
	
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		
		req.setCharacterEncoding("UTF-8");
		Log log = new Log();
		
		String title = req.getParameter("title");
		String content = req.getParameter("content");
		
		Connection con = dbmgr.getConnection();
		PreparedStatement stmt = null;
		//입력 순서: 시퀀스, 제목, 소개글, 본문
		String sql = "INSERT INTO board VALUES (board_no_seq.nextval, ?, ?, sysdate)";
		
		try {
			stmt = con.prepareStatement(sql);
			stmt.setString(1, title); //제목 부분
			stmt.setString(2, content); //본분 부분
			stmt.executeUpdate(); //쿼리 실행
		} catch (SQLException e) {
			log.debug("Error Source: BoardWriter.java : SQLException");
			log.debug("SQLState: " + e.getSQLState());
			log.debug("Message: " + e.getMessage());
			log.debug("Oracle Error Code: " + e.getErrorCode());
			log.debug("sql: " + sql);
		} finally {
			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (con != null) {
				dbmgr.freeConnection(con);
			}
			log.close();
			String path = req.getContextPath();
			resp.sendRedirect(path + "/board/list.jsp");
		}
	}
}

이 서블릿을 web.xml에 등록하고 서블릿 매핑을 /servlet/BoardWriter으로 설정한다.

<servlet>
  <servlet-name>BoardWriter</servlet-name>
  <servlet-class>net.java_school.board.BoardWriter</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>BoardWriter</servlet-name>
  <url-pattern>/servlet/BoardWriter</url-pattern>
</servlet-mapping>

상세보기 페이지

/board/view.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>상세보기</title>
<script type="text/javascript">
function goModify(no) {
	location.href="modify_form.jsp?no=" + no;
}

function goDelete(no) {
	var check = confirm('정말로 삭제하시겠습니까?');
	if (check) {
		location.href="../servlet/BoardDeleter?no=" + no;
	}
}
</script>
</head>
<body>
<h1>상세보기</h1>
<%
int no = Integer.parseInt(request.getParameter("no"));
Log log = new Log();
Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;
String sql = "SELECT no, title, content, wdate FROM board WHERE no = ?";
try {
	con = dbmgr.getConnection();
	stmt = con.prepareStatement(sql);
	stmt.setInt(1, no);
	rs = stmt.executeQuery();
	
	while (rs.next()) {
		String title = rs.getString("title");
		String content = rs.getString("content");
		Date wdate = rs.getDate("wdate");
		if (content == null) content = "";
%>
<h2>제목: <%=title %>, 작성일: <%=wdate.toString() %></h2>
<p>
<%=content = content.replaceAll(System.getProperty("line.separator"), "<br />") %>
</p>
<%
	}
} catch (SQLException e) {
	log.debug("Error Source : board/view.jsp : SQLException");
	log.debug("SQLState : " + e.getSQLState());
	log.debug("Message : " + e.getMessage());
	log.debug("Oracle Error Code : " + e.getErrorCode());
	log.debug("sql : " + sql);
} finally {
	if (rs != null) {
		try {
			rs.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (stmt != null) {
		try {
			stmt.close();
		} catch (SQLException e) {

			e.printStackTrace();
		}
	}
	if (con != null) {
		dbmgr.freeConnection(con);
	}
	log.close();
}
%>
<a href="list.jsp">목록</a>
<input type="button" value="수정" onclick="javascript:goModify('<%=no %>')">
<input type="button" value="삭제" onclick="javascript:goDelete('<%=no %>')">
</body>
</html>

글 수정

/board/modify_form.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<%
int no = Integer.parseInt(request.getParameter("no"));

Log log = new Log();

Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;

String title = null;
String content = null;
String sql = "SELECT title, content FROM board WHERE no = ?";

try {
	con = dbmgr.getConnection();
	stmt = con.prepareStatement(sql);
	stmt.setInt(1, no);
	rs = stmt.executeQuery();
	
	if (rs.next()) {
	    title = rs.getString("title");
	    content = rs.getString("content");
	    if (content == null) content = "";
	}
} catch (SQLException e) {
	log.debug("Error Source: board/modify_form.jsp : SQLException");
	log.debug("SQLState: " + e.getSQLState());
	log.debug("Message: " + e.getMessage());
	log.debug("Oracle Error Code: " + e.getErrorCode());
	log.debug("sql: " + sql);
} finally {
	if (rs != null) {
		try {
			rs.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (stmt != null) {
		try {
			stmt.close();
		} catch (SQLException e) {

			e.printStackTrace();
		}
	}
	if (con != null) {
		dbmgr.freeConnection(con);
	}
	log.close();
}
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>수정</title>
</head>
<body>
<h1>수정</h1>
<form action="../servlet/BoardModifier" method="post">
<input type="hidden" name="no" value="<%=no %>">
<table>
<tr>
	<td>제목</td>
	<td><input type="text" name="title" size="50" value="<%=title %>" /></td>
</tr>
<tr>
	<td colspan="2">
		<textarea name="content" rows="30" cols="100"><%=content %></textarea>
	</td>
</tr>
<tr>
	<td colspan="2">
		<input type="submit" value="전송">
		<input type="reset" value="취소">
		<a href="view.jsp?no=<%=no %>">상세보기</a>
	</td>
</tr>
</table>
</form>
</body>
</html>
BoardModifier.java
package net.java_school.board;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

import java.sql.*;

import net.java_school.db.dbpool.*;
import net.java_school.util.*;

public class BoardModifier extends HttpServlet {
  
	private static final long serialVersionUID = -971206071575571573L;

	OracleConnectionManager dbmgr = null;
	
	@Override
	public void init() throws ServletException {
		ServletContext sc = getServletContext();
		dbmgr = (OracleConnectionManager)sc.getAttribute("dbmgr");
	}
	
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
	
		req.setCharacterEncoding("UTF-8");
		Log log = new Log();
		
		int no = Integer.parseInt(req.getParameter("no"));
		String title = req.getParameter("title");
		String content = req.getParameter("content");
		
		Connection con = dbmgr.getConnection();
		PreparedStatement stmt = null;
		
		String sql = "UPDATE board SET title = ?, content = ? WHERE no = ?";
		
		try {
			stmt = con.prepareStatement(sql);
			stmt.setString(1, title); //제목 부분
			stmt.setString(2, content); //본분 부분
			stmt.setInt(3, no); //primary key
			stmt.executeUpdate(); //쿼리 실행
		} catch (SQLException e) {
			log.debug("Error Source: BoardModifier.java : SQLException");
			log.debug("SQLState: " + e.getSQLState());
			log.debug("Message: " + e.getMessage());
			log.debug("Oracle Error Code: " + e.getErrorCode());
			log.debug("sql: " + sql);
		} finally {
			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (con != null) {
				dbmgr.freeConnection(con);
			}
			log.close();
			
			String path = req.getContextPath();
			resp.sendRedirect( path + "/board/view.jsp?no=" + no);
		}
	}
}

이 서블릿을 web.xml에 등록하고 서블릿 매핑을 /servlet/BoardModifier으로 설정한다.

<servlet>
  <servlet-name>BoardModifier</servlet-name>
  <servlet-class>net.java_school.board.BoardModifier</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>BoardModifier</servlet-name>
  <url-pattern>/servlet/BoardModifier</url-pattern>
</servlet-mapping>

글 삭제

BoardDeleter.java
package net.java_school.board;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

import java.sql.*;

import net.java_school.db.dbpool.*;
import net.java_school.util.*;

public class BoardDeleter extends HttpServlet {

	private static final long serialVersionUID = 664510406708983868L;
	
	OracleConnectionManager dbmgr = null;
	
	@Override
	public void init() throws ServletException {
		ServletContext sc = getServletContext();
		dbmgr = (OracleConnectionManager)sc.getAttribute("dbmgr");
	}
	
	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		doPost(req, resp);
	}
	
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
		req.setCharacterEncoding("UTF-8");
		Log log = new Log();
		
		int no = Integer.parseInt(req.getParameter("no"));
		
		Connection con = dbmgr.getConnection();
		PreparedStatement stmt = null;
		String sql = "delete board where no = ?";
		
		try {
			stmt = con.prepareStatement(sql);
			stmt.setInt(1, no);
			stmt.executeUpdate(); //쿼리 실행
		} catch (SQLException e) {
			log.debug("Error Source: BoardDeleter.java : SQLException");
			log.debug("SQLState: " + e.getSQLState());
			log.debug("Message: " + e.getMessage());
			log.debug("Oracle Error Code: " + e.getErrorCode());
			log.debug("sql: " + sql);
		} finally {
			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (con != null) {
				dbmgr.freeConnection(con);
			}
			log.close();
			String path = req.getContextPath();
			resp.sendRedirect(path + "/board/list.jsp");
		}
	}
}

이 서블릿을 web.xml에 등록하고 서블릿 매핑을 /servlet/BoardDeleter으로 설정한다.

<servlet>
  <servlet-name>BoardDeleter</servlet-name>
  <servlet-class>net.java_school.board.BoardDeleter</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>BoardDeleter</servlet-name>
  <url-pattern>/servlet/BoardDeleter</url-pattern>
</servlet-mapping>

페이지 분할 기능

게시판의 게시글 레코드가 10000개라면 list.jsp안의 while문은 10000번 실행된다.
데이터가 많으면 목록에서 레코드를 모두 보여 주지 않고 레코드를 그룹으로 나누어 보여주는 기능이 필요하다.
이러한 기능을 "페이지 분할 기능"이라고 부르기로 하자.

레코드를 그룹으로 묶기 위한 쿼리

레코드를 10개씩 묶는다면 각 그룹에 해당하는 레코드를 가져오는 오라클 쿼리문이다.

그룹 1 레코드
SELECT no,title,wdate FROM (
	SELECT ROWNUM R, A.* FROM (select no, title, wdate
		FROM board ORDER BY no DESC) A)
WHERE R BETWEEN 1 and 10;
그룹 2 레코드
SELECT no,title,wdate FROM (
	SELECT ROWNUM R, A.* FROM (select no, title, wdate
		FROM board ORDER BY no DESC) A)
WHERE R BETWEEN 11 and 20;
그룹 3 레코드
SELECT no,title,wdate FROM (
	SELECT ROWNUM R, A.* FROM (select no, title, wdate
		FROM board ORDER BY no DESC) A)
WHERE R BETWEEN 21 and 30;

ROWNUM은 오라클의 가상 컬럼으로, 쿼리 결과에서 1부터 시작하여 순차적인 값을 가진다.
ROWNUM을 WHERE 절의 조건에 쓰면 보여줄 그룹에 해당하는 레코드를 추출할 수 있다.
list.jsp를 요청할 때 레코드 그룹 번호를 파라미터로 넘겨준다면 그룹에 해당하는 ROWNUM의 시작 레코드 번호와 마지막 레코드 번호를 구할 수 있다.
list.jsp에 전달할 레코드 그룹 번호에 해당하는 파라미터를 curPage라 정하고 list.jsp를 아래 코드를 참조하여 수정한다.

/board/list.jsp
<%
// .. 중간생략 ..
int curPage = (request.getParameter("curPage") == null) ? 1 : Integer.parseInt(request.getParameter("curPage"));
// 시작 레코드 계산  
int start = (curPage - 1) * 10 + 1;
// 마지막 레코드 계산
int end = start + 10 - 1;

// ... 중간 생략 ...

String sql = "SELECT no,title,wdate FROM (" + 
          	"SELECT ROWNUM R, A.* FROM (SELECT no, title, wdate " +
          	"FROM board ORDER BY no DESC) A) " + 
      	"WHERE R BETWEEN ? AND ?";
      
stmt = con.prepareStatement(sql);
stmt.setInt(1, start);
stmt.setInt(2, end);
rs = pstmt.executeQuery();

// ... 중간 생략 ...
%>

이제 주소창에서 http://localhost:port/board/list.jsp?curPage=1을 요청하면 그룹 1의 레코드를 볼 수 있고, http://localhost:port/board/list.jsp?curPage=2를 요청하면 그룹 2의 레코드를 볼 수 있게 되었다. 하지만, 웹 브라우저의 주소창에서 curPage 파라미터를 고쳐가며 페이지를 이동하는, 그런 게시판은 없다. 게시판은 아래와 같은 페이지를 이동할 수 있는 링크를 제공한다.

<a href="list.jsp?curPage=1">[1]</a>

마지막 페이지 번호를 알아낸다면 1부터 마지막 페이지 번호까지 for 문을 이용해 위와 같은 링크를 만들 수 있다. 마지막 페이지는 어떻게 알 수 있을까? 페이지가 1부터 시작하므로 '마지막 페이지 번호'는 '총 페이지 수'와 같다. 총 페이지 수는 총 레코드 수를 페이지당 레코드 갯수인 10으로 나누면 계산된다. list.jsp의 적당한 위치에 아래 코드를 추가한다.

int totalRecord = 0; //총 레코드 수를 저장할 변수
String sql = "SELECT count(*) FROM board";
stmt = con.prepareStatement(sql);
rs = pstmt.executeQuery();
rs.next();
totalRecord = rs.getInt(1);

list.jsp에 총 레코드 수 구하는 코드 아래, 적당한 위치에 아래를 추가한다.

int totalPage = 0; //총 페이지 수를 저장할 변수

if (totalRecord != 0) {
   if (totalRecord % 10 == 0) {
      totalPage = totalRecord / 10;
   } else {
      totalPage = totalRecord / 10 + 1;
   }
}

이제 총 페이지 수, 즉 마지막 페이지 번호를 구했다. 이 시점에서 ROWNUM의 시작 레코드와 마지막 레코드를 구하는 코드를 좀 더 우아하게 고쳐 보겠다. 페이지 당 레코드 수를 저장하기 위한 변수 numPerPage를 선언하고, 아래 코드를 참조하여 list.jsp의 코드를 수정한다.

int numPerPage = 10; //페이지당 레코드 수
int start = (curPage - 1) * numPerPage + 1; //시작 레코드
int end = start + numPerPage - 1; //마지막 레코드

총 페이지를 구하는 코드 역시 변경해야 한다.

int totalPage = 0;
if (totalRecord != 0) {
  if (totalRecord % numPerPage == 0) {
    totalPage = totalRecord / numPerPage;
  } else {
    totalPage = totalRecord / numPerPage + 1;
  }	
}

페이지 이동 링크를 생성하는 코드를 list.jsp의 가장 하단에 생기도록 추가한다.

<%
for (int i = 1; i <= totalPage; i++) {
%>
   <a href="list.jsp?curPage=<%=i%>">[<%=i%>]</a>
<%
}
%>

페이지 분할 기능 알고리즘을 정리하면 다음과 같다.

  1. 총 레코드 수를 구한다.
  2. 페이지당 보일 레코드 수를 결정하고 총 페이지 수를 구한다.
  3. 첫 번째 레코드 번호와 마지막 레코드 번호를 구하고 레코드를 출력한다.
  4. 페이지 이동 링크를 만든다.

다음은 페이지 분할 기능을 추가한 list.jsp이다.

/board/list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>목록</title>
</head>
<body style="font-size: 11px;">
<h1>목록</h1>
<%
Log log = new Log();

Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;

String sql = null;

//1.총레코드 수를 구한다.
int totalRecord = 0;
try {
	con = dbmgr.getConnection();
	sql = "SELECT count(*) FROM board";
	stmt = con.prepareStatement(sql);
	rs = stmt.executeQuery();
	rs.next();
	totalRecord = rs.getInt(1);
} catch (SQLException e) {
} finally {
	if (rs != null) {
		try {
			rs.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (stmt != null) {
		try {
			stmt.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (con != null) {
		dbmgr.freeConnection(con);
	}	
}

//2.페이지당 보일 레코드 수를 결정하고 총 페이지 수를 구한다.
int numPerPage = 10; //한 페이지에서 보일 레코드 수
int totalPage = 0; //총 페이지수
if (totalRecord != 0) {
	if (totalRecord % numPerPage == 0) {
		totalPage = totalRecord / numPerPage;
	} else {
		totalPage = totalRecord / numPerPage + 1;
	}
}

//3.첫 번째 레코드 번호와 마지막 레코드 번호를 구한다.
int curPage = request.getParameter("curPage") == null ? 1 : Integer.parseInt(request.getParameter("curPage"));

//시작 레코드 계산
int start = (curPage - 1) * numPerPage + 1;
//마지막 레코드 계산
int end = start + numPerPage - 1;
//해당 페이지의 레코드 셋을 구한 후 출력한다.

try {
	con = dbmgr.getConnection();
	sql="SELECT no,title,wdate FROM (" +
	         "SELECT ROWNUM R, A.* FROM (" + 
	          "SELECT no, title, wdate FROM board ORDER BY no DESC) A) " +
	         "WHERE R BETWEEN ? AND ?";
	
	stmt = con.prepareStatement(sql);
	stmt.setInt(1, start);
	stmt.setInt(2, end);
	rs = stmt.executeQuery();

	while (rs.next()) {
		int no = rs.getInt("no");
		String title = rs.getString("title");
		Date wdate = rs.getDate("wdate");
%>
<%=no %> <a href="view.jsp?no=<%=no %>"><%=title %></a> <%= wdate.toString() %><br />
<hr />
<%
  }
} catch(SQLException e) {
	log.debug("Error Source : board/list.jsp : SQLException");
	log.debug("SQLState : " + e.getSQLState());
	log.debug("Message : " + e.getMessage());
	log.debug("Oracle Error Code : " + e.getErrorCode());
	log.debug("sql : " + sql);
} finally {
	if (rs != null) {
		try {
			rs.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (stmt != null) {
		try {
			stmt.close();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	if (con != null) {
		dbmgr.freeConnection(con);
	}
	log.close();
}

//4.각 페이지에 대한  이동 링크를 만든다.
for (int i = 1; i <= totalPage; i++) {
%>
	<a href="list.jsp?curPage=<%=i %>">[<%=i %>]</a>
<%
}
%>
<p>
<a href="write_form.jsp?curPage=<%=curPage %>">글쓰기</a>
</p>
</body>
</html>

목록 페이지를 방문하여 페이지 이동 링크를 클릭하여 테스트한다.

JSP에서 page란 변수를 쓸 수 없는 이유
JSP에는 page란 내재객체Implicit Object가 있다.
JSP가 서블릿으로 변환될 때 다음과 같은 코드가 생성되어 추가된다.
Object page = this;
결론적으로 page란 이름의 변수를 JSP 스크립틀릿에서 사용할 수 없다.

Oracle 11g의 rank() 함수

오라클 11g부터 rank() 함수를 이용할 수 있다.
다음은 scott 계정의 emp 테이블에서 급여 순으로 사원 데이터를 정렬하는 쿼리문이다.

SELECT empno,ename,sal,rank() over (order by sal desc) as rank FROM emp;

이 함수를 이용하면 위의 게시판 목록에 해당하는 쿼리문을 조금 간단하게 줄일 수 있다.
게시판 목록를 구하는 쿼리문을 rank() 함수를 이용하여 바꾸어 보자.

SELECT no,title,wdate 
FROM (
	SELECT rank() over (order by no desc) R,no,title,wdate FROM board
) 
WHERE R BETWEEN 1 and 10;

페이지 이동 링크 수 제한 기능

완벽하게 보이는 페이지 분할 기능에도 문제가 있다.
레코드가 10000개이고 numPerPage가 10이라면 하단의 페이지 이동 링크는 [1] [2] [3] ...... [999] [1000] 식으로 1000개가 생긴다.
1000개의 페이지 이동 링크는 웹 디자이너가 만든 예쁜 디자인을 헤집어 놓는다.
페이지 이동 링크 역시 그룹으로 나누면 해결된다.
이 기능을 '페이지 이동 링크 수 제한 기능'이라고 부르겠다.

페이지를 그룹화하기

페이지 이동 링크 수를 제한하기 위해서는 페이지를 그룹화한다.1
그룹당 페이지 수를 5로 정했다면, 다시 말해 페이지 이동 링크 수를 5개 보이도록 제한한다면 [1] [2] [3] [4] [5] 링크는 1 그룹에 속하고, [6] [7] [8] [9] [10]는 2 그룹에 속한다.
페이지 링크 그룹화 페이지 그룹 단위를 pagePerBlock라고 하고 curPage 페이지가 속한 페이지 그룹 번호를 block이라는 변수에 저장한다면, block은 다음 코드로 구할 수 있다.

curPage가 속한 block 구하기
//페이지 그룹 번호를 저장할 변수 선언과 초기화
int block = 1;

//블록 당 페이지 수를 저장할 변수와 초기화
int pagePerBlock = 5;

if (curPage % pagePerBlock == 0) {
	block = curPage / pagePerBlock;
} else {
	block = curPage / pagePerBlock + 1;
}

현재 페이지(curPage)가 속한 그룹 번호 block을 구했다면 block에 속한 첫 번째 페이지와 마지막 페이지 번호를 다음 코드로 구할 수 있다.

block에 속한 첫 번째 페이지 번호와 마지막 페이지 번호 구하기
//block에 속한 첫 번째 페이지 계산 
int firstPage = (block - 1) * pagePerBlock + 1;

//block에 속한 마지막 페이지 계산
int lastPage =  block * pagePerBlock;

루프 문을 이용해서 첫 번째 페이지부터 마지막 페이지까지 링크를 만든다.

<%
for (int i = firstPage; i <= lastPage; i++) {
%>
   <a href="list.jsp?curPage=<%=i%>">[<%=i%>]</a>
<%
}
%>

위의 코드를 참고하여 기존의 list.jsp를 수정한다.

/board/list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>목록</title>
</head>
<body style="font-size: 11px;">
<h1>목록</h1>
<%
Log log = new Log();

Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;

String sql = null;

//1.총레코드 수를 구한다.
int totalRecord = 0;
try {
    con = dbmgr.getConnection();
    sql = "SELECT count(*) FROM board";
    stmt = con.prepareStatement(sql);
    rs = stmt.executeQuery();
    rs.next();
    totalRecord = rs.getInt(1);
} catch (SQLException e) {
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (stmt != null) {
        try {
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (con != null) {
        dbmgr.freeConnection(con);
    }    
}

//2.페이지당 보일 레코드 수를 결정하고 총 페이지 수를 구한다.
int numPerPage = 10; //한 페이지에서 보일 레코드 수
int totalPage = 0; //총 페이지수
if (totalRecord != 0) {
    if (totalRecord % numPerPage == 0) {
        totalPage = totalRecord / numPerPage;
    } else {
        totalPage = totalRecord / numPerPage + 1;
    }
}

//3.첫 번째 레코드 번호와 마지막 레코드 번호를 구한다.
int curPage = request.getParameter("curPage") == null ? 1 : Integer.parseInt(request.getParameter("curPage"));

//시작 레코드 계산
int start = (curPage - 1) * numPerPage + 1;
//마지막 레코드 계산
int end = start + numPerPage - 1;
//해당 페이지의 레코드 셋을 구한 후 출력한다.

try {
    con = dbmgr.getConnection();
    sql="SELECT no,title,wdate FROM (" +
             "SELECT ROWNUM R, A.* FROM (" + 
              "SELECT no, title, wdate FROM board ORDER BY no DESC) A) " +
             "WHERE R BETWEEN ? AND ?";
    
    stmt = con.prepareStatement(sql);
    stmt.setInt(1, start);
    stmt.setInt(2, end);
    rs = stmt.executeQuery();

    while (rs.next()) {
        int no = rs.getInt("no");
        String title = rs.getString("title");
        Date wdate = rs.getDate("wdate");
%>
<%=no %> <a href="view.jsp?no=<%=no %>"><%=title %></a> <%= wdate.toString() %><br />
<hr />
<%
  }
} catch(SQLException e) {
    log.debug("Error Source : board/list.jsp : SQLException");
    log.debug("SQLState : " + e.getSQLState());
    log.debug("Message : " + e.getMessage());
    log.debug("Oracle Error Code : " + e.getErrorCode());
    log.debug("sql : " + sql);
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (stmt != null) {
        try {
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (con != null) {
        dbmgr.freeConnection(con);
    }
    log.close();
}

//4.각 페이지에 대한  이동 링크를 만든다.

//페이지 그룹 번호를 저장할 변수 선언과 초기화
int block = 1;

//블록 당 페이지 수를 저장할 변수와 초기화
int pagePerBlock = 5;

if (curPage % pagePerBlock == 0) {
    block = curPage / pagePerBlock;
} else {
    block = curPage / pagePerBlock + 1;
}

//block에 속한 첫 번째 페이지 계산 
int firstPage = (block - 1) * pagePerBlock + 1;

//block에 속한 마지막 페이지 계산
int lastPage =  block * pagePerBlock;

for (int i = firstPage; i <= lastPage; i++) {
%>
   <a href="list.jsp?curPage=<%=i%>">[<%=i%>]</a>
<%
}
%>
<p>
<a href="write_form.jsp?curPage=<%=curPage %>">글쓰기</a>
</p>
</body>
</html>

페이지 링크 그룹화 현재 페이지가 속한 페이지 그룹(block)을 구해서 해당 페이지 그룹에 속한 페이지만 보여주는 데 성공했다.
하지만 다른 페이지 그룹으로 이동할 수 있는 방법이 없다.
인접한 페이지 그룹으로 이동할 수 있도록 링크는 다음 방법으로 생성할 수 있다.
block이 1보다 크면 [이전] 링크를 생성하고 firstPage - 1인 페이지로 링크하고, block이 총 블록 수(마지막 블록 번호)보다 작다면 [다음] 링크를 생성하고 lastPage + 1인 페이지로 링크한다.
이렇게 [이전] [다음] 링크를 사용하면 인접한 페이지 그룹으로 이동할 수 있다.
[다음] 링크를 만들기 위해선 총 블록 수, 즉 마지막 블록을 구하는 코드가 추가로 필요하다.

총 블록 수 구하기 (마지막 블록 번호 구하기)
//총 블록 수를 저장할 변수 선언과 초기화 
int totalBlock = 0;

if (totalPage > 0) { 
	if (totalPage % pagePerBlock == 0) {
		totalBlock = totalPage / pagePerBlock;
	} else {
		totalBlock = totalPage / pagePerBlock + 1;
	}
}

[이전]과 [다음] 링크를 아래 코드로 생성할 수 있다.

[이전] 링크 생성 코드
<%
//현재 block > 1이면 [이전] 링크를 만들고 firstPage - 1 페이지로 링크
int prevPage = 0;
if(block > 1) {
	prevPage = firstPage - 1;
%>
	<a href="list.jsp?curPage=<%=prevPage %>">[이전]</a>
<%
}
%>
[다음] 링크 생성 코드
<%
//block < totalBlock이면 [다음] 링크를 만들고  lastPage + 1 페이지로 링크  
if(block < totalBlock) {
	int nextPage = lastPage + 1;
%>
	<a href="list.jsp?curPage=<%=nextPage %>">[다음]</a>
<%
}
%>

위 설명대로 list.jsp를 수정한다.

/board/list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>목록</title>
</head>
<body style="font-size: 11px;">
<h1>목록</h1>
<%
Log log = new Log();

Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;

String sql = null;

//1.총레코드 수를 구한다.
int totalRecord = 0;
try {
    con = dbmgr.getConnection();
    sql = "SELECT count(*) FROM board";
    stmt = con.prepareStatement(sql);
    rs = stmt.executeQuery();
    rs.next();
    totalRecord = rs.getInt(1);
} catch (SQLException e) {
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (stmt != null) {
        try {
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (con != null) {
        dbmgr.freeConnection(con);
    }    
}

//2.페이지당 보일 레코드 수를 결정하고 총 페이지 수를 구한다.
int numPerPage = 10; //한 페이지에서 보일 레코드 수
int totalPage = 0; //총 페이지수
if (totalRecord != 0) {
    if (totalRecord % numPerPage == 0) {
        totalPage = totalRecord / numPerPage;
    } else {
        totalPage = totalRecord / numPerPage + 1;
    }
}

//3.첫 번째 레코드 번호와 마지막 레코드 번호를 구한다.
int curPage = request.getParameter("curPage") == null ? 1 : Integer.parseInt(request.getParameter("curPage"));

//시작 레코드 계산
int start = (curPage - 1) * numPerPage + 1;
//마지막 레코드 계산
int end = start + numPerPage - 1;
//해당 페이지의 레코드 셋을 구한 후 출력한다.

try {
    con = dbmgr.getConnection();
    sql="SELECT no,title,wdate FROM (" +
             "SELECT ROWNUM R, A.* FROM (" + 
              "SELECT no, title, wdate FROM board ORDER BY no DESC) A) " +
             "WHERE R BETWEEN ? AND ?";
    
    stmt = con.prepareStatement(sql);
    stmt.setInt(1, start);
    stmt.setInt(2, end);
    rs = stmt.executeQuery();

    while (rs.next()) {
        int no = rs.getInt("no");
        String title = rs.getString("title");
        Date wdate = rs.getDate("wdate");
%>
<%=no %> <a href="view.jsp?no=<%=no %>"><%=title %></a> <%= wdate.toString() %><br />
<hr />
<%
  }
} catch(SQLException e) {
    log.debug("Error Source : board/list.jsp : SQLException");
    log.debug("SQLState : " + e.getSQLState());
    log.debug("Message : " + e.getMessage());
    log.debug("Oracle Error Code : " + e.getErrorCode());
    log.debug("sql : " + sql);
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (stmt != null) {
        try {
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (con != null) {
        dbmgr.freeConnection(con);
    }
    log.close();
}

//4.각 페이지에 대한  이동 링크를 만든다.

//페이지 그룹 번호를 저장할 변수 선언과 초기화
int block = 1;

//블록 당 페이지 수를 저장할 변수와 초기화
int pagePerBlock = 5;

if (curPage % pagePerBlock == 0) {
    block = curPage / pagePerBlock;
} else {
    block = curPage / pagePerBlock + 1;
}

//block에 속한 첫 번째 페이지 계산 
int firstPage = (block - 1) * pagePerBlock + 1;

//block에 속한 마지막 페이지 계산
int lastPage =  block * pagePerBlock;

//총 블록 수를 저장할 변수 선언과 초기화 
int totalBlock = 0;

if (totalPage > 0) { 
  if (totalPage % pagePerBlock == 0) {
      totalBlock = totalPage / pagePerBlock;
  } else {
      totalBlock = totalPage / pagePerBlock + 1;
  }
}

//현재 block > 1이면 [이전] 링크를 만들고 firstPage - 1 페이지로 링크
int prevPage = 0;
if(block > 1) {
  prevPage = firstPage - 1;
%>
  <a href="list.jsp?curPage=<%=prevPage %>">[이전]</a>
<%
}

for (int i = firstPage; i <= lastPage; i++) {
%>
   <a href="list.jsp?curPage=<%=i%>">[<%=i%>]</a>
<%
}

//block < totalBlock이면 [다음] 링크를 만들고  lastPage + 1 페이지로 링크  
if(block < totalBlock) {
    int nextPage = lastPage + 1;
%>
    <a href="list.jsp?curPage=<%=nextPage %>">[다음]</a>
<%
}
%>
<p>
<a href="write_form.jsp?curPage=<%=curPage %>">글쓰기</a>
</p>
</body>
</html>

다음은 list.jsp를 수정한 후 테스트한 화면이다.
인접한 페이지 그룹으로 이동할 수 있는 [이전], [다음] 링크를 확인할 수 있다.
페이지 그룹화 페이지 그룹화

이상이 없어 보이지만 여전히 버그가 있다.
총 레코드가 101개가 되도록 레코드를 추가한 후, [다음] 링크를 이용하여 마지막 블록으로 이동한다.
페이지 그룹화 마지막 블록에 불필요한 페이지 링크([12] [13] [14] [15])가 생성되었다.
레코드가 101개일 때 numPerPage가 10이면 totalPage는 11이다.
그리고 pagePerBlock이 5라면 totalBlock은 3으로 계산된다.
블록 3에 속하는 마지막 페이지 번호는 15로 계산되어 필요없는 [12] [13] [14] [15] 링크가 만들어지게 된다.
불필요한 페이지 링크가 생성되는 않게 하려면, 마지막 블록에서 마지막 페이지 번호를 총 페이지 수(마지막 페이지 번호)로 설정해야 한다.
아래 코드를 페이지 이동 링크를 출력하는 for문 앞에 추가한다.

if (block >= totalBlock) {
	lastPage = totalPage;
}
/board/list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>목록</title>
</head>
<body style="font-size: 11px;">
<h1>목록</h1>
<%
Log log = new Log();

Connection con = null;
PreparedStatement stmt = null;
ResultSet rs = null;

String sql = null;

//1.총레코드 수를 구한다.
int totalRecord = 0;
try {
    con = dbmgr.getConnection();
    sql = "SELECT count(*) FROM board";
    stmt = con.prepareStatement(sql);
    rs = stmt.executeQuery();
    rs.next();
    totalRecord = rs.getInt(1);
} catch (SQLException e) {
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (stmt != null) {
        try {
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (con != null) {
        dbmgr.freeConnection(con);
    }    
}

//2.페이지당 보일 레코드 수를 결정하고 총 페이지 수를 구한다.
int numPerPage = 10; //한 페이지에서 보일 레코드 수
int totalPage = 0; //총 페이지수
if (totalRecord != 0) {
    if (totalRecord % numPerPage == 0) {
        totalPage = totalRecord / numPerPage;
    } else {
        totalPage = totalRecord / numPerPage + 1;
    }
}

//3.첫 번째 레코드 번호와 마지막 레코드 번호를 구한다.
int curPage = request.getParameter("curPage") == null ? 1 : Integer.parseInt(request.getParameter("curPage"));

//시작 레코드 계산
int start = (curPage - 1) * numPerPage + 1;
//마지막 레코드 계산
int end = start + numPerPage - 1;
//해당 페이지의 레코드 셋을 구한 후 출력한다.

try {
    con = dbmgr.getConnection();
    sql="SELECT no,title,wdate FROM (" +
             "SELECT ROWNUM R, A.* FROM (" + 
              "SELECT no, title, wdate FROM board ORDER BY no DESC) A) " +
             "WHERE R BETWEEN ? AND ?";
    
    stmt = con.prepareStatement(sql);
    stmt.setInt(1, start);
    stmt.setInt(2, end);
    rs = stmt.executeQuery();

    while (rs.next()) {
        int no = rs.getInt("no");
        String title = rs.getString("title");
        Date wdate = rs.getDate("wdate");
%>
<%=no %> <a href="view.jsp?no=<%=no %>"><%=title %></a> <%= wdate.toString() %><br />
<hr />
<%
  }
} catch(SQLException e) {
    log.debug("Error Source : board/list.jsp : SQLException");
    log.debug("SQLState : " + e.getSQLState());
    log.debug("Message : " + e.getMessage());
    log.debug("Oracle Error Code : " + e.getErrorCode());
    log.debug("sql : " + sql);
} finally {
    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (stmt != null) {
        try {
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    if (con != null) {
        dbmgr.freeConnection(con);
    }
    log.close();
}

//4.각 페이지에 대한  이동 링크를 만든다.

//페이지 그룹 번호를 저장할 변수 선언과 초기화
int block = 1;

//블록 당 페이지 수를 저장할 변수와 초기화
int pagePerBlock = 5;

if (curPage % pagePerBlock == 0) {
    block = curPage / pagePerBlock;
} else {
    block = curPage / pagePerBlock + 1;
}

//block에 속한 첫 번째 페이지 계산 
int firstPage = (block - 1) * pagePerBlock + 1;

//총 블록 수를 저장할 변수 선언과 초기화 
int totalBlock = 0;

if (totalPage > 0) { 
  if (totalPage % pagePerBlock == 0) {
      totalBlock = totalPage / pagePerBlock;
  } else {
      totalBlock = totalPage / pagePerBlock + 1;
  }
}

//block에 속한 마지막 페이지 계산
int lastPage =  block * pagePerBlock;
if (block >= totalBlock) {
  lastPage = totalPage;
}

//현재 block > 1이면 [이전] 링크를 만들고 firstPage - 1 페이지로 링크
int prevPage = 0;
if(block > 1) {
  prevPage = firstPage - 1;
%>
  <a href="list.jsp?curPage=<%=prevPage %>">[이전]</a>
<%
}

for (int i = firstPage; i <= lastPage; i++) {
%>
   <a href="list.jsp?curPage=<%=i%>">[<%=i%>]</a>
<%
}

//block < totalBlock이면 [다음] 링크를 만들고  lastPage + 1 페이지로 링크  
if(block < totalBlock) {
    int nextPage = lastPage + 1;
%>
    <a href="list.jsp?curPage=<%=nextPage %>">[다음]</a>
<%
}
%>
<p>
<a href="write_form.jsp?curPage=<%=curPage %>">글쓰기</a>
</p>
</body>
</html>

최종 페이지 분할 기능 알고리즘

  1. 블록 당 페이지 이동 링크 수를 정한다.
  2. 총 블록 수를 계산한다.
  3. 사용자가 요청한 페이지(현재 페이지)가 속한 블록을 계산한다.
  4. 현재 페이지가 속한 블록에서 링크할 첫 번째 페이지와 마지막 페이지를 계산한다.
  5. 현재 페이지가 속한 블록에서 불필요한 페이지를 제거한다.
  6. block > 1이면 firstPage - 1로 이동하는 [이전] 링크를 만든다.
  7. 루프 문을 사용해여 첫 번째 페이지부터 마지막 페이지까지 링크를 만든다.
  8. block < totalBlcok이면 lastPage + 1로 이동하는 [다음] 링크를 만든다.

페이지 분할 기능 추가에 따른 게시판 컴포넌트 수정

list.jsp?curPage=5에서 view.jsp을 방문한 후 view.jsp에서 목록 링크를 클릭하면 list.jsp로 방문하게 된다.
즉, 5페이지에서 상세보기를 보고 다시 목록으로 돌아오는데 1페이지로 돌아온다.
5페이지에서 상세보기로 이동하고, 상세보기에서 목록 링크를 클릭하면 다시 5페이지로 돌아가야 한다.
그렇게 하려면 '새 글쓰기를 처리하는 서블릿'을 제외한 게시판과 관련된 모든 컴포넌트에 curPage 파라미터를 전달하고, curPage를 전달받은 컴포넌트는 다른 컴포넌트로 이동하기 위한 링크의 쿼리 스트링에 이 파라미터를 추가해야 한다.
/board/list.jsp 파일을 열고 아래를 참고하여 다음과 같이 링크의 쿼리 스트링을 수정한다.

list.jsp에서 view.jsp로 이동할 때 curPage 파라미터 전달
<a href="view.jsp?no=<%=no %>&curPage=<%=curPage %>"><%=title %></a> <%= wdate.toString() %>
list.jsp에서 write_form.jsp로 이동할 때 curPage 파라미터 전달
<a href="write_form.jsp?curPage=<%=curPage %>">글쓰기</a>

/board/view.jsp 파일을 열고 다른 컴포넌트로의 이동 링크의 쿼리 스트링을 수정한다.

/board/view.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<%
String curPage = request.getParameter("curPage");
%>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>상세보기</title>
<script type="text/javascript">
function goModify(no, curPage) {
	location.href="modify_form.jsp?no=" + no + "&curPage=" + curPage;
}

function goDelete(no, curPage) {
	var check = confirm('정말로 삭제하시겠습니까?');
	if (check) {
		location.href="../servlet/BoardDeleter?no=" + no + "&curPage=" + curPage;
	}
}
</script>
</head>
<body>
<h1>상세보기</h1>

<!-- ..중간 생략 .. -->

<a href="list.jsp?curPage=<%=curPage %>">목록</a>
<input type="button" value="수정" onclick="javascript:goModify('<%=no %>', '<%=curPage %>')">
<input type="button" value="삭제" onclick="javascript:goDelete('<%=no %>', '<%=curPage %>')">
</body>
</html>

/board/write_form.jsp 파일을 열고 목록으로 돌아가는 부분의 코드를 수정한다.
폼 액션 속성값을 ../servlet/BoardWriter?curPage=<%=curPage %>로 고쳐선 안 된다.
왜 그럴까? 새 글은 언제나 목록 첫 페이지로 이동해야 확인할 수 있기 때문이다.
5 페이지에서 새 글을 등록했는데 다시 5페이지로 돌아간다면 자신이 작성한 새 글을 확인할 수 없다.
따라서 새 글을 등록하면 목록 첫 페이지로 이동해야 한다.

/board/write_form.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
String curPage = request.getParameter("curPage");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>글쓰기</title>
</head>
<body>
<!-- 본문 시작 -->
<h1>글쓰기</h1>
<form action="../servlet/BoardWriter" method="post">
<table>
<tr>
	<td>제목</td>
	<td><input type="text" name="title" size="50"></td>
</tr>
<tr>
	<td colspan="2">
		<textarea name="content" rows="20" cols="100"></textarea>
	</td>
</tr>
<tr>
	<td colspan="2">
		<input type="submit" value="전송">
		<input type="reset" value="취소">
		<a href="list.jsp?curPage=<%=curPage %>">목록</a>
	</td>
</tr>
</table>
</form>  
<!-- 본문 끝 -->
</body>
</html>
/board/modify_form.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<%
int no = Integer.parseInt(request.getParameter("no"));
String curPage = request.getParameter("curPage");

Log log = new Log();


//.. 중간 생략 ..

%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>수정</title>
</head>
<body>
<!-- 본문 시작 -->
<h1>수정</h1>
<form action="../servlet/BoardModifier" method="post">
<input type="hidden" name="no" value="<%=no %>">
<input type="hidden" name="curPage" value="<%=curPage %>">
<table>
<tr>
	<td>제목</td>
	<td><input type="text" name="title" size="50" value="<%=title %>" /></td>
</tr>
<tr>
	<td colspan="2">
		<textarea name="content" rows="30" cols="100"><%=content %></textarea>
	</td>
</tr>
<tr>
	<td colspan="2">
		<input type="submit" value="전송">
		<input type="reset" value="취소">
		<a href="view.jsp?no=<%=no %>&curPage=<%=curPage %>">상세보기</a>
	</td>
</tr>
</table>
</form>
<!-- 본문 끝 -->
</body>
</html>
BoardModifier 서블릿 수정
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
		throws ServletException, IOException {

	req.setCharacterEncoding("UTF-8");
	Log log = new Log();
	
	int no = Integer.parseInt(req.getParameter("no"));
	String curPage = req.getParameter("curPage");
	
	//..중간 생략 ..
				
	String path = req.getContextPath();
	resp.sendRedirect(path + "/board/view.jsp?no=" + no + "&curPage=" + curPage);
	
}
BoardDeleter 서블릿 수정
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
		throws ServletException, IOException {
	
	req.setCharacterEncoding("UTF-8");
	Log log = new Log();
	
	int no = Integer.parseInt(req.getParameter("no"));
	String curPage = req.getParameter("curPage");
		
	//..중간 생략 ..
	
	String path = req.getContextPath();
	resp.sendRedirect(path + "/board/list.jsp?curPage=" + curPage);
	
}

페이징 처리는 웹 프로그래머라면 반드시 정복해야 한다.
구현과 충분한 테스트를 통해서 페이징에 대한 완벽한 이해를 해야 한다.

검색 기능

게시글이 많아지면 필요해지는 기능이 검색 기능이다.
다음 코드를 list.jsp의 가장 아래에 추가한다.

<form action="list.jsp" method="post">
	<input type="text" size="10" maxlength="30" name="keyword" />
	<input type="submit" value="Search" />
</form>

list.jsp에 curPage외에 keyword 파라미터도 함께 전달해야 한다.
전달받은 keyword 파라미터가 널인 경우 "" 문자로 바꾸어 주면, 나중에 널 체크를 하지 않아도 되어 편리하다.
다음 코드를 list.jsp의 적당한 위치에 추가한다.

request.setCharacterEncoding("UTF-8");
String keyword = request.getParameter("keyword");
if (keyword == null) keyword = "";

request.setCharacterEncoding("UTF-8"); 코드는 한글 검색을 위해 필요하다.
이 코드는 getParameter() 메서드가 실행되기 전에 실행되야 한다.
검색 기능이 추가되면 검색 조건에 따라서 총 레코드 수가 변화하기 때문에 list.jsp의 총 레코드 수 구하는 부분을 수정한다.

if (keyword.equals("")) {
	sql = "SELECT count(*) FROM board";
} else {
	sql = "SELECT count(*) FROM board " +
		"WHERE title LIKE '%" + keyword + "%' " + 
		"OR content LIKE '%" + keyword + "%'";
}

해당 페이지의 레코드을 가져오는 쿼리를 수정한다.

if (keyword.equals("")) {
	sql = "SELECT no,title,wdate " + 
		"FROM (SELECT ROWNUM R, A.* FROM (" +
		"SELECT no,title,wdate FROM board ORDER BY no DESC) A) " +
		"WHERE R BETWEEN ? AND ?";
} else {
	sql = "SELECT no,title,wdate " +
		"FROM (SELECT ROWNUM R, A.* FROM (" +
		"SELECT no,title,wdate FROM board " +
		"WHERE title LIKE '%" + keyword + "%' OR content LIKE '%" + keyword + "%' " +
		"ORDER BY no DESC) A) " +
		"WHERE R BETWEEN ? AND ?";
}

검색결과의 목록 페이지에서 하단의 5페이지 이동 링크를 클릭했을 때, 검색을 하지 않은 목록 5페이지가 보이면 안 된다.
list.jsp 파일을 열고 list.jsp로의 링크에 keyword 파라미터가 전달되도록 수정한다.

<a href="list.jsp?curPage=<%=prevPage %>&keyword=<%=keyword %>">[이전]</a>
<a href="list.jsp?curPage=<%=i %>&keyword=<%=keyword %>">[<%=i %>]</a>
<a href="list.jsp?curPage=<%=nextPage %>&keyword=<%=keyword %>">[다음]</a>

list.jsp를 방문한 후 검색을 테스트한다.
검색 후 목록에서 상세보기로 이동한 후 다시 목록으로 돌아오면 검색된 목록으로 돌아오지 못한다.
view.jsp 요청할 때 curPage 외에 keyword란 파라미터를 전달해야 다시 검색 목록으로 돌아갈 수 있다.
list.jsp 파일을 열고 아래를 참조하여 상세보기와 글쓰기에 대한 링크를 수정한다.

<a href="view.jsp?no=<%=no %>&curPage=<%=curPage %>&keyword=<%=keyword %>"><%=title %></a> <%= wdate.toString() %>
<a href="write_form.jsp?curPage=<%=curPage %>&keyword=<%=keyword %>">글쓰기</a>

view.jsp에는 curPage와 keyword 파라미터를 수신하고 list.jsp로 돌아갈 때는 이 파라미터를 이용하도록 코드를 수정한다.
글쓰기를 제외한 게시판의 다른 컴포넌트 역시 keyword 파라미터를 수신하고 코드에서 다른 컴포넌트로의 링크를 수정한다.

/board/view.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<%
request.setCharacterEncoding("UTF-8");
int no = Integer.parseInt(request.getParameter("no"));
String curPage = request.getParameter("curPage");
String keyword = request.getParameter("keyword");
%>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>상세보기</title>
<script type="text/javascript">
function goModify(no, curPage, keyword) {
	location.href="modify_form.jsp?no=" + no + "&curPage=" + curPage + "&keyword=" + keyword;
}

function goDelete(no, curPage, keyword) {
	var check = confirm('정말로 삭제하시겠습니까?');
	if (check) {
		location.href="../servlet/BoardDeleter?no=" + no + "&curPage=" + curPage + "&keyword=" + keyword;
	}
}
</script>
</head>
<body>
<h1>상세보기</h1>

<%
//..중간 생략 ..
%>

<a href="list.jsp?curPage=<%=curPage %>&keyword=<%=keyword %>">목록</a>
<input type="button" value="수정" onclick="javascript:goModify('<%=no %>','<%=curPage %>','<%=keyword %>')">
<input type="button" value="삭제" onclick="javascript:goDelete('<%=no %>','<%=curPage %>','<%=keyword %>')">
</body>
</html>

아래 컴포넌트도 위와 같은 이유로 수정한다.
이때 BoardWriter.java는 수정할 필요가 없다.

/board/write_form.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
request.setCharacterEncoding("UTF-8");
String curPage = request.getParameter("curPage");
String keyword = request.getParameter("keyword");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>글쓰기</title>
</head>
<body>
<!-- 본문 시작 -->
<h1>글쓰기</h1>
<form action="../servlet/BoardWriter" method="post">
<table>
<tr>
	<td>제목</td>
	<td><input type="text" name="title" size="50"></td>
</tr>
<tr>
	<td colspan="2">
		<textarea name="content" rows="20" cols="100"></textarea>
	</td>
</tr>
<tr>
	<td colspan="2">
		<input type="submit" value="전송">
		<input type="reset" value="취소">
		<a href="list.jsp?curPage=<%=curPage %>&keyword=<%=keyword %>">목록</a>
	</td>
</tr>
</table>
</form>  
<!-- 본문 끝 -->
</body>
</html>
/board/modify_form.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ page import="net.java_school.util.*" %>
<%@ page import="net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" scope="application" class="net.java_school.db.dbpool.OracleConnectionManager" />
<%
request.setCharacterEncoding("UTF-8");
String no = request.getParameter("no");
String curPage = request.getParameter("curPage");
String keyword = request.getParameter("keyword");
Log log = new Log();

//.. 중간 생략 ..

%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>수정</title>
</head>
<body>
<!-- 본문 시작 -->
<h1>수정</h1>
<form action="../servlet/BoardModifier" method="post">
<input type="hidden" name="no" value="<%=no %>">
<input type="hidden" name="curPage" value="<%=curPage %>">
<input type="hidden" name="keyword" value="<%=keyword %>">
<table>
<tr>
	<td>제목</td>
	<td><input type="text" name="title" size="50" value="<%=title %>" /></td>
</tr>
<tr>
	<td colspan="2">
		<textarea name="content" rows="30" cols="100"><%=content %></textarea>
	</td>
</tr>
<tr>
	<td colspan="2">
		<input type="submit" value="전송">
		<input type="reset" value="취소">
		<a href="view.jsp?no=<%=no %>&curPage=<%=curPage %>&keyword=<%=keyword %>">상세보기</a>
	</td>
</tr>
</table>
</form>
<!-- 본문 끝 -->
</body>
</html>
BoardModifier 서블릿 수정
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
		throws ServletException, IOException {

	req.setCharacterEncoding("UTF-8");
	Log log = new Log();
	
	int no = Integer.parseInt(req.getParameter("no"));
	String curPage = req.getParameter("curPage");
	String keyword = req.getParameter("keyword");
	
	//..중간 생략 ..
				
	String path = req.getContextPath();
	keyword = java.net.URLEncoder.encode(keyword,"UTF-8");
	resp.sendRedirect(path + "/board/view.jsp?no=" + no + "&curPage=" + curPage + "&keyword=" + keyword);
	
}
BoardDeleter 서블릿 수정
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp)
		throws ServletException, IOException {
	
	req.setCharacterEncoding("UTF-8");
	Log log = new Log();
	
	int no = Integer.parseInt(req.getParameter("no"));
	String curPage = req.getParameter("curPage");
	String keyword = req.getParameter("keyword");
		
	//..중간 생략 ..
	
	String path = req.getContextPath();
	keyword = java.net.URLEncoder.encode(keyword,"UTF-8");
	resp.sendRedirect(path + "/board/list.jsp?curPage=" + curPage + "&keyword=" + keyword);

}

keyword = java.net.URLEncoder.encode(keyword,"UTF-8");

keyword = java.net.URLEncoder.encode(keyword,"UTF-8");코드가 필요한 이유는 HttpServletResponse 의 sendRedirect() 메서드의 인자가 자바 문자열인데 자바 문자열과 URL 주소 문자의 인코딩이 서로 다르기 때문이다.
URLEncoder의 encode() 메서드는 한글과 같은 문자에 대한 바이트 값을 얻을 수 있게 한다.
이 코드가 우리가 원하는 방식으로 동작하려면 서버 설정을 건드려야 한다.
{톰캣홈}/conf/server.xml 파일을 열고 Connector 엘리먼트 중 port 속성값이 8080인(우리는 8989로 바꿔야 했다)
Connector 엘리먼트에 URIEncoding 속성을 다음과 같이 UTF-8인지를 확인한다.
URIEncoding 속성이 없다면 아래처럼 추가한다.

<Connector port="8080" protocol="HTTP/1.1" 
	connectionTimeout="20000" 
	URIEncoding="UTF-8"
	redirectPort="8443" />

톰캣은 쿼리 스트링을 포함한 URL의 디폴트 캐릭터 인코딩으로 ISO-8859-1을 사용한다.
즉, GET 파라미터의 인코딩은 ISO-8859-1을 사용한다.
URIEncoding="UTF-8" 설정은 URL에 대한 캐릭터 인코딩을 UTF-8로 변경한다.
이렇게 설정하면 값이 한글인 파라미터를 GET 방식으로 깨지지 않게 전송할 수 있다.
톰캣을 재실행한다.
아래 파일을 ROOT 애플리케이션의 도큐먼트베이스에 생성한다.

/sender.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
	"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<%
String name = "홍길동";
%>
<form id="test" action="taker.jsp?name=<%=name %>" method="post">
	<input type="hidden" name="nickname" value="의적" />
	<input type="submit" />
</form>
</body>
</html>
/taker.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
	"http://www.w3.org/TR/html4/loose.dtd">
<%
request.setCharacterEncoding("UTF-8");
String name = request.getParameter("name");
String nickname = request.getParameter("nickname");
%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Insert title here</title>
</head>
<body>
<%=name %><br />
<%=nickname %>
</body>
</html>

http://localhost:port/sender.jsp를 방문하고 서밋 버튼을 클릭한다.
이동한 taker.jsp에서 한글이 깨지지 않고 모두 출력되는지 확인한다.
list.jsp에서 검색폼의 method 속성을 method="post" 에서 method="get"으로 변경한 후 한글 검색이 되는지 확인한다.

주석
  1. "페이지 분할 기능"에서 그룹화의 대상은 레코드이고, 레코드 그룹 번호를 저장하는 변수는 curPage이다.
참고