ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링(Spring) 개발 - (17) 페이징 (전자정부 프레임워크 이용)
    Spring 2015.11.09 17:25

    이번글에서는 페이징에 대해서 이야기를 합니다.


    페이징은 두가지 방식을 소개하려고 합니다.


    첫번째가 지금 설명하려는 전자정부 프레임워크를 이용하는 페이징 방법이고


    두번째는 jQuery와 Ajax를 이용한 페이징 방법입니다. 


    인터넷에서 페이징을 찾아보면 참 많은 글들이 있습니다. 저는 그런것들을 좀 더 편하게, 공통적인 부분을 정리해서 사용하는걸 소개해드리려고 합니다.


    11.14 수정 - 태그라이브러리를 설정하는 부분이 추가되었습니다.


    ------------------------------------------------------------------------------------


    1. 설정 및 공통기능

    전자정부 프레임워크의 페이징 기능을 사용하기 위해서는 몇가지 설정이 필요하다.


    1. 라이브러리

    pom.xml에 전자정부 프레임워크 라이브러리를 추가해야한다.

    그런데 전자정부 프레임워크는 별도의 저장소에서 다운을 받아야하기 때문에 repository를 등록하고 라이브러리를 추가해주자.

    먼저 repository는 다음과 같다.

    
    	egovframe
    	http://www.egovframe.go.kr/maven/
    	
    		true
    	
    	
    		false
    	
    
    

    그 다음으로 라이브러리는 다음과 같다.

    
    	egovframework.rte
    	egovframework.rte.ptl.mvc
    	3.5.0
    
    

    2. 페이징 Bean 설정

    다음으로는 페이징을 사용하기 위한 bean을 설정할 차례이다. 

    context-common.xml에 다음의 내용을 작성하자. 

    <bean id="textRenderer" class="egovframework.rte.ptl.mvc.tags.ui.pagination.DefaultPaginationRenderer"/>   
        <bean id="paginationManager" class="egovframework.rte.ptl.mvc.tags.ui.pagination.DefaultPaginationManager">
            <property name="rendererType">
                <map>
                    <entry key="text" value-ref="textRenderer"/>
                </map>
            </property>
        </bean>
    

    이것은 전자정부 프레임워크의 페이지 랜더방식을 결정하는 부분이다. 이에 대한 자세한 설명은 전자정부 프레임워크의 설명을 참조하길 바란다.

    (http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:ptl:view:paginationtag)


    3.SQL

    페이징의 기능을 구현하기에 앞서 쿼리에 대해서 잠시 설명을 하려고 한다.

    오라클의 페이징 쿼리는 그 종류가 몇가지가 있다. 

    그 중에서 대표적으로 사용되는 쿼리들은 다음과 같다.

    SELECT
        *
    FROM 
        (SELECT 
            ROWNUM AS RNUM, T1.*
        FROM
            (SELECT 
                *
            FROM 
                TB_BOARD
            ORDER BY IDX DESC) T1
        WHERE ROWNUM <= 20)
     WHERE RNUM >= 0;
    

    또는

    SELECT
        * 
    FROM(
        SELECT
            T1.*,
            ROWNUM AS RNUM,
            COUNT(*) OVER() AS TOTAL_CNT
        FROM(
            SELECT
                *
            FROM
                TB_BOARD
            ORDER BY IDX DESC) T1
        )
    WHERE
        RNUM > 0 AND RNUM <= 20
    

    이러한 쿼리는 실행속도도 괜찮고, 대용량 (100만건 이상)의 데이터에서도 큰 성능저하가 없이 실행이 되기 때문에 많이 사용되는 쿼리이다. 

    그런데 필자의 경험상 딱 한번, 이러한 페이징 쿼리가 제대로 동작하지 않는 경우가 있었다. 

    첫페이지에서는 20개를 가져오는데, 2페이지에서는 30개, 3페이지에서는 40개 이런식으로 데이터를 이상하게 가져오는 경우가 딱 한번 있었다. 

    그것도 개발서버에서는 문제가 없었는데 실제서버에서 저렇게 동작을 했었다. 


    그 후로는 이런식으로 페이징 쿼리를 작성하였다.

    SELECT 
        AAA.*
    FROM(
        SELECT 
            COUNT(*) OVER() AS TOTAL_COUNT,
            AA.*
        FROM(
            SELECT
                ROW_NUMBER() OVER (ORDER BY IDX DESC) RNUM,
                IDX,
                TITLE,
                HIT_CNT,
                CREA_DTM
            FROM
                TB_BOARD
        
        ) AA
    ) AAA
    WHERE 
        AAA.RNUM BETWEEN 0 AND 20
    

    바로 ROW_NUMBER() 함수를 이용한 방식이다. 

    이러한 방식은 오라클에서 공식적으로 보여주고 있는 방식이다. (http://www.oracle.com/technetwork/issue-archive/2007/07-jan/o17asktom-093877.html) URL의 하단에 Pagination in Getting Rows N Through M 부분을 확인하면 된다.

    따라서 앞으로는 위와 같은 쿼리를 이용하여 페이징을 처리를 할 계획이다. 


    * 여기서 9번째 줄의 ROW_NUMBER() OVER () RNUM 부분이 굉장히 중요하다. *

    OVER() 안에서 정렬조건, 즉 정렬을 수행할 컬럼과 순서를 정하고 해당 결과를 RNUM이라는 별칭으로 조회를 하고 있다. 

    꼭 여기서 정렬을 수행하고, 그 결과를 RNUM이라는 이름으로 해야한다. 

    페이징 쿼리는 프로그램의 여러곳에서 사용이 되기 때문에 공통기능으로 작성할 것이고, 쿼리 역시 마찬가지이다. 공통적으로 사용될 수 있는 부분은 따로 작성을 해서 참조를 하는식으로 할 예정이다. 따라서 해당컬럼의 이름을 다르게 작성한다면 에러가 발생할 것이다. 

    맨 마지막의 WHERE절을 보면 AAA.RNUM BETWEEN0 AND 20 이라고 되어있는것을 볼 수 있다. 해당 부분이 공통으로 빠질 부분이다. 여기서 AAA.RNUM이 방금 이야기한 ROW_NUMBER() 컬럼인것을 확인하자. (RNUM이라는 이름이 아니더라도 상관은 없다. 단, 공통으로 빼는 부분에서는 해당 컬럼의 이름을 동일하게 맞춰야한다.) 

    그럼 이제 해당 쿼리를 한번 확인해보자. 


    먼저, TB_BOARD에 데이터를 좀 만들어놓자. 

    기존에 작성을 했을때는 데이터가 몇개 없기 때문에 페이징이 제대로 동작하는지 확인하기가 어렵다.

    다음의 쿼리를 실행시키자.

    BEGIN
        FOR i IN 1..500 LOOP
        INSERT INTO TB_BOARD(IDX, TITLE, CONTENTS, HIT_CNT, DEL_GB, CREA_DTM, CREA_ID) VALUES(SEQ_TB_BOARD_IDX.NEXTVAL, '제목 '||i, '내용 '||i, 0, 'N', SYSDATE, 'Admin');
        END LOOP;
    END;
    /
    

    TB_BOARD 테이블에 500건의 데이터를 집어넣는 쿼리이다. 

    그 후 위에서 작성한 페이징 쿼리를 실행시켜보면 다음과 같은 결과를 볼 수 있다.

    TB_BOARD 테이블에 기존 데이터에 500건의 데이터를 추가하고 페이징 쿼리를 실행시킨 결과이다.

    화면 하단에서 볼 수 있듯이 20개의 데이터를 정상적으로 조회한 것을 확인할 수 있다. 

    여기서 숫자를 0, 20 대신에 원하는 부분을 입력하게 되면 해당 데이터가 조회된다. 각자 확인을 하길 바란다.


    4. AbstractDAO

    이제 DAO에 페이징 쿼리용 메서드를 하나 만드려고 한다. 페이징을 처리하는 로직은 항상 똑같기 때문에 하나의 메서드를 정의해놓고 해당 메서드를 호출하면 좀 더 편하게 개발을 할 수가 있다. 

    AbstractDAO.java 파일에 다음의 내용을 추가한다.

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public Map selectPagingList(String queryId, Object params){
    	printQueryId(queryId);
    	
    	Map<String,Object> map = (Map<String,Object>)params;
    	PaginationInfo paginationInfo = null;
    	
    	if(map.containsKey("currentPageNo") == false || StringUtils.isEmpty(map.get("currentPageNo")) == true)
    		map.put("currentPageNo","1");
    	
    	paginationInfo = new PaginationInfo();
    	paginationInfo.setCurrentPageNo(Integer.parseInt(map.get("currentPageNo").toString()));
    	if(map.containsKey("PAGE_ROW") == false || StringUtils.isEmpty(map.get("PAGE_ROW")) == true){
    		paginationInfo.setRecordCountPerPage(15);
    	}
    	else{
    		paginationInfo.setRecordCountPerPage(Integer.parseInt(map.get("PAGE_ROW").toString()));
    	}
    	paginationInfo.setPageSize(10);
    	
    	int start = paginationInfo.getFirstRecordIndex();
    	int end = start + paginationInfo.getRecordCountPerPage();
    	map.put("START",start+1);
    	map.put("END",end);
    	
    	params = map;
    	
    	Map<String,Object> returnMap = new HashMap<String,Object>();
    	List<Map<String,Object>> list = sqlSession.selectList(queryId,params);
    	
    	if(list.size() == 0){
    		map = new HashMap<String,Object>();
    		map.put("TOTAL_COUNT",0);  
    		list.add(map);
    		
    		if(paginationInfo != null){
    			paginationInfo.setTotalRecordCount(0);
    			returnMap.put("paginationInfo", paginationInfo);
    		}
    	}
    	else{
    		if(paginationInfo != null){
    			paginationInfo.setTotalRecordCount(Integer.parseInt(list.get(0).get("TOTAL_COUNT").toString()));
    			returnMap.put("paginationInfo", paginationInfo);
    		}
    	}
    	returnMap.put("result", list);
    	return returnMap;
    }
    

    소스를 간단히 살펴보자. 

    먼저 8번째 줄에서 볼 수 있는 currentPageno는 현재 페이지 번호를 의미한다. 예상치 못한 상황에서 이 값이 없을 경우 에러가 발생하는걸 방지하기 위한 부분이다.


    그 다음 볼수있는 PaginationInfo 클래스는 페이징에 필요한 정보를 가지고 있는 전자정부 프레임워크의 클래스다. 이 클래스에 여러가지 값을 설정하고 그 값을 이용해서 화면에 출력할 것을 계산하기도 한다.


    그 다음으로는 13~18번째 줄은 한 페이지에 몇개의 행을 보여줄 것인지를 설정한다. 만약 화면에서 특정한 값을 보내준다면 그에 맞는 행을 보여주고, 그 값이 없으면 15개를 보여주도록 하였다.


    19번째줄은 한번에 보여줄 페이지의 크기를 지정하였다. [이전] 1~10 [다음] 식으로 나올 때 1~10의 10개를 설정한 것이다.


    그 다음으로 시작과 끝 값을 계산해서 파라미터에 추가해주고 쿼리를 실행시킨다. 


    AbstractDAO의 selectList는 단순히 결과를 반환하고 끝났지만, 여기서는 추가로 작업을 해야할 것이 남아있다.

    만약 조회된 결과값이 없으면 그에 해당하는 결과를 화면에 표시할 수 있도록 TOTAL_COUNT라는 값을 추가해주었다. 

    그리고 반환될 결과에 위에서 만든 paginationInfo 클래스와 조회 후 결과리스트를 각각 paginationInfo와 result라는 key로 저장하여 반환을 하였다.


    5. 태그 라이브러리 추가

    페이징 태그를 작성하기 위해서 JSP에서는 전자정부 태그 라이브러리를 추가해야한다.

    include-header.jspf 파일에 다음의 태그 라이브러리를 추가하자.

    <%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui" %>


    2. 개발 소스

    위에서 작성한것은 공통적으로 사용될 부분을 작성하였다. 이제 이러한 것을 바탕으로 기존의 게시판을 변경하도록 하겠다.


    1. JSP

    먼저 jsp를 변경하도록 하자. 

    boardList.jsp를 다음과 같이 수정하도록 한다.

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html lang="ko">
    <head>
    <%@ include file="/WEB-INF/include/include-header.jspf" %>
    </head>
    <body>
    	<h2>게시판 목록</h2>
    	<table class="board_list">
    		<colgroup>
    			<col width="10%"/>
    			<col width="*"/>
    			<col width="15%"/>
    			<col width="20%"/>
    		</colgroup>
    		<thead>
    			<tr>
    				<th scope="col">글번호</th>
    				<th scope="col">제목</th>
    				<th scope="col">조회수</th>
    				<th scope="col">작성일</th>
    			</tr>
    		</thead>
    		<tbody>
    			<c:choose>
    				<c:when test="${fn:length(list) > 0}">
    					<c:forEach var="row" items="${list}" varStatus="status">
    						<tr>
    							<td>${row.IDX }</td>
    							<td class="title">
    								<a href="#this" name="title">${row.TITLE }</a>
    								<input type="hidden" id="IDX" value="${row.IDX }">
    							</td>
    							<td>${row.HIT_CNT }</td>
    							<td>${row.CREA_DTM }</td>
    						</tr>
    					</c:forEach>	
    				</c:when>
    				<c:otherwise>
    					<tr>
    						<td colspan="4">조회된 결과가 없습니다.</td>
    					</tr>
    				</c:otherwise>
    			</c:choose>	
    		</tbody>
    	</table>
    	
    	<c:if test="${not empty paginationInfo}">
    		<ui:pagination paginationInfo = "${paginationInfo}" type="text" jsFunction="fn_search"/>
    	</c:if>
    	<input type="hidden" id="currentPageNo" name="currentPageNo"/>
    	
    	<br/>
    	<a href="#this" class="btn" id="write">글쓰기</a>
    	
    	<%@ include file="/WEB-INF/include/include-body.jspf" %>
    	<script type="text/javascript">
    		$(document).ready(function(){
    			$("#write").on("click", function(e){ //글쓰기 버튼
    				e.preventDefault();
    				fn_openBoardWrite();
    			});	
    			
    			$("a[name='title']").on("click", function(e){ //제목 
    				e.preventDefault();
    				fn_openBoardDetail($(this));
    			});
    		});
    		
    		
    		function fn_openBoardWrite(){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/openBoardWrite.do' />");
    			comSubmit.submit();
    		}
    		
    		function fn_openBoardDetail(obj){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/openBoardDetail.do' />");
    			comSubmit.addParam("IDX", obj.parent().find("#IDX").val());
    			comSubmit.submit();
    		}
    		
    		function fn_search(pageNo){
    			var comSubmit = new ComSubmit();
    			comSubmit.setUrl("<c:url value='/sample/openBoardList.do' />");
    			comSubmit.addParam("currentPageNo", pageNo);
    			comSubmit.submit();
    		}
    	</script>	
    </body>
    </html>
    

    먼저 48~50번째 줄을 살펴보자. 

    이 부분이 화면에서 페이징으로 바뀔 부분이다. 전자정부 프레임워크에서는 페이징을 커스텀태그를 이용하여 화면에 보여주도록 구성되어있다. 이게 무슨 소리인지는 잠시 후에 결과를 보면서 다시 이야기를 하겠다.

    여기서 살펴볼것은 <ui:pagination> 태그에서 paginationInfo = "${paginationInfo}" 와 jsFunction="fn_search" 부분이다. 

    먼저 paginationInfo는 페이징 태그를 만들기 위해서 필요한 정보들을 의미한다. 앞서서 AbstractDAO에서 결과를 반환할 때, paginationInfo와 result를 반환하도록 설정한것을 다시 한번 확인해보자. 

    그때 반환했던 paginationInfo가 여기서 사용되는 것이다. 

    그리고 jsFunction은 페이징 태그를 클릭했을 때 수행할 함수를 의미한다. 여기서는 fn_search 라는 함수를 호출하도록 하였다.

    그 다음으로 51번 줄에서는 currentPageNo라는 hidden 태그를 놓고, 현재 페이지 번호를 저장하도록 하였다.



    그 다음으로는 84번째줄의 fn_search 함수를 살펴보자. 

    이 함수가 위에서 jsFunction에서 호출하려고 하는 함수이다. 

    특별한 부분은 없고 게시판 목록을 호출할 때 currentPageNo라는 값을 같이 전송해주는것을 확인하자. 


    여기서 잠시 앞으로 모든 작업을 끝낸 후의 결과를 한번 보고 넘어가자.

    앞으로 모든 작업을 완료하고 실행시키면 다음과 같은 화면을 볼 수 있다.


    게시글 목록이 15개씩 보여지면서 하단에는 [처음][이전] 1~10 [다음][마지막] 이라는 태그가 생긴것을 확인할 수 있다. 

    위에서 <ui:pagination>태그 부분이 실제 화면에서는 이런식으로 화면에 표시가 된다. 


    2. JAVA

    이제 서버 부분을 하나씩 살펴보도록 하겠다. Controller, Service, DAO 순서로 하나씩 살펴보자.

    1) Controller 

    SampleController.java 파일을 열어서 openBoardList.do를 다음과 같이 수정하자.

    @RequestMapping(value="/sample/openBoardList.do")
    public ModelAndView openBoardList(CommandMap commandMap) throws Exception{
    	ModelAndView mv = new ModelAndView("/sample/boardList");
    	
    	Map<String,Object> resultMap = sampleService.selectBoardList(commandMap.getMap());
    	
    	mv.addObject("paginationInfo", (PaginationInfo)resultMap.get("paginationInfo"));
    	mv.addObject("list", resultMap.get("result"));
    	
    	return mv;
    }
    

    기존의 openBoardList가 약간 수정되었다. 

    먼저 samplerService.selectBoardList의 결과로 map을 반환받는것을 볼 수 있다. 이것은 앞에서 AbstractDAO를 만들었을 때 paginationInfo와 result 두가지를 반환도록 했기 때문에 결과값이 list에서 map 형식으로 수정되었다. 

    그리고 그 결과값인 resultMap에서 paginationInfo와 result를 mv에 담아주는것을 확인할 수 있다. 

    앞의 jsp에서 <ui:pagination paginationInfo="${paginationInfo}" 라고 했던것을 다시 기억해보자. 그 값을 사용할 수 있도록 클라이언트에 paginationInfo라는 클래스를 보내주었다. 


    2) Service

    SampleService.java파일과 SampleServiceImpl.java 파일의 selectBoardList 메서드를 각각 다음과 같이 변경한다.


    SampleService.java

    Map<String, Object> selectBoardList(Map<String, Object> map) throws Exception;
    


    SampleServiceImpl.java

    @Override
    public Map<String, Object> selectBoardList(Map<String, Object> map) throws Exception {
    	return sampleDAO.selectBoardList(map);
    }
    

    리턴값이 list에서 map으로 변경되었다. 


    3) DAO

    Sample DAO.java 파일을 다음과 같이 변경한다.

    @SuppressWarnings("unchecked")
    public Map<String, Object> selectBoardList(Map<String, Object> map) throws Exception{
    	return (Map<String, Object>)selectPagingList("sample.selectBoardList", map);
    }
    

    DAO에서는 selectList 대신 방금 만든 selectPagingList를 사용하도록 변경하였다. 그리고 리턴값도 마찬가지로 map 인것을 볼 수 있다.


    4) XML

    이제 마지막으로 쿼리문을 변경할 차례이다. 

    Sample_SQL.xml에서 selectBoardList 쿼리를 다음과 같이 변경하자.

    <select id="selectBoardList" parameterType="hashmap" resultType="hashmap">
    	<include refid="common.pagingPre"/> 
    	<![CDATA[
    		SELECT
    			ROW_NUMBER() OVER (ORDER BY IDX DESC) RNUM,
    			IDX,
    			TITLE,
    			HIT_CNT,
    			CREA_DTM
    		FROM
    			TB_BOARD
    		WHERE
    			DEL_GB = 'N'
    	]]>
    	<include refid="common.pagingPost"/> 
    </select>
    

    여태까지 작성했던 쿼리들과는 약간 다른것을 확인할 수 있다.

    쿼리의 앞,뒤로 <include> 라는게 붙어있는 것을 볼 수 있다.

    이것은 MyBatis의 기능으로 쿼리의 일부분을 만들어놓고, 그것을 가져다 사용할 수 있도록 해주는 방법이다.

    위에서 페이징쿼리에 대해서 이야기할 때, "페이징 쿼리는 프로그램의 여러곳에서 사용이 되기 때문에 공통기능으로 작성할 것이고, 쿼리 역시 마찬가지이다. 공통적으로 사용될 수 있는 부분은 따로 작성을 해서 참조를 하는식으로 할 예정이다." 라는 이야기를 했었다. 

    여기서 그것을 확인할 수 있다. 페이징 쿼리에서 항상 똑같이 사용되는 부분을 각각 pagingPre와 pagingPost라는 이름으로 만들어놓고, 실제 개발을 할때는 이 부분을 그냥 붙여넣는 식으로 해서 최대한 중복되는 것을 적게 하였다. 

    따라서 페이징이 들어가는 다른 모든 부분도 이와 같이 앞,뒤로 페이징 쿼리만 참조하고 그 외 부분은 페이징을 신경쓰지 않고 쿼리를 작성하면 된다. 

    단, 1) RNUM은 필수로 만들어줘야한다. 

    2) 모든 쿼리를 이렇게 작성할 수는 없다. 화면에 보여줄 결과와 쿼리에 따라서는 페이징 쿼리 부분도 약간 바꿔야 할수도 있다. 그럴때는 어쩔수없이 전체적인 쿼리를 직접 써야한다. 그렇지만 그런 경우는 많이 없다. 


    어쨋든 앞에서 페이징 쿼리에 대해서 설명했기 때문에 여기서 쿼리에 대해서 설명하는것은 넘어가도록 하겠다. 


    그럼 마지막으로 pagingPre와 paginPost를 만들자. 

    첨부파일을 할때 Common_SQL.xml을 만들었었다. 이 xml은 공통적으로 사용되는 쿼리를 모아놓은 부분이기 때문에 여기에 paginPre와 pagingPost를 작성하면 된다.

    Common_SQL.xml을 열고 다음을 작성하자. 

    <sql id="pagingPre">
    	<![CDATA[
    		SELECT 
    			AAA.*
    		FROM(
    			SELECT 
    				COUNT(*) OVER() AS TOTAL_COUNT,
    				AA.*
    			FROM(  
    	]]>
    </sql>
    
    <sql id="pagingPost">
    	<![CDATA[
    			) AA
    		) AAA
    		WHERE 
    			AAA.RNUM BETWEEN #{START} AND #{END}
    	]]>
    </sql>
    

    앞에서 페이징 쿼리에 대해서 설명했던 부분들이다. 따라서 특별한 설명은 하지 않겠다. 


    5. 결과 확인

    여기까지 작성하면 끝이다. 이제 결과를 확인해보자. 


    앞에서 본 결과 화면이다. 최신 글부터 15개의 글이 보이고 하단에 페이징 태그도 보인다. 

    다음으로는 이클립스의 로그를 살펴보자.

    selectBoardList 쿼리를 수행할 때, 앞뒤로 페이징 쿼리가 붙어있는 것을 확인할 수 있다. 그리고 시작과 종료값이 1, 15로 되어있는 것도 확인할 수 있다. 


    그 외에 다른 페이지도 정확히 호출되는지 살펴보자.

    8을 클릭하여 8번 페이지를 호출하였다. 8번에 해당하는 페이지가 조회된 것을 확인할 수 있다. 

    쿼리 역시 106~120번 행을 가져오도록 계산되어 있는것을 확인할 수 있다. 


    [마지막]을 클릭하였다. 게시글의 마지막을 보여주면서 31~34까지의 태그가 정상적으로 나오는 것을 확인할 수 있다.


    ------------------------------------------------------------------------------------


    이렇게해서 전자정부 프레임워크를 사용하는 페이징을 살펴봤습니다. 


    기존의 페이징에 관련된 다른 포스팅과 비교해서 가능한 공통정인 부분을 모아서, 실제 개발을 할때는 최대한 기존 소스의 변경이 없도록 한 부분을 보실수 있을듯 합니다. 


    물론 이것도 100% 완벽한 방법은 아니기 때문에 좀 더 발전시킬 수도 있을것이구요. 


    그리고 여기서는 단순히 글자로 된 페이징 태그만 이야기를 했는데, 전자정부 프레임워크는 이미지 형식으로도 바꿀수 있도록 되어있습니다. 또 스타일을 변경할 수도 있구요. 


    이 글에서는 전자정부 프레임워크를 어떻게 사용하는지에 대해서 이야기를 하는게 아니기때문에 해당 내용에 대해서는 포스팅을 하지 않으려고 합니다. 전자정부프레임워크 홈페이지에도 어떻게 하면 되는지 설명이 되어있기도 하니까요.


    그럼 다음글에서는 jQuery와 Ajax를 이용해서 페이징을 하는것을 이야기하도록 하겠습니다.


    first.zip



    댓글 123

    • 이전 댓글 더보기
    • 대학생 2016.02.17 00:53

      Request processing failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.IncompleteElementException: Could not find SQL statement to include with refid 'common.pagingPre'] with root cause
      java.lang.IllegalArgumentException: XML fragments parsed from previous mappers does not contain value for common.pagingPre

      라는 에러가 뜨는데 이건 어떤 문제일까요? 태그 라이브러리 추가나 AbstractDATO 문제나 쿼리 문제는 아니라고 생각하고 있습니다.

      • 와니 2016.02.18 19:53

        대학생이라면 해석이 가능할겁니다. 에러에 상세하게 어떤 문제인지 나와있네요.

    • gazelle88 2016.02.19 19:23

      최고입니다!!!감사해요 항상

    • 초보인대 2016.03.03 14:36

      안녕하세요 늘 감사하게 수업 잘 보고 있습니다.
      페이징 중에 어떻게 해야할지 몰라 글 남깁니다.
      AbstractDAO에서 설정한게 전혀 돌아가지 않는 것 같습니다.
      함수 내 로그도 안찍히고, jsp에서도 안뽑혀옵니다.
      함수가 아예 안돌아가는것같아요. 이럴때 어디를 봐야할까요?ㅜㅜ
      답변주시면 감사하겠습니다

      --오류도 전혀 뜨지않습니다 ㅜㅜ
      --중간까지 했습니다.

      • 나도초보 2016.03.10 13:29

        저도 같은현상이였고 다른이유에서 일지도 모르지만 저같은 경우는 DAO.java에서 리턴을 return (Map<String, Object>;)selectList("sample.selectBoardList", map); 이렇게 수정 전 소스였습니다..
        return (Map<String, Object>;)selectPagingList("sample.selectBoardList", map); 이렇게 수정하니 페이징 처리 되어 나옵니다~

      • 받아먹기전문 2016.06.15 14:00

        전자정부 프레임워크를 사용하면서 메이븐 충돌이 일어난 것 같습니다. Build Path 에 log4j 라이브러리확인 하시고 충돌나는 lib가 있을겁니다.
        서버기동시 콘솔상단에 Multiple ~~ 경로 따라가셔서 충돌나는 것을 하나 지우시고 메이븐 update 해보세요.

    • 초보개발인데손이개발 2016.03.10 16:01

      안녕하세요..
      이번에 자바를 공부하면서 진짜 너무나도 큰 도움이 되고 있어요.
      정말로 감사의 인사를 드립니다.

      다름이 아니라 페이징 관련하여 질문이 있어서 이렇게 질문을 드려요.
      제가 지금 중견 회사에 인턴으로 들어와서 자바(뿐만이 아니라 프로그래밍 자체를)를 처음으로 연수를 받고 있는데요..

      지금 제가 받은 과제가 더미 데이터를 토대로 프로젝트 별 연말 정산을 자바로 처리를 하여 웹으로 띄워야합니다.
      연말 정산의 방식은 프로젝트 내의 총결산 데이터가 없고 해당 프로젝트의 사원들을 사원 테이블에서 불러와서 일일히 계산 및 처리를 해야해요..
      여기까지는 별 문제가 없었는데..
      연도마다 다르기는 하지만 몇십건 되는 프로젝트가 있는 해의 경우엔 페이징으로 처리를 할려고 합니다.

      그런데 이번 코드 찬찬히 살펴보고 뜯어봤는데..
      DB의 SQL문으로 ROW_NUMBER를 선언을 하여 그걸 기준으로 페이징을 하는걸로 이해를 했어요.
      (이걸 이해하는데 이것저것 삽질하며 꼬박 4일이나 걸렸네요...ㅠㅠ
      덕분에 글쓴님의 위에 적어주신 abstractDAO쪽 페이징 관련 코딩 다 외워버림..이해도 아직 다 못했지만 그래도 좋은게 좋은거겠죠 허허..)

      어쨋든 지금 제가 짠 코딩의 방식으로는 하나하나의 컬럼을 불러와서 자바에서 가공을 하고, 그리고 결과값을 DB가 아닌 웹페이지로 전송하는 방식이라..
      그러다 보니 자바에서 가공한 결과물엔 당연히 RNUM이라는 변수가 안들어가구요..

      이럴 경우에는 어떻게 페이징 처리를 해야할지가 난감해서 질문 드립니다.
      어떤게 제일 베스트이고 효율적인지도.. 될지 안될지도 지금 불명확하고..
      무엇보다도 4일 밤낮을 새면서 고민을 했더니 .... 의욕과 의지가...

      쪼끔 길어졌네요. 간단히 요약하면..
      1. DB에서 불러온 값이 아닌 비연동으로 자바 내에서 처리한 결과를 페이징을 처리하는 방법.
      - 자바 service 내에서 int rnum = 0; 을 선언하여 for문안에서 처리를 하며 맵에 넣어도 abstractDAO쪽에서 인식을 해 줄까요?
      - 그게 아니라면 더 좋은 방식이 있을까요?

      사설이나 질문 내용이 길기도 하고 너무 초보적인 질문같아 부끄럽고 죄송해요...ㅠㅠㅋ

      • 초보인데손이개발 2016.03.18 11:53

        넵...결국 스스로 해결(?)이랄까 다른 방향이지만 원하는 결과를 냈습니다..ㅠㅠ
        결론을 말씀드리자면 자바로 재가공한 결과값을 프로젝트별 연말 정산만을 관리하는 신규 테이블을 생성, insert를 하여 다시 추출하는 형식으로 만들었습니다.
        아래 페이지 넘버를 내는 것까지는 성공을 했으나 페이지 내의 목록을 보여주는 START&END 변수가 지정이 안되어있어서 모든 목록이 뜨는 불상사가 발생을 했어요.
        사실 파라미터 값으로 해당 변수들을 주고 받고 하는 형식으로 하면 해결이 될 문제였지만..
        그럴 경우 코딩 자체도 길어질 뿐더러 개인적으로 너무 더러워진다랄까.. 마음에 안들어서 차라리 테이블을 하나 더 생성을 했네요..
        하지만 기존의 코딩 자체를 갈아엎는 수준..까지는 아니더라도 재코딩 해야하는 부분이 상당했어서 시간이 오래 걸린건 함정..
        어쨋든 감사합니다 :)

    • 서버개발자 2016.05.19 10:25

      안녕하세요. 감동적인 포스팅 잘 봤습니다~^^
      문의 드릴게 있어서요~
      프로젝트 이름을 first > 다른 프로젝트명으로 변경 시 프로젝트 전체적으로 에러가 나는데..아무래도 Maven Dependencies가 사라지면서 오류가 나는거 같아요..혹시 왜이런지 아시나요?프로젝트명을 바꿀 수 있는 방법이 있을까요..

    • 서버개발자 2016.05.19 10:27


      안녕하세요. 감동적인 포스팅 잘 봤습니다~^^
      문의 드릴게 있어서요~
      프로젝트 이름을 first > 다른 프로젝트명으로 변경 시 프로젝트 전체적으로 에러가 나는데..아무래도 Maven Dependencies가 사라지면서 오류가 나는거 같아요..혹시 왜이런지 아시나요?프로젝트명을 바꿀 수 있는 방법이 있을까요..
      아 그리고 또 한가지..페이징으르 전자정부 프레임워크를 사용했는데..
      이러면 전자정부 프레임워크 프로젝트라고 할 수 있는 건가요?
      전자정부 프레임워크 프로젝트라고 할려면..첨부터 전자정부 프레임워크의 기본틀을 가져다 써야...그렇게 정의 할 수 있는걸까요..

      • 세상살이 2016.05.20 15:41

        프로젝트 명을 바꾸시려면 바꾸실것들이 좀 있죠. resource>config>spring에 있는 설정 파일 안에 있는 것들을 해당 프로젝트 명으로 다 바꾸세요. 특히 context-common.xml 에 보시면 <context:component-scan base-package="first"> 여기서 first를 해당 프로젝트로 바꾸시는거 잊지 마시구요. WEB-INF>config 에 있는 web.xml 안에 있는 내용을 바꾸시는것도 역시...

      • 받아먹기전문 2016.06.15 13:53

        일일이 수작업으로 다 바꾸는 것보다 이클립스 기능중에 일괄적으로 다 바꿔 주는 것들이 있습니다. 한번 실행시켜주시고 환경설정부터 꼼꼼하게 리네이밍해보세요. 그리고 전자정부 프레임워크는 말그대로 프레임워크를 전자정부 프레임워크를 쓰는 것입니다. 서버개발자님이 말씀하신 것은 전자정부프레임워크의 컴포넌트를 가져다 쓰는 개념이겠네요.

    • 피곤해요 2016.05.19 16:41

      안녕하세요
      이리저리 시행착오 겪으며 잘 따라왔는데 전자정부프레임워크 페이징 처리에서 DAO에서 AbstractDAO의 selectPagingList로 호출이 안되고 selectList쪽으로 자꾸 호출이 됩니다.
      디버깅을 계속해봐도
      @SuppressWarnings("unchecked";)
      public Map<String, Object> selectBoardList(Map<String, Object> map) throws Exception{
      return (Map<String, Object>;)selectPagingList("sample.selectBoardList", map);
      }
      이부분에서 exception으로 바로 빠져버리면서 selectList쪽으로 넘어가고

      심각: Servlet.service() for servlet [action] in context with path [/first] threw exception [Request processing failed; nested exception is org.springframework.jdbc.UncategorizedSQLException: Error setting null for parameter #1 with JdbcType OTHER . Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. Cause: java.sql.SQLException: 부적합한 열 유형: 1111
      ; uncategorized SQLException for SQL []; SQL state [99999]; error code [17004]; 부적합한 열 유형: 1111; nested exception is java.sql.SQLException: 부적합한 열 유형: 1111] with root cause
      java.sql.SQLException: 부적합한 열 유형: 1111

      이 에러로 빠져버립니다. 비슷한 에러사항 해결하신 분 계시면 꼭좀 피드백 부탁드립니다ㅠ

      • 세상살이 2016.05.20 16:03

        http://oraclejavanew.kr/bbs/board.php?bo_table=LecEnterprise&wr_id=101 여기에 답 나와있네요

    • 개붕 2016.05.23 15:10

      강좌 보면서 많이 배웠습니다. 감사합니다^^

    • 초보삽질 2016.06.07 11:26

      MYSQL - TB_BOARD 데이터 생성 쿼리문
      ==========================
      DELIMITER $$
      CREATE PROCEDURE myFunction()
      BEGIN
      DECLARE i INT DEFAULT 1;
      WHILE (i < 501) DO
      INSERT INTO TB_BOARD(TITLE, CONTENTS, HIT_CNT, DEL_GB, CREA_DTM, CREA_ID) VALUES(concat('제목',i), concat('내용',i), 0, 'N', SYSDATE(), 'Admin');
      SET i = i + 1;
      END WHILE;
      END$$
      DELIMITER ;
      ==================
      쿼리문 호출
      ==========
      CALL myFunction();
      ===============
      쿼리문 삭제
      ================
      DROP PROCEDURE myFunction;

    • 최유진 2016.06.24 16:22

      혹시 my sql에서는 페이징 쿼리문이 어떻게 되나요?

    • 김정두 2016.07.21 10:57

      안녕하세요 잘 보고 있습니다.
      다름이 아니라 랜더방식을 이미지랜더로 해보고 싶은데
      전자정부 프레임워크 들어가보니 이미지랜더를 구현해야 하는거 같더라고요
      초밥이라 그러는데 혹시 이미지랜더 하는 방식같은거 가이드하실 생각 없으신가요 ㅠ

    • 상실 2016.07.21 15:47

      안녕하세요 너무 감사히 잘 보고 있습니다.
      현재 개발자인데 원리도 잘 모르고 마냥 쓰고 있었네요 ㅠ.ㅠ
      주옥같은 강의 앞으로도 계속 찾아보겠습니다. 감사합니다.

      참고:
      페이징 쿼리 pre, post 부분 mysql 적용한 사례 첨부드립니다.
      <sql id="pagingPre">
      <![CDATA[
      select @i as TOTAL_COUNT, AAA.*
      from(
      select
      @i := @i + 1 as RNUM
      , AA.*
      from(
      ]]>
      </sql>

      <sql id="pagingPost">
      <![CDATA[
      ) AA, (select @i := 0, @j := 0) temp
      ) AAA
      where AAA.RNUM limit #{START}, #{END}
      ]]>
      </sql>

      • SpringWorld 2017.01.20 02:43

        mysql쓰시는분들 추가로 참고하세요
        위와같이 적용했을경우 mysql은 selectBoardList쿼리문에 ROW_NUMBER() OVER (ORDER BY IDX DESC) RNUM, 없는상태로 진행하셔야하고요(*mysql은 ROW_NUMBER() OVER가 없습니다)
        AbstractDAO에서 selectPagingList에 end변수에 강제로 15(한페이지갯수)를 주셔야해요 이유는 limit #{START}, #{END}에서 #{END}가 늘어나면안되요ㅎ 혹시 모르시는분들있을까봐 추가로 설명붙입니다.

      • 지나가던mysql 2017.07.19 15:54

        mysql을 사용하는데요
        해당 쿼리문을 적용뒤에 TOTAL_COUNT 가져오는 부분에서 534.0 이런형태로 나값을 가져오는것 같아서요 Integer.parseInt 로 형변환을해도 정수가 아니라서 데이터형 에러가 나는것 같습니다..

        paginationInfo.setTotalRecordCount(Integer.parseInt(list.get(0).get("TOTAL_COUNT";).toString()));
        ㅜㅜ

      • 지나가던mysql 2017.07.20 19:17

        mysql db에서 TOTAL_COUNT를 받아올때 String 데이터형으로 받아오는데 토탈페이지수 + .0 이 붙어서 문제가 됐었는데 해당부분을 Float.parseFloat 형변환해서 (int) 캐스팅하니 정수값이 나오네요..ㅠ

    • 어렵네요 2016.09.05 16:14

      안녕하세요. 덕분에 아주 열심히 스프링 공부를 하고 있습니다.
      지금까지 문제없이 잘 따라서 해보고 있는데
      한가지 에러가 있어서 문의좀 드릴려고요...

      AbstractDAO.java 클래스에서
      -- the method isEmpty(object) is undefined for the type stringutils --
      이렇게 isEmpty 에서 에러가 나는데요... 이건 어떻게 해결해야될지 도저히 찾을 수가 없어서요.
      지금 현재 이클립스 neon 이고요. jdk 1.8버전에 mysql 쓰고 있습니다.
      여기서 갑자기 막혀서 넘 답답해요 ㅠㅠ 도와주십시요^^

      • 스프링고우 2016.09.13 13:17

        StringUtils 가 다른 걸로 임포트 된듯 싶습니다.

        import org.springframework.util.StringUtils; 이걸로 임포트 해보시고 실행 한번 해보세요

      • 까비르 2016.10.05 15:02

        우선 StringUtils객체의 isEmpty 메소드는..
        String 형의 데이터가 와야합니다..

        소스에 map.get("currentPageNo";)의경우.
        확인해보시면 Object 를 리턴하고 있습니다.. 형이 달라서 오류가 나는거구요. 결국 Object to String 으로 변환을 해주시면 오류가 잡힙니다.. .toString(); 을 하시거나
        (String) map.get("currentPageNo";) 으로 형변환하시면 오류가 잡힐껍니다. 오류는 이클립스에서 보면 이미 다알려주고있습니다... 왼만한 오류는 자세히 보는 습관을 들이는것이좋습니다.

      • Rick 2017.08.25 12:14

        isempty에러라면 springframwork 버전이 안맞아서 발생할수도 있습니다.
        저도 3.1.1에는 isempty가 없어 오류발생해서 상위버전으로변경하고나니 잘되네요

    • 로그 안뜨시는분 2016.11.09 13:34

      <dependency>
      <groupId>egovframework.rte</groupId>
      <artifactId>egovframework.rte.fdl.property</artifactId>
      <version>2.7.0</version>
      </dependency>
      2.7버전으로 해야뜹니다.
      3.0 이상은 log4j2로 구현해야하더군요.

      • 2.7.0 빈설정 2017.03.06 20:37

        2.7.0 은 common.xml 파일에서 빈설정은 어떻개 해주었나요?
        2.7.0 버전의 jar 파일을 까봤는데 페이징 관련 클래스는 따로 찾지를 못해서요. 2.7.0 버젼으로 페이징 구현하셨다면 알려주세요. 3.5.0 버전은 로그가 보이지 않아서요

    • new1mm 2016.12.05 18:31

      안녕하세요 submit()에서 var frm = $("#"+this.formId)[0]; 이 값을 오ㅐ 해주는 건가요? 어떤 form 을 가리키는지 설정을 해주는 건가요?

    • 뒹구르르 2017.01.26 10:40

      따라 해서 안되는 부분이 있었는데 댓글들 보고 참조해서 해결했네요 ㅋㅋ 왠만한 교과서보다 나은 것 같아요 감사합니다 주인장님!! ㅋㅋ

    • 흰독수리 2017.02.24 16:41

      정말 도움을 많이 받고 있습니다.
      paginationInfo 영역에 전페이지, 이전페이지 등 화살표 이미지가 보이지 않는데요.
      왜그러는지 도무지 알수가 없습니다.
      어떤부분이 잘못된건지요?
      paginationInfo

      • 돌겟당 2017.08.01 10:20

        저랑 같네요 ㅠ 진짜 이틀내내봤는데 도저히 모르겠네요. 혹시 해결법 찾으셨나요?

        다음 jquery, ajax로 해도 마찬가지네요 첫페이지는 잘 나오는데...

      • 돌겟당 2017.08.01 11:33

        DB2를 사용해서 COUNT OVER가 제대로 안먹혀서 다르게 사용했더니 TOTAL_COUNT를 잘못된 값으로 가져와서 생기는 문제였네요. ㅎㅎ LEFT JOIN으로 쿼리를 바꾸고 해결했습니다.

    • Favicon of http://none BlogIcon Rick 2017.08.25 15:02

      지나가던mysql님 어디부분 수정하셨나요? 저도 같은문제로 지금 속앓이하고있는중이에요 ㅠㅠ

      • 지나가던mysql 2017.09.15 09:16

        AbstractDAO.java 파일안에

        paginationInfo.setTotalRecordCount(Integer.parseInt(list.get(0).get("TOTAL_COUNT";).toString()));



        paginationInfo.setTotalRecordCount((int) Float.parseFloat(list.get(1).get("TOTAL_COUNT";).toString()));

        이렇게 했던부분이였네요 도움이 되시길~ 캐스팅부분이 제대로 안됐는데 아직도 이유는 잘모르겠어요 ㅠ

    • 초보개발자 2018.05.14 13:41

      저도 mysql로 실습했었는데 많은 분들께서 같은 문제로 고민하신 것 같아 수정안 공유합니다.
      <selectBoardList 쿼리 부분>
      SELECT *
      FROM (
      SELECT COUNT(*) AS TOTAL_COUNT
      FROM TB_BOARD
      WHERE DEL_GB = 'N'
      ) A CROSS JOIN TB_BOARD
      WHERE DEL_GB = 'N'
      ORDER BY IDX DESC
      LIMIT #{LIMIT}
      OFFSET #{OFFSET}
      --------------------------
      설명

      결과가 COUNT(*) 하나만 나오는 테이블 / 원래 있던 테이블을 크로스 조인하여 새 테이블을 만듭니다. LIMIT은 한 페이지에 나올 수 있는 아이템의 최대 개수, OFFSET엔 몇 번째 인덱스부터 시작할 것인지를 명시하면 됩니다.

      AbstractDAO 클래스의 selectPagingList 메소드도 맞춰서 수정해야 합니다.
      1. int start 변수는 필요없으니 제거합니다.
      2. int start, int end 선언 아래쪽에서 map에 아이템을 넣는 부분을 전부 지우고 아래처럼 수정합니다.
      map.put("LIMIT", end); // LIMIT를 end(15)로 고정함
      map.put("OFFSET", (paginationInfo.getCurrentPageNo() - 1) * end);
      // 현재 페이지 - 1과 end를 곱하면 offset이 나옴. 1페이지 -> 0, 2페이지 -> 15, 3페이지 -> 30, ...

      더 좋은 방법 있으신 분은 같이 공유해주셨으면 좋겠습니다.

    • spring프링프링 2019.04.17 15:45

      postgreSQL 사용중이구 모든 과정을 따라 했습니다만...
      이러한 오류가 계속 뜨네요...
      로그에는 찍히는데...jsp화면이 뜨질 않아요..ㅠ

      2019-04-17 13:54:14,427 DEBUG [first.common.logger.LoggerInterceptor] ====================================== START ======================================
      2019-04-17 13:54:14,427 DEBUG [first.common.logger.LoggerInterceptor] Request URI : /first/sample/openBoardList.do
      2019-04-17 13:54:14,438 DEBUG [first.common.dao.AbstractDAO] QueryId : starkoff.selectBoardList
      2019-04-17 13:54:14,441 INFO SQL : SELECT
      AAA.*
      FROM(
      SELECT
      COUNT(*) OVER() AS TOTAL_COUNT,
      AA.*
      FROM(



      SELECT
      ROW_NUMBER() OVER (ORDER BY IDX DESC) RNUM,
      IDX,
      TITLE,
      HIT_CNT,
      CREA_DTM
      FROM wpschema.TB_BOARD
      WHERE DEL_GB = 'N'



      ) AA
      ) AAA
      WHERE
      AAA.RNUM BETWEEN 1 AND 15
      2019-04-17 13:54:14,444 INFO [jdbc.resultsettable] |------------|-----|----|------|--------|---------------------------|
      2019-04-17 13:54:14,445 INFO [jdbc.resultsettable] |total_count |rnum |idx |title |hit_cnt |crea_dtm |
      2019-04-17 13:54:14,445 INFO [jdbc.resultsettable] |------------|-----|----|------|--------|---------------------------|
      2019-04-17 13:54:14,445 INFO [jdbc.resultsettable] |3 |1 |3 |제목 |0 |2019-04-16 10:17:17.330275 |
      2019-04-17 13:54:14,445 INFO [jdbc.resultsettable] |3 |2 |2 |제목 |0 |2019-04-16 10:09:04.702467 |
      2019-04-17 13:54:14,445 INFO [jdbc.resultsettable] |3 |3 |1 |제목 |0 |2019-04-16 10:08:48.709918 |
      2019-04-17 13:54:14,445 INFO [jdbc.resultsettable] |------------|-----|----|------|--------|---------------------------|
      4월 17, 2019 1:54:14 오후 org.apache.catalina.core.StandardWrapperValve invoke
      SEVERE: Servlet.service() for servlet [action] in context with path [/first] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause
      java.lang.NullPointerException
      at first.common.dao.AbstractDAO.selectPagingList(AbstractDAO.java:100)
      at first.board.BoardDAO.selectBoardList(BoardDAO.java:15)
      at first.board.BoardDAO$$FastClassByCGLIB$$54df3ff2.invoke(<generated>;)
      at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
      at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:698)
      at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
      at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:91)
      at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
      at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:631)
      at first.board.BoardDAO$$EnhancerByCGLIB$$46acf5aa.selectBoardList(<generated>;)
      at first.board.BoardServiceImpl.selectBoardList(BoardServiceImpl.java:32)
      at first.board.BoardController.openBoardList(BoardController.java:28)
      at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
      at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
      at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.base/java.lang.reflect.Method.invoke(Method.java:566)
      at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:219)
      at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
      at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
      at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:745)
      at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:686)
      at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
      at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:925)
      at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856)
      at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:936)
      at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:827)
      at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
      at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:812)
      at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
      at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
      at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
      at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
      at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
      at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
      at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
      at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
      at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
      at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
      at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
      at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
      at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)
      at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
      at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
      at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650)
      at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
      at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
      at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)
      at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
      at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)
      at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)
      at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
      at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
      at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
      at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
      at java.base/java.lang.Thread.run(Thread.java:834)

Designed by Tistory.