학습 목표
- PageInfo 클래스의 구조와 역할 이해
- 페이지네이션 계산 로직의 수학적 원리 파악
- Oracle ROWNUM을 활용한 페이징 쿼리 작성 방법 학습
- MVC 패턴에서 페이지네이션 데이터 전달 과정 이해
- JSP에서 JSTL을 활용한 페이지네이션 UI 구현 방법 학습
<PageInfo 클래스 개요>
- 페이지네이션 정보 통합 관리
PageInfo 클래스는 웹 애플리케이션에서 페이지네이션에 필요한 모든 정보를 하나의 객체로 통합 관리하는 VO 클래스다. 개별 변수들을 분산 관리하는 대신, 관련된 정보들을 캡슐화하여 코드의 가독성과 유지보수성을 향상시킨다.
@Getter
@Setter
@ToString
public class PageInfo {
private int currentPage; // 현재 페이지
private int listCount; // 총 게시글 수
private int pageLimit; // 페이지 버튼 개수
private int boardLimit; // 한 페이지당 게시글 수
private int maxPage; // 최대 페이지 수
private int startPage; // 시작 페이지 번호
private int endPage; // 끝 페이지 번호
}
- 생성자를 통한 자동 계산
PageInfo 객체 생성 시 생성자에서 모든 페이지네이션 정보를 자동으로 계산한다. 이는 개발자가 매번 복잡한 계산 로직을 작성할 필요 없이 간단하게 페이지네이션 정보를 얻을 수 있게 해준다.
public PageInfo(int currentPage, int listCount, int pageLimit, int boardLimit) {
this.currentPage = currentPage;
this.listCount = listCount;
this.pageLimit = pageLimit;
this.boardLimit = boardLimit;
// 자동 계산 로직
this.maxPage = (int)Math.ceil((double)this.listCount/this.boardLimit);
this.startPage = ((this.currentPage - 1) / this.pageLimit) * this.pageLimit + 1;
this.endPage = this.startPage + this.pageLimit - 1;
this.endPage = this.endPage > this.maxPage ? this.maxPage : this.endPage;
}
<페이지네이션 계산 로직>
- 최대 페이지 수 계산
총 게시글 수를 한 페이지당 게시글 수로 나누어 올림 처리한다. 이는 마지막 페이지에 일부 게시글만 있어도 별도의 페이지로 처리하기 위함이다.
// Math.ceil을 사용한 올림 처리
this.maxPage = (int)Math.ceil((double)this.listCount/this.boardLimit);
// 예시
// 100개 게시글, 10개씩 표시 → 10페이지
// 107개 게시글, 10개씩 표시 → 11페이지 (마지막 페이지에 7개)
실행 결과:
총 게시글: 107개, 페이지당 게시글: 10개
최대 페이지: 11페이지
- 시작 페이지 번호 계산
현재 페이지를 기준으로 페이지 버튼 그룹의 시작 번호를 계산한다. 이는 페이지 버튼을 일정한 개수씩 그룹화하여 표시하기 위함이다.
// 시작 페이지 계산 공식
this.startPage = ((this.currentPage - 1) / this.pageLimit) * this.pageLimit + 1;
// 예시 (pageLimit = 5일 때)
// currentPage: 1 → startPage: 1
// currentPage: 7 → startPage: 6
// currentPage: 11 → startPage: 11
실행 결과:
현재 페이지: 7, 페이지 버튼 개수: 5
시작 페이지: 6 (6, 7, 8, 9, 10 버튼 표시)
- 끝 페이지 번호 계산
시작 페이지에 페이지 버튼 개수를 더하고, 최대 페이지를 초과하지 않도록 제한한다.
// 끝 페이지 계산
this.endPage = this.startPage + this.pageLimit - 1;
this.endPage = this.endPage > this.maxPage ? this.maxPage : this.endPage;
// 예시
// startPage: 21, pageLimit: 10, maxPage: 25
// endPage: 25 (21~25 버튼만 표시)
<Controller에서 PageInfo 활용>
- ListController 구현
ListController에서는 사용자 요청 파라미터를 받아 PageInfo 객체를 생성하고, 이를 통해 필요한 데이터를 조회한다.
@WebServlet("/list.bo")
public class ListController extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 요청 파라미터 처리
int currentPage = request.getParameter("cpage") != null ?
Integer.parseInt(request.getParameter("cpage")) : 1;
// 2. 총 게시글 수 조회
int listCount = new BoardService().selectAllBoardCount();
// 3. 페이지네이션 설정
int pageLimit = 5; // 페이지 버튼 5개씩 표시
int boardLimit = 5; // 한 페이지당 게시글 5개
// 4. PageInfo 객체 생성 (자동 계산)
PageInfo pi = new PageInfo(currentPage, listCount, pageLimit, boardLimit);
// 5. 해당 페이지 게시글 조회
ArrayList<Board> list = new BoardService().selectAllBoard(pi);
// 6. JSP로 데이터 전달
request.setAttribute("list", list);
request.setAttribute("pi", pi);
request.getRequestDispatcher("views/board/listView.jsp").forward(request, response);
}
}
- 파라미터 처리 및 기본값 설정
사용자가 페이지 번호를 지정하지 않은 경우 기본값으로 1페이지를 설정한다. 이는 첫 방문 시에도 정상적으로 목록이 표시되도록 보장한다.
// 삼항 연산자를 활용한 안전한 파라미터 처리
int currentPage = request.getParameter("cpage") != null ?
Integer.parseInt(request.getParameter("cpage")) : 1;
<Service와 DAO에서 PageInfo 활용>
- BoardService 구현
BoardService에서는 PageInfo 객체를 받아 해당 페이지의 게시글을 조회한다. 이는 비즈니스 로직과 데이터 접근 로직을 분리하는 MVC 패턴의 핵심이다.
public class BoardService {
// 총 게시글 수 조회
public int selectAllBoardCount() {
Connection conn = getConnection();
int listCount = new BoardDao().selectAllBoardCount(conn);
close(conn);
return listCount;
}
// PageInfo를 활용한 게시글 목록 조회
public ArrayList<Board> selectAllBoard(PageInfo pi) {
Connection conn = getConnection();
ArrayList<Board> list = new BoardDao().selectAllBoard(conn, pi);
close(conn);
return list;
}
}
- BoardDao에서 ROWNUM 활용
Oracle의 ROWNUM을 활용하여 페이징 쿼리를 구현한다. 서브쿼리를 사용하여 정렬된 결과에 ROWNUM을 부여하고, BETWEEN 절로 원하는 범위의 데이터를 추출한다.
public ArrayList<Board> selectAllBoard(Connection conn, PageInfo pi) {
ArrayList<Board> list = new ArrayList<>();
PreparedStatement pstmt = null;
ResultSet rset = null;
String sql = prop.getProperty("selectAllBoard");
try {
// 시작 행과 끝 행 계산
int startRow = (pi.getCurrentPage() - 1) * pi.getBoardLimit() + 1;
int endRow = startRow + pi.getBoardLimit() - 1;
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, startRow);
pstmt.setInt(2, endRow);
rset = pstmt.executeQuery();
while(rset.next()) {
Board b = new Board();
b.setBoardNo(rset.getInt("BOARD_NO"));
b.setCategoryName(rset.getString("CATEGORY_NAME"));
b.setBoardTitle(rset.getString("BOARD_TITLE"));
b.setMemberId(rset.getString("MEMBER_ID"));
b.setCount(rset.getInt("COUNT"));
b.setCreateDate(rset.getString("CREATE_DATE"));
list.add(b);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
close(rset);
close(pstmt);
}
return list;
}
<Oracle 페이징 쿼리 구조>
- ROWNUM을 활용한 페이징 쿼리
Oracle에서 페이징을 구현할 때는 ROWNUM을 활용한 서브쿼리 구조를 사용한다. 이는 정렬된 결과에 순번을 부여하고, 원하는 범위의 데이터만 추출하는 방식이다.
-- board-mapper.xml의 selectAllBoard 쿼리
SELECT *
FROM (
SELECT ROWNUM RNUM, A.*
FROM (
SELECT BOARD_NO,
CATEGORY_NAME,
BOARD_TITLE,
MEMBER_ID,
COUNT,
TO_CHAR(CREATE_DATE, 'YYYY-MM-DD') AS "CREATE_DATE"
FROM BOARD B
JOIN CATEGORY USING(CATEGORY_NO)
JOIN MEMBER M ON(BOARD_WRITER = MEMBER_NO)
WHERE B.STATUS = 'Y'
AND BOARD_TYPE = 1
ORDER BY BOARD_NO DESC
) A
)
WHERE RNUM BETWEEN ? AND ?
- 쿼리 구조 분석
- 내부 쿼리: 정렬된 게시글 목록 조회
- 중간 쿼리: ROWNUM을 부여하여 순번 생성
- 외부 쿼리: BETWEEN 절로 원하는 범위 추출
실행 결과:
현재 페이지: 2, 페이지당 게시글: 5개
시작 행: 6, 끝 행: 10
→ 6번째부터 10번째 게시글 조회
<JSP에서 페이지네이션 UI 구현>
- JSTL을 활용한 페이지네이션 버튼
JSP에서는 JSTL의 조건문과 반복문을 활용하여 동적인 페이지네이션 UI를 구현한다. PageInfo 객체의 속성들을 활용하여 현재 페이지 상태에 맞는 버튼을 표시한다.
<!-- 페이지네이션은 게시글이 있을 때만 표시 -->
<c:if test="${pi.listCount > 0}">
<div class="pagination">
<!-- 이전 버튼 -->
<c:if test="${pi.startPage > 1}">
<a class="btn btn-primary"
href="${pageContext.request.contextPath}/list.bo?cpage=${pi.startPage - 1}">
< 이전
</a>
</c:if>
<!-- 페이지 번호들 -->
<c:forEach var="p" begin="${pi.startPage}" end="${pi.endPage}">
<c:choose>
<c:when test="${p == pi.currentPage}">
<button class="btn btn-primary" disabled>${p}</button>
</c:when>
<c:otherwise>
<a class="btn btn-outline-primary"
href="${pageContext.request.contextPath}/list.bo?cpage=${p}">${p}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<!-- 다음 버튼 -->
<c:if test="${pi.endPage < pi.maxPage}">
<a class="btn btn-primary"
href="${pageContext.request.contextPath}/list.bo?cpage=${pi.endPage + 1}">
다음 >
</a>
</c:if>
</div>
</c:if>
- 조건부 버튼 표시
- 이전 버튼:
startPage > 1일 때만 표시 - 다음 버튼:
endPage < maxPage일 때만 표시 - 현재 페이지: 비활성화된 버튼으로 표시
- 다른 페이지: 클릭 가능한 링크로 표시
<페이지네이션의 장점과 활용>
- 코드 재사용성
PageInfo 클래스를 활용하면 다른 컨트롤러에서도 동일한 방식으로 페이지네이션을 구현할 수 있다. 이는 일관된 사용자 경험을 제공하고 개발 효율성을 향상시킨다.
// 다른 컨트롤러에서도 동일한 방식으로 활용 가능
public class ReplyController extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
int currentPage = request.getParameter("cpage") != null ?
Integer.parseInt(request.getParameter("cpage")) : 1;
int listCount = new ReplyService().selectReplyCount();
PageInfo pi = new PageInfo(currentPage, listCount, 5, 10);
ArrayList<Reply> list = new ReplyService().selectReplyList(pi);
request.setAttribute("list", list);
request.setAttribute("pi", pi);
// ...
}
}
- 유지보수성 향상
페이지네이션 로직이 PageInfo 클래스에 캡슐화되어 있어, 계산 방식 변경 시 한 곳만 수정하면 된다. 이는 코드의 유지보수성을 크게 향상시킨다.
'Servlet' 카테고리의 다른 글
| JSP/Servlet_10) 썸네일 게시판 사진 미리보기 구현 (0) | 2025.10.17 |
|---|---|
| JSP/Servlet_09) Ajax를 사용한 비동기 방식 (0) | 2025.10.16 |
| JSP/Servlet_07) commons-fileupload2 라이브러리를 활용한 DB 파일 업로드 구현 (0) | 2025.10.14 |
| Servlet_06) 웹 애플리케이션 세션 관리와 보안 강화 (0) | 2025.10.09 |
| Servlet_05) Lombok과 Factory Method 패턴 (0) | 2025.10.08 |