Paging
말 그대로 페이지를 넘기게 해주는 기술로
게시판의 경우에 사용한다.
한 페이지당 보여줄 게시글 수를 넘어가면 2번,3번....페이지로 페이지 이동이 일어나는 기술을 말한다.
단순히 UI화면에서의 편리성의 목적보다
DB사용에 있어서 효율성을 위해서 사용한다.
한 번에 많의 양의 게시글들을 매번 많은 사람들이 가져오게 되면 DB사용량이 증가하게 되고
요청에 대한 처리 속도가 느려지므로
응답속도 또한 느려지게 된다.
이게 바로 사용자의 불편함으로 연결된다.
따라서, UI의 편리성과 더불어 DB의 효율성을 높여 사용자의 만족감을 주기 위해 Paging기술은 꼭 필요하다.
1.
com.myweb.domain에 Criterion(기준)이라는 이름으로 class생성.
이 클래스에서 페이지에 대한 기준을 정한다.
변하는 것들 말고 무조건 있어야 하는것들을 객체화 하는것으로
변하는 부분에 대해서는 따로 PagingVO클래스를 만들어 준다.(2번내용)
- 한 페이당 보여줄 글의 갯수(amount)
- 내가 현재 보고 있는 페이지 번호(pageNum)
2.
com.myweb.domain에 PagingVO class생성.
Paging시 변하는 것들을 선언해 주는 부분.
- 전체게시글 갯수 (totalCount)
- 페이징라인의 가장 앞번호 (beginPagingNum)
- 페이징라인의 가장 뒷번호 (endPagingNum)
- 앞 페이지가 있느냐 없느냐 (private boolean prev, next)
- 페이징 구현을 위한 기준 객체 (private Criterion cri)
3. endPagingNum
먼저 endPagingNum에 대한 커스터마이징이 필요하다.
예로, Pagination 부분에서 한번에 보여지는 페이지수(amount)가 10이라고 가정하면
endPagingNum은
1/10.0 = 0.1
2/10.0 = 0.2
3/10.0 = 0.3
.
.
.
9/10.0 = 0.9
10/10.0 = 1.0
에서
내가 가질 수 있는 최대 정수를 나태내주는 Math.ceil()을 해준다.
그러면 위의 경우는 모두 1.0이 되고,
1.0을 int로 해서 정수 1로 만들어 준 후
*10을 하면
1~10까지 어떤 페이지를 눌러도 10이라는 endPaginNum이 만들어진다.
this.endPagingNum = (int)(Math.ceil(cri.getPageNum()/10.0))*10;
4.beginPagingNum
시작페이지는 엔딩페이지에서 -9를 해주면 된다.
this.beginPagingNum = this.endPagingNum - 9;
5. realEndPagingNum
진짜 끝 페이지를 의미한다.
- 첫번째 방법
totalCount가 121이라고 하면
realEndPagingNum은 121/10.0 = 12.1
여기서 ceiling하면 13.0
int 하면 13!!!
int realEndPagingNum = (int)(Math.ceil((totalCount/10.0)));
- 두번째 방법
int realEndPagingNum = (int)(Math.ceil((totalCount*1.0)/cri.getAmount()));
6.
endPagingNum과 realPaginaNum의 괴리해결
총 totalCount가 121라고 할 때
1~10까지의 endPagingNum = 10으로
realPagingNum을 넘지 않지만
11~는
endPagingNum이 20으로 실제 DB의
realPagingNum인 13을 넘게 되므로
이 괴리를 해결하는 코드가 필요하다.
계산된 마지막 페이징 번호에 실제 존재 할 수 있는 마지막 페이징 번호를 대입
if(this.endPagingNum >= realEndPagingNum) {
this.endPagingNum = realEndPagingNum;
}
7.
이전페이지(prev)와 다음페이지(next)가 있어야 하는 경우
this.prev = beginPagingNum > 1;
this.next = this.endPagingNum < realEndPagingNum;
8. JUnitTest(참고사항)
JUnitTest에서는 서버를 사용하지 않고 지금 코드에서의 구동 여부를 알아 볼 수 있다.
먼저, 테스트를 위해 src/test/java/com/myweb/ctrl에 DummyInsert 클래스를 만든다.
DummyInsert는 JUnitTest를 하는 용도의 클래스이다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class DummyInsert {
private static Logger log = LoggerFactory.getLogger(DummyInsert.class);
@Inject
private ProductDAO pdao;
@Test
public void insertProductDummy() {
for (int i = 0; i < 255; i++) {
ProductVO pvo = new ProductVO();
pvo.setTitle(i + "번째 상품명");
pvo.setWriter("admin@admin.com");
pvo.setContent(i + "번째 상품 상세정보");
pvo.setPrice(i + 10000);
pvo.setImgfile("NONE");
pdao.insertProduct(pvo);
}
}
}
9.
Bootstrap에서 Pagination의 Ex를 list.jsp에 복붙.
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Prev</a></li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
10.
ProductCtrl에 cri객체를 사용하겠다고 선언!!
cri를 DB로 보내기 위해(Ctrl단을 거쳐서 DB로 가기때문에) getList에 parameter로 던진다.
@GetMapping("/list")
public void list(Model model, Criterion cri) {
model.addAttribute("pList", psv.getList(cri));
}
이 parameter는 ProductDAO -> ProductDAOImpl -> ProductService -> ProductServiceImpl 순으로 받아오기때문에
그 반대의 순서인 ProductService -> ProductServiceImpl -> ProductDAO ->ProductDAOImpl 순으로
getList()메소드에 Criterion객체를 던져준다.
//ProductService.java
public List<ProductVO> getList(Criterion cri);
//ProductServiceImpl.java
@Override
public List<ProductVO> selectList(Criterion cri) {
return sql.selectList(ns+"list", cri);
}
//ProductDAO.java
public List<ProductVO> selectList(Criterion cri);
//ProductDAOImpl.java
@Override
public List<ProductVO> selectList(Criterion cri) {
return sql.selectList(ns+"list", cri);
}
11.
ProductCtrl에서
cri가 db에 가서 10개만 가져와서 Plist의 이름으로 받고
list.jsp에 뿌려지기 위해 pgvo의 이름으로 PagingVO객체를 받는다.
@GetMapping("/list")
public void list(Model model, Criterion cri) {
model.addAttribute("pList", psv.getList(cri));
int totalCount = psv.getTotalCount();
model.addAttribute("pgvo", new PagingVO(totalCount, cri));
}
int totalCount를 생성하여 Service단으로부터 getTotalCount()메소드를 받게 하고
순서에 따라 ProductService -> ProductServiceImpl -> ProductDAO ->ProductDAOImpl 순으로
메소드를 생성하고 Criterion객체도 파라미터로 던져준다.
//ProductService.java
public int getTotalCount(Criterion cri);
//ProductServiceImpl.java
@Override
public int getTotalCount(Criterion cri) {
return pdao.selectTotalCount(cri);
}
//ProductDAO.java
public int selectTotalCount(Criterion cri);
//ProductDAOImpl.java
@Override
public int selectTotalCount(Criterion cri) {
return sql.selectOne(ns+"totalCount", cri);
}
후에 Mapper에서 pno의 갯수(count(pno) = totalCount)를 조회하여 전체를 몇 개로 페이징 할지 결정한다.
//productMapper.xml
<select id="totalCount" resultType="java.lang.Integer">
select count(pno) from tbl_product
</select>
12.
페이징 방식은 db에서 총게시글(totalCount)을 가져와서 java에서 1~10, 11~20 이렇게 가져오는게 아니라
첫번째 페이지, 두번째 페이지 선택해서DB로 보내면 DB에서 그 해당하는 게시글만 가져오는 방식이다.
따라서 query문의 수정이 불가피하다.
내가 선택한 페이지에 의해 연산.
mysql은 limit연산자로 쉽게 할 수 있지만
oracle에서는 rowNum(order by보다 먼저 생성되는 특성)때문에 subquery문을 이용해서 페이징 해야한다.
Oracle rownum과 Subquery이용한 Query문의 이해가 필요하다.
Oracle에서 Subquery이용하여 페이징할때는 속도가 느려지고 정확도가 떨어지기 때문에 OrderBy는 잘 사용하지 않는다.
대신 주석으로 /*+INDEX_DESC(tbl_product pk_product)*/ 을 이용하여 최신순부터 순서대로 뽑을 수 있게 해준다.
(주석을 참고하여 반영하는 특성이 있다.)
여기서 <![CDATA[...]]>는 Query문의 '<','>','&' 이런요소를 태그로 읽지 않고 문자 자체로 인식해주는 태그이다.
<select id="list" parameterType="Criterion" resultType="ProductVO">
<![CDATA[
select pno,title,writer,readcount,modd8,imgfile
from(select /*+INDEX_DESC(tbl_product pk_product)*/
rownum as rn,pno,title,writer,readcount,modd8,imgfile
from tbl_product
where rownum <= #{pageNum}*{amount})
where rn > (#{pageNum-1) * #{amount}
]]>
</select>
13. list.jsp 빌드
next 클릭시 1~10중 어는 곳에 있든지 11번페이지로 넘어가야 한다.
따라서 pageNum${pgvo.endPagingNum + 1 }로 pageNum을 해준다.
prev클릭시 11~12 어느 곳에 있어도 10번페이지로 넘어가야 한다.
따라서 pageNum${pgvo.endPagingNum + 1 }로 pageNum을 해준다.
<ul class="pagination">
<c:if test="${pgvo.prev }">
<li class="page-item"><a class="page-link"
href="/product/list?pageNum=${pgvo.beginPagingNum - 1 }&amount=${pgvo.cri.amount }">Prev</a>
</li>
</c:if>
<c:forEach begin="${pgvo.beginPagingNum }" end="${pgvo.endPagingNum }" var="i">
<li class="page-item ${pgvo.cri.pageNum ==i? 'active':'' }">
<a class="page-link" href="/product/list?pageNum=${i }&amount=${pgvo.cri.amount}">${i }</a>
</li>
</c:forEach>
<c:if test="${pgvo.next }">
<li class="page-item"><a class="page-link"
href="/product/list?pageNum=${pgvo.endPagingNum + 1 }&amount=${pgvo.cri.amount }">Next</a>
</li>
</c:if>
</ul>
14.
지금은 상품목록에서 상품정보로 들어간 다음 다시 상품목록을 누르고 나올때
내가 보고있는 페이지네이션이 아니라 1번페이지로 돌아오게 된다.
사용자에게 매우 불편한 점이므로 해결이 필요한 부분이다.
순서
list -> Productctrl -> detail -> Productctrl ->list
- list.jsp에서 던진다
리스트의 컬럼중 title을 클릭시 detail로 넘어가게 했기때문에
이 부분에서 cri객체의 pageNum과 amount를 들고 ProductCtrl로 이동한다.
<td>
<a href="/product/detail?pno=${pvo.pno }&pSign=0&pageNum=${pgvo.cri.pageNum}&amount=${pgvo.cri.amount}">${pvo.title }</a>
</td>
- ProductCtrl
ProductCtrl에서 ({"/detail" "/modify"})의 주소로 오게 되는데
여기선 아직 cri객체를 사용한다고 선언하지 않았기 때문에
Criterion cri를 Parameter로 던져 오토바인딩 시켜서 detail에서 cri를 쓸 수 있게 준비해준다.
- detail.jsp에서 받는다
ProductCtrl에서 던진 cri객체를 이용하여 pageNum=${cri.pageNum}&amount=${cri.amount} 의 형태로 받는다.
<tr>
<th colspan="2">
<a href="/product/list?pageNum=${cri.pageNum }&amount=${cri.amount}" class="btn btn-success">목록</a>
<c:if test="${sesInfo.email eq pvo.writer || sesInfo.email eq 'admin@admin.com'}">
<a href="/product/modify?pno=${pvo.pno }&pSign=0" class="btn btn-warning">수정</a>
<a href="#" class="btn btn-outline-danger" id="delBtn">삭제</a>
</c:if>
</th>
</tr>
- 다시 ProductCtrl로 돌아간다.
상품에 대한 상세정보를 확인한 뒤 다시 리스트로 돌아갈 때
내가 클릭한 페이지의 리스트로 그대로 돌아가기 위해서 다시 리스트 페이지로 이동할 필요가 있는데
detail.jsp에서 목록부분을 클릭시 일단 ProductCtrl의 ("/list")부분으로 이동하여
pageNum=${cri.pageNum}&amount=${cri.amount}를 던져주게 된다.
- 마지막 list.jsp
이제 리스트 화면을 보여주기 위해 다시 list.jsp로 pageNum=${cri.pageNum}&amount=${cri.amount}가지고 돌아와서
내가 클릭했던 그 페이지가 있는 리스트를 보여주게 된다.
처음에 빌드했던 그대로 받으면 된다.
15.
수정/삭제뒤 다시 리스트 화면으로 돌아올때도 마찬가지로 해당 페이지의 리스트로 돌아오도록
이미 detail.jsp와 PoductCtrl에 이미 cri객체를 받겠다고 Parameter로 빌드 해놨기 때문에 cri를 가져다 쓰기만 하면 되는 간단한 작업이다.
- 수정
따라서 deail.jsp에서 수정 버튼부분에 cri객체의 변수를 던져주면 된다.
<c:if test="${sesInfo.email eq pvo.writer || sesInfo.email eq 'admin@admin.com'}">
<a href="/product/modify?pno=${pvo.pno }&pSign=0&pageNum=${cri.pageNum }&amount=${cri.amount}"
class="btn btn-warning">수정</a>
<a href="#" class="btn btn-outline-danger" id="delBtn">삭제</a>
</c:if>
그럼 PoductCtrl거쳐서 modify.jsp로 이동하여 cri를 받는다.
type=hidden은 보통 post로 보낼때는 주소창에서 해당 값들이 보이지 않게 해주는 것이다.
보안이라고 하기엔 뭣하지만 (보안은 나중에 암호화를 해야하지만) 그래도 주소창에서라도 보이지 않게 해주는 기능인 것이다.
<input type="hidden" name="pageNum" value="{cri.pageNum}">
<input type="hidden" name="amount" value="{cri.amount}">
이제 다시 ProductCtrl의 @PostMapping ("/modify")로 와서 parameter로 cri객체를 던지고 받아준다.
//ProductCtrl
@PostMapping("/modify")
public String modify(MultipartHttpServletRequest multiReq, Criterion cri) {
int isUp = psv.modify(fp.fileModify(multiReq));
return "redirect:/product/detail?pSign=" + isUp
+ "&pno=" + multiReq.getParameter("pno")
+ "&pageNum="+cri.getPageNum()
+ "&amount="+cri.getAmount();
}
*참고사항*
ctrl에서 view로 데이터를 보낼때는 model객체를 사용하고
ctrl에서 java가 처리할 신호들(다시 ctrl로 올때 )은 redirect사용(객체에 싣지 않고 간단하게). - webapp으로 안나가니까
다시 detail.jsp로 와서 cri가 받고 ...반복 : userpathtigger
- 삭제
삭제 또한 detail.jsp에서 delBtn으로 작동하기 때문에 똑같이 cri를 실어서 ProductCtrl로 보내준다.
delBtn을 누르면 delForm이 동작하게 javascript에서 만들어놨기 때문에
delForm에서 hidden의 형태로 cri의 PageNum과 amount를 싣고 ProductCtrl로 보낸다.
<form action="/product/remove" id="delForm" method="post">
<input type="hidden" name="pno" value="${pvo.pno }">
<input type="hidden" name="imgfile" value="${pvo.imgfile }">
<input type="hidden" name="pageNum" value="${cri.pageNum}">
<input type="hidden" name="amount" value="${cri.amount}">
</form>
ProductCtrl에서 Criterion cri객체를 받겠다고 선언하고 list.jsp로 리턴할때 cri에 pageNum과 amount를 같이 보내줘서
list.jsp로 왔을 때 내가 누른 페이지 그대로 나오게 된다.
//ProductCtrl
@PostMapping("/remove")
public String remove(@RequestParam("pno") int pno, @RequestParam("imgfile")String imgfile ,
RedirectAttributes reAttr, Criterion cri) {
int isRm = fp.removeFile(imgfile);
isRm = psv.remove(pno);
if (isRm > 0) {
reAttr.addFlashAttribute("pSign", "삭제가 완료되었습니다");
}
return "redirect:/product/list?&pageNum="+cri.getPageNum()
+"&amount="+cri.getAmount();
}
---------------------------------------------------------------------------------------------------------
이로써 모든 페이징 작업이 끝났다
멀고도 험난한 과정이었지만 배운다는 건 항상 설레는 일이다.
'_Programming > Spring' 카테고리의 다른 글
Spring.Comment (0) | 2020.07.02 |
---|---|
Spring.Search(검색기능) (0) | 2020.07.02 |
Spring.파일첨부(file) (0) | 2020.06.30 |
Spring.classpath (0) | 2020.06.29 |
Sping.Bean (0) | 2020.06.28 |