JSP/Servlet_08) Pagination 게시글 페이징 처리
2025. 10. 15. 16:48

학습 목표

  • 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 ?

- 쿼리 구조 분석

  1. 내부 쿼리: 정렬된 게시글 목록 조회
  2. 중간 쿼리: ROWNUM을 부여하여 순번 생성
  3. 외부 쿼리: 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}">
                &lt; 이전
            </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}">
                다음 &gt;
            </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 클래스에 캡슐화되어 있어, 계산 방식 변경 시 한 곳만 수정하면 된다. 이는 코드의 유지보수성을 크게 향상시킨다.