본문 바로가기

스프링 Spring

21. 04. 07.

11.4 조회 페이지와 이동


get.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<jsp:include page="../includes/header.jsp"/>

                <!-- Begin Page Content -->
                <div class="container-fluid">

                    <!-- Page Heading -->
                    <h1 class="h3 mb-2 text-gray-800">Tables</h1>
                    <p class="mb-4">DataTables is a third party plugin that is used to generate the demo table below.
                        For more information about DataTables, please visit the <a target="_blank"
                            href="https://datatables.net">official DataTables documentation</a>.</p>

                    <!-- DataTales Example -->
                    <div class="card shadow mb-4">
                        <div class="card-header py-3">
                            <h6 class="m-0 mt-2 font-weight-bold text-primary float-left">Board List</h6>
                            <a href="register" class="btn btn-primary float-right">write</a>
                        </div>
                        <div class="card-body">
                            <div class="table-responsive">
                                <div id="dataTable_wrapper" class="dataTables_wrapper dt-bootstrap4 no-footer">
                                <div class="row"><div class="col-sm-12 col-md-6"><div class="dataTables_length" id="dataTable_length"><label>Show <select name="dataTable_length" aria-controls="dataTable" class="custom-select custom-select-sm form-control form-control-sm"><option value="10">10</option><option value="25">25</option><option value="50">50</option><option value="100">100</option></select> entries</label></div></div><div class="col-sm-12 col-md-6"><div id="dataTable_filter" class="dataTables_filter"><label>Search:<input type="search" class="form-control form-control-sm" placeholder="" aria-controls="dataTable"></label></div></div></div>
                                <div class="row">
                                        <table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
                                            <thead>
                                                <tr>
                                                    <th>번호</th>
                                                    <th>제목</th>
                                                    <th>작성자</th>
                                                    <th>작성일</th>
                                                    <th>수정일</th>
                                                </tr>
                                            </thead>
                                            <tbody>
                                                <c:forEach items="${list}" var="board">
                                                <tr>
                                                    <td>${board.bno}</td>
                                                    <td><a href="get?bno=${board.bno}"><c:out value="${board.title}"/></a></td>
                                                    <td><c:out value="${board.writer}"/></td>
                                                    <td><fmt:formatDate value="${board.regdate}"/></td>
                                                    <td><fmt:formatDate value="${board.updateDate}"/></td>
                                                </tr>
                                                </c:forEach>
                                            </tbody>
                                             </table>
                                    </div>
                                        <div class="row">
                                            <div class="col-sm-12 col-md-5">
                                                <div class="dataTables_info" id="dataTable_info" role="status" aria-live="polite">Showing 1 to 10 of 10 entries</div>
                                            </div>
                                            <div class="col-sm-12 col-md-7">
                                                <div class="dataTables_paginate paging_simple_numbers" id="dataTable_paginate">
                                                    <ul class="pagination">
                                                        <c:if test="${pageMaker.prev}">
                                                        <li class="paginate_button page-item previous disabled" id="dataTable_previous">
                                                            <a href="list?pageNum=${pageMaker.startPage - 1}&amount=10" aria-controls="dataTable" tabindex="0" class="page-link">Prev</a>
                                                        </li>
                                                        </c:if>
                                                        <c:forEach begin="${pageMaker.startPage}" end="${pageMaker.endPage}" var="num">
                                                        <li class="paginate_button page-item ${num == pageMaker.cri.pageNum ? 'active' : ''}">
                                                            <a href="list?pageNum=${num}&amount=10" aria-controls="dataTable" tabindex="0" class="page-link">${num}</a>
                                                        </li>
                                                        </c:forEach>
                                                        <c:if test="${pageMaker.next}">
                                                        <li class="paginate_button page-item next" id="dataTable_next">
                                                            <a href="list?pageNum=${pageMaker.endPage + 1}&amount=10" aria-controls="dataTable" tabindex="0" class="page-link">Next</a>
                                                        </li>
                                                        </c:if>
                                                    </ul>
                                                </div>
                                            </div>
                                        </div>
                                </div>
                            </div>
                    </div>

                </div>
                <!-- /.container-fluid -->

            </div>
            <!-- Logout Modal-->
            <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"
                aria-hidden="true">
                <div class="modal-dialog" role="document">
                    <div class="modal-content">
                        <div class="modal-header">
                            <h5 class="modal-title" id="myModalLabel">Alert</h5>
                            <button class="close" type="button" data-dismiss="modal" aria-label="Close">
                                <span aria-hidden="true">×</span>
                            </button>
                        </div>
                        <div class="modal-body">처리 완료</div>
                        <div class="modal-footer">
                            <button class="btn btn-danger" type="button" data-dismiss="modal">Close</button>
                        </div>
                    </div>
                </div>
            </div>
            <script>
                $(function() {
                    var result = '${result}';

                    checkModal(result);
                    history.replaceState({}, null, null); // 히스토리 값 초기화
                    function checkModal(result) {
                        if(result === '' || history.state) { // 타입까지 비교
                            return;
                        }
                        if(parseInt(result) > 0) {
                            $(".modal-body").html("게시글 " + result + "번 글이 등록되었습니다.");
                        }
                        $("#myModal").modal("show");
                    }
                });

            </script>
            <!-- End of Main Content -->
<jsp:include page="../includes/footer.jsp"/>

🔶 input 태그는 value안에 ${}를 넣지만 input 태그가 아닌 것들은 태그 사이에 넣어야 함. 그래서 textarea 태그는 태그 사이에 ${board.content} 넣은 것.
🔶 정석대로 하면 title, content, writer에는 <c:out> 처리를 해야 한다. XSS(Cross Site Scripting)을 막기 위해서!


✅ 목록 페이지와 뒤로 가기 문제

등록 -> 목록 -> 조회까지 한 후 '뒤로 가기'를 하면 다시 게시물의 등록 결과를 확인하는 방식으로 동작하게 된다.
'뒤나 앞으로 가기'를 하면 서버를 다시 호출하는 게 아니라 과거에 자신이 가진 모든 데이터를 활용하기 때문인데(캐시에 있는 데이터를 다시 가져 오는 것.), 조회 페이지나 목록 페이지를 여러 번 뒤나 앞으로 이동해도 서버에서는 첫 호출을 제외하고는 별다른 변화가 없다(로그가 안 찍힘).

list.jsp 일부

<script>
    $(function() {
        var result = '${result}';

        checkModal(result);
        history.replaceState({}, null, null); // 히스토리 값 초기화
        function checkModal(result) {
            if (result === '' || history.state) { // 타입까지 비교
                return;
            }
            if (parseInt(result) > 0) {
                $(".modal-body").html("게시글 " + result + "번 글이 등록되었습니다.");
            }
            $("#myModal").modal("show");
        }
    });
</script>

🔶 한 번 checkModal한 후에는 history 값을 초기화하겠다는 뜻 (브라우저 콘솔에서 history.state를 찍어보면 null이 나오는데, null 대신 빈 객체 {}로 대체). 스크립트의 모든 처리가 끝나게 되면 history에 쌓이는 상태는 모달창을 보여줄 필요가 없는 상태가 된다.



11.5 게시물의 수정/삭제 처리


✅ 수정/삭제 페이지로 이동

BoardController.java 일부

    @GetMapping({"get", "modify"})
    public void get(@RequestParam Long bno, Model model) {
        log.info("get or modify.....");
        model.addAttribute("board", service.get(bno));
    }

🔶 수정/삭제 페이지로 이동하는 것은 조회 페이지와 같아서 배열로 처리함. (@GetMapping이나 @PostMapping 등에는 URL을 배열로 처리할 수 있다.)
🔶 modify.jsp를 자동으로 찾으려 함.

modify.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<jsp:include page="../includes/header.jsp"/>

                <!-- Begin Page Content -->
                <div class="container-fluid">

                    <!-- Page Heading -->
                    <h1 class="h3 mb-2 text-gray-800">Tables</h1>
                    <p class="mb-4">DataTables is a third party plugin that is used to generate the demo table below.
                        For more information about DataTables, please visit the <a target="_blank"
                            href="https://datatables.net">official DataTables documentation</a>.</p>

                    <!-- DataTales Example -->
                    <div class="card shadow mb-4">
                        <div class="card-header py-3">
                            <h6 class="m-0 font-weight-bold text-primary">Board view</h6>
                        </div>
                        <div class="card-body">
                        <form method="post" class="needs-validation" novalidate>
                            <div class="form-group">
                                <label for="bno" class="font-wieght-bold text-warning">bno</label>
                                <input type="text" class="form-control" id="bno" name="bno" required value="${board.bno}" readonly>
                                <div class="valid-feedback">Valid.</div>
                                 <div class="invalid-feedback">Please fill out this field.</div>
                            </div>
                            <div class="form-group">
                                <label for="title" class="font-wieght-bold text-warning">title</label>
                                <input type="text" class="form-control" id="title" placeholder="게시글 제목 입력" name="title" required value="${board.title}">
                                <div class="valid-feedback">Valid.</div>
                                 <div class="invalid-feedback">Please fill out this field.</div>
                            </div>
                            <div class="form-group">
                                <label for="content" class="font-wieght-bold text-warning">content</label>
                                <textarea class="form-control" rows=10 id="content" placeholder="게시글 내용 입력" name="content" required>${board.content }</textarea>
                                <div class="valid-feedback">Valid.</div>
                                 <div class="invalid-feedback">Please fill out this field.</div>
                            </div>
                            <div class="form-group">
                                <label for="writer" class="font-wieght-bold text-warning">writer</label>
                                <input type="text" class="form-control" id="writer" placeholder="게시글 작성자" name="writer" required value="${board.writer}" readonly>
                                <div class="valid-feedback">Valid.</div>
                                 <div class="invalid-feedback">Please fill out this field.</div>
                            </div>
                            <a href="list" data-oper="list" class="btn btn-outline-info float-right mr-1">List</a>
                            <button data-oper="remove" class="btn btn-outline-danger float-right mr-1" formaction="remove">Remove</button>
                            <button data-oper="modify" class="btn btn-outline-warning float-right mr-1">Modify</button>
                        </form>
                        </div>
                    </div>
                </div>
                <script>
                // Disable form submissions if there are invalid fields
                (function() {
                  'use strict';
                  window.addEventListener('load', function() {
                    // Get the forms we want to add validation styles to
                    var forms = document.getElementsByClassName('needs-validation');
                    // Loop over them and prevent submission
                    var validation = Array.prototype.filter.call(forms, function(form) {
                      form.addEventListener('submit', function(event) {
                        if (form.checkValidity() === false) {
                          event.preventDefault();
                          event.stopPropagation();
                        }
                        form.classList.add('was-validated');
                      }, false);
                    });
                  }, false);
                })();
                </script>
                <!-- /.container-fluid -->

            </div>
            <!-- End of Main Content -->
<jsp:include page="../includes/footer.jsp"/>

🔶 remove 버튼에 있는 formaction은 http://localhost:8080/board/modify?bno=163855에서 remove 버튼을 누르면 현재 목록인 modify?bno=163855를 제거하고 remove로 바꾸겠다는 말! 가끔 잊는데 까먹지 말자 😂
🔶 단순 페이지 이동 시에 지나치게 스크립트를 사용하면 안 된다.



12. 오라클 데이터베이스 페이징 처리


12.1 order by의 문제

데이터의 양이 많을수록 정렬 작업은 많은 리소스를 소모한다. 그래서 DB를 이용할 때 웹에서 신경쓰는 것은 빠르게 처리되는 것, 필요한 양만큼만 데이터를 가져오는 것이다. 이것이 페이징을 하는 이유다.

✅ 실행 계획과 order by

DB에 전달된 SQL문은 SQL 파싱 -> SQL 최적화 -> SQL 실행 과정을 거쳐 처리된다.

파싱: 단순히 '바꾸다'보다 더 강한 의미. 그 문구가 적합한지 check를 한다. 즉 '검증을 통한 변경'. SQL문의 문법이 맞는지, 해당 테이블이 존재하는지, 스키마의 접근 권한이 있는지 등. parseInt도 문자열이 전부 정수로 이루어져 있는지 check한다는 것.




12.2 order by 보다는 인덱스

인덱스라는 존재가 이미 정렬된 구조이므로 인덱스를 이용해서 정렬을 생략하는 게 가장 일반적인 방법이다.

12.3 인덱스를 이용하는 정렬

✅ 인덱스와 오라클 힌트

오라클은 select문을 전달할 때 '힌트'를 사용할 수 있는데, 말 그대로 데이터베이스에 '전달한 select문을 이렇게 실행해 주면 좋겠다.'라는 힌트다.

SELECT 
    /*+ INDEX_DESC(TBL_BOARD PK_BOARD)*/
    ROWNUM RN, TBL_BOARD.*
FROM TBL_BOARD
WHERE BNO > 0
AND ROWNUM <= 20;
SELECT * FROM TBL_BOARD WHERE BNO > 0 ORDER BY BNO DESC;

🔶 두 퀴리문의 cost는 같지만 힌트를 사용한 것은 인덱스를 통해 내림차순을 하겠다고 명시적으로 지정. 🔶 아래 쿼리문은 order by를 한번 더 태워야 하는 단점이 있는 것이다. 🔶 특정 인덱스가 중간에 삭제가 되거나 변경이 되면 리빌드를 해줘야 한다.


12.4 ROWNUM과 인라인뷰

오라클은 페이지 처리를 위해 ROWNUM이라는 특별한 키워드를 사용해서 데이터에 순번을 붙여 사용함.
ROWNUM은 실제 데이터가 아니라 테이블에서 데이터를 추출한 후에 처리되는 변수이므로 상황에 따라 그 값이 매번 달라질 수 있다.
ROWNUM을 조회하려면 스키마를 정확히 써 줘야 조회가 가능하다.

SELECT 
    ROWNUM, TBL_BOARD.*
FROM TBL_BOARD
WHERE BNO > 0;

  • 인라인뷰(In-line View) 처리

'SELECT문 안쪽 FROM에 다시 SELECT문' => 어떤 결과를 구하는 SELECT문이 있고, 그 결과를 다시 대상으로 삼아서 SELECT를 하는 것.

WITH TMP AS (
    SELECT
    /*+ INDEX_DESC(TBL_BOARD PK BOARD)*/
    ROWNUM RN, TBL_BOARD.*
    FROM TBL_BOARD
    WHERE BNO > 0
)
SELECT BNO, TITLE, WRITER, REGDATE, UPDATEDATE FROM TMP
WHERE RN >= 11
AND RN <= 20;

🔶 인라인 뷰를 쓸 때 WITH절을 사용하면 가독성이 더 좋다.



결국 정리하면

  • 필요한 순서로 정렬된 데이터에 ROWNUM 붙이기
  • 처음부터 해당 페이지의 데이터를 ROWNUM <=30 같은 조건을 이용해서 구하기
  • 구해놓은 데이터를 하나의 테이블처럼 간주하고 인라인뷰로 처리하기
  • 인라인뷰에서 필요한 데이터만을 남기기.



13. MyBatis와 스프링에서 페이징 처리 ✨

페이징 처리를 위해 SQL을 실행할 때 필요한 파라미터에는 1. 페이지 번호(pageNum), 2. 한 페이지당 몇 개의 데이터(amount)를 보여줄 것인지가 있다.

Criteria.java

package site.levinni.domain;

import org.springframework.web.util.UriComponentsBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Criteria {
    private int pageNum = 1;
    private int amount = 10;
} 

13.1 MyBatis 처리와 테스트


BoardMapper.java에 추가해 줌.

    public List<BoardVO> getListWithPaging(Criteria cri);

쿼리문을 아래처럼 수정하자.

WITH TMP AS (
    SELECT
    /*+ INDEX_DESC(TBL_BOARD PK BOARD)*/
    ROWNUM RN, TBL_BOARD.*
    FROM TBL_BOARD
    WHERE BNO > 0
    AND ROWNUM <= 20
)
SELECT BNO, TITLE, WRITER, REGDATE, UPDATEDATE FROM TMP
WHERE RN >= 11;

🔶 바깥 WHERE 절에 있던 AND 부분을 인라인 뷰쪽에 넣어서 선행 쪽에서 아예 값을 줄여서 옴.



🔶 CARDINALITY 수가 확 줄기 때문에 코스트에서 차이나는 걸 알 수 있다.

BoardMapper.xml에 쿼리문 추가해 주고, (세미콜론 빼고 가져오자!! 😢)

BoardMapperTests.java에 해당 테스트 코드 추가.

    @Test
    public void testGetListWithPaging() {
        mapper.getListWithPaging(new Criteria(5, 20)).forEach(log::info);
    }

sql문이 정상적으로 동작하면 고정값들을 변수로 담기.

<select id="getListWithPaging" resultType="site.levinni.domain.BoardVO">
    <![CDATA[
    WITH TMP AS (
        SELECT 
        /*+ INDEX_DESC(TBL_BOARD PK_BOARD)*/
        ROWNUM RN, TBL_BOARD.*
        FROM TBL_BOARD
        WHERE BNO > 0
        AND ROWNUM <= #{pageNum} * #{amount}
    )
    SELECT BNO, TITLE, CONTENT, WRITER, REGDATE, UPDATEDATE FROM TMP
    WHERE RN > (#{pageNum} - 1) * #{amount}
    ]]>
</select>



13.2 BoardController와 BoardService 수정

페이징 처리는 브라우저에서 들어오는 정보를 기준으로 동작하기 때문에 컨트롤러와 서비스에서 전달되는 파라미터들을 받는 형태로 수정해야 한다.

BoardService.java

package site.levinni.service;

import java.util.List;
import site.levinni.domain.BoardVO;
import site.levinni.domain.Criteria;

public interface BoardService {
    void register (BoardVO boardVO); 

    BoardVO get(Long bno); // 상세 조회

    boolean modify(BoardVO boardVO);

    boolean remove(Long bno);

//    List<BoardVO> getList();
    List<BoardVO> getList(Criteria cri); // 페이징 처리가 된 목록 조회
}

BoardServiceImpl.java 일부

    @Override
    public List<BoardVO> getList(Criteria cri) {
        // TODO Auto-generated method stub
        log.info("getList....");
        return mapper.getListWithPaging(cri);
    }

🔶 Criteria 객체는 결국 서비스가 아닌 컨트롤러가 관리하게 되는 것.

BoardServiceTests.java 일부

    @Test
    public void testGetList() {
        service.getList(new Criteria()).forEach(log::info);
    }

BoardController.java 일부

    @GetMapping("/list")
    public void list(Criteria cri, Model model) {
        log.info("list.....");
        model.addAttribute("list", service.getList(cri));
        model.addAttribute("pageMaker", new PageDTO(cri, service.getTotal(cri)));
    }

🔶 이처럼 Criteria 클래스를 하나 만들어 두면 파라미터나 리턴 타입으로 사용할 수 있어서 편리하다.

BoardControllerTests.java 일부

    @Test
    public void testList() throws Exception {
        log.info(mvc.perform(MockMvcRequestBuilders.get("/board/list")
                .param("pageNum", "1")
                .param("amount", "20")
                )
            .andReturn()
            .getModelAndView()
            .getModelMap()
            );
    }



14. 페이징 화면 처리

  • 브라우저 주소창에서 페이지 번호를 전달해서 결과를 확인하는 단계
  • JSP에서 페이지 번호를 출력하는 단계
  • 각 페이지 번호에 클릭 이벤트 처리
  • 전체 데이터 개수를 반영해서 페이지 번호 조절

목록 페이지에서 조회·수정·삭제 페이지까지 페이지 번호가 계속 유지되어야 한다.

14.1 페이징 처리할 때 필요한 정보들

  • 현재 페이지 번호(page)
  • 이전과 다음으로 이동 가능한 링크 표시 여부(prev, next)
  • 화면에서 보이는 페이지의 시작 번호와 끝 번호(startPage, endPage)


14.2 페이징 처리를 위한 클래스 설계

클래스를 구성하면 Controller 계층에서 JSP 화면에 전달할 때에도 객체를 생성해서 Model에 담아 보내는 과정이 단순해짐.

PageDTO.java

package site.levinni.domain;

import lombok.Data;

@Data
public class PageDTO {
    private int startPage;
    private int endPage;
    private boolean prev;
    private boolean next;

    private int total;
    private Criteria cri;

    public PageDTO(Criteria cri, int total) {
        this.cri = cri;
        this.total = total;

        endPage = (cri.getPageNum() + 9) / 10 * 10;
        startPage = endPage - 9;

        int realEnd = (total + 9) / 10; 
        endPage = realEnd < endPage ? realEnd : endPage;

        prev = startPage > 1;
        next = endPage < realEnd;
    }
}

'스프링 Spring' 카테고리의 다른 글

21. 04. 13.  (0) 2021.04.14
21. 04. 12.  (0) 2021.04.13
21. 04. 06.  (0) 2021.04.06
Part3. 기본적인 웹 게시물 관리 21. 04. 05.  (0) 2021.04.06
21. 04. 02.  (0) 2021.04.03