스프링 - Part 3 - 기본적인 웹 게시물 관리 09 (검색처리)
검색 처리
검색 기능과 SQL
게시물의 검색 기능
- 제목/내용/작성자와 같이 단일 항목 검색
- 제목 or 내용, 제목 or 작성자, 내용 or 작성자, 제목 or 내용 or 작성자와 같은 다중 항목 검색
검색항목은 제목/내용/작성자와 같은 단일 항목 검색과 제목 or 내용과 같이 복합적인 항목으로 검색하는 방식이 존재한다.
단일 학목은 인라인뷰 안쪽에서 필요한 데이터를 가져올 때 검색 조건이 적용되어야 하기 때문에 WHERE문 뒤에 검색 조건이 추가되고, ROWNUM 조건이 뒤따르게 하면 문제가 없다.
다중 항목 검색
2개 이상의 조건이 붙는 다중 항목의 검색이다.
예 ) 제목이나 내용중에 TEST라는 문자열이 있는 게시물들을 검색하고 싶다면 다음과 같이 작성될 것.
select
*
from
(
select /*+INDEX_DESC(tbl_board pk_board) */
rownum rn, bno, title, content, writer, regdate, updatedate
from
tbl_board
where
title like '%Test%' or content like '%Test%'
and rownum <= 20
)
where rn > 10;
MyBatis의 동적 SQL
MyBatis의 동적 태그들
MyBatis는 기존의 iBatis에서 발전하면서 복잡했던 동적 SQL을 작성하는 태그들이 많이 정리되어서 다음과 같이 몇가지의 태그들만 이용한다.
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
검색 조건 처리를 위한 Criteria의 변화
페이징 처리에 사용했던 Criteria의 의도는 단순히 pageNum과 amount라는 파라미터를 수집하기 위해서이다. 페이징 처리에 검색 조건 처리가 들어가면 Criteria 역시 변화가 필요하다.
검색 조건을 처리하기 위해서는 검색 조건(type)과 검색에 사용하는 키워드가 필요하므로, 기존의 Criteria를 확장할 필욕 ㅏ있다.
Criteria 클래스 수정
// Criteria.java
package org.codehows.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class Criteria {
private int pageNum;
private int amount;
private String type;
private String keyword;
public Criteria() {
this(1, 10);
}
public Criteria(int pageNum, int amount) {
this.pageNum = pageNum;
this.amount = amount;
}
public String[] getTypeArr() {
return type == null? new String[] {}: type.split("");
}
}
Criteria 클래스는 type과 keyword라는 변수를 추가한다. getter/setter는 Lombok을 통해서 생성하고, getTypeArr은 검색 조건이 각 글자(T,W,C)로 구성되어있으므로 검색 조건을 배열로 만들어서 한번에 처리하기 위함이다.
getTypeArr()을 이용해서 MyBatis의 동적 태그를 활용 할 수 있다.
BoardMapper.xml에서 Criteria 처리
BoardMapper.xml은 기존의 getListWithPaging()을 수정해서 동적 SQL을 처리한다.
BoardMapper.xml의 검색 및 페이징 처리
// BoardMapper.xml // getListWithPaging 부분 수정
... 생략 ...
<select id="getListWithPaging" resultType="org.codehows.domain.BoardVO">
<![CDATA[
select
bno, title, content, writer, regdate, updatedate
from
(
select /*+INDEX_DESC(tbl_board pk_board) */
rownum rn, bno, title, content, writer, regdate, updatedate
from
tbl_board
where
]]>
<trim prefix="(" suffix=") AND " prefixOverrides="OR">
<foreach item='type' collection="typeArr">
<trim prefix="OR">
<choose>
<when test="type == 'T'.toString()">
title like '%'||#{keyword}||'%'
</when>
<when test="type == 'C'.toString()">
title like '%'||#{keyword}||'%'
</when>
<when test="type == 'W'.toString()">
title like '%'||#{keyword}||'%'
</when>
</choose>
</trim>
</foreach>
</trim>
<![CDATA[
rownum <= #{pageNum} * #{amount}
)
where rn > (#{pageNum} -1) * #{amount}
]]>
</select>
... 생략 ...
테스트 코드를 작성.
src/test/java 밑의 BoardMapperTests 클래스 일부
// BoardMapperTests.java
... 생략 ...
@Test
public void testSearch() {
Criteria cri = new Criteria();
cri.setKeyword("새로");
cri.setType("TC");
List<BoardVO> list = mapper.getListWithPaging(cri);
list.forEach(board -> log.info(board));
}
testSearch()는 Criteria 객체의 type과 keyword를 넣어서 원하는 SQL이 생성되는지 확인하기 위함이다.
<sql> <include>와 검색 데이터의 개수 처리
MyBatis는 <sql>이라는 태그를 이용해서 SQL의 일부를 별도로 보관하고, 필요한 경우에 include시키는 형태로 사용할 수 있다.
BoardMapper.xml의 목록과 데이터 개수 처리
// BoardMapper.xml 부분 수정
<select id="getListWithPaging" resultType="org.codehows.domain.BoardVO">
<![CDATA[
select
bno, title, content, writer, regdate, updatedate
from
(
select /*+INDEX_DESC(tbl_board pk_board) */
rownum rn, bno, title, content, writer, regdate, updatedate
from
tbl_board
where
]]>
<include refid="criteria"></include>
<![CDATA[
rownum <= #{pageNum} * #{amount}
)
where rn > (#{pageNum} -1) * #{amount}
]]>
</select>
<select id="getTotalCount" resultType="int">
select count(*) from tbl_board
where
<include refid="criteria"></include>
bno > 0
</select>
<sql> 태그는 id라는 속성을 이용해서 필요한 경우에 동일한 SQL의 일부를 재사용 할 수 있게 한다.
화면에서 검색 조건처리
화면에서 검색은 다음과 같은 사항들을 주의해서 개발해야 한다.
- 페이지 번호가 파라미터로 유지되었던 것처럼 검색 조건과 키워드 역시 항상 화면 이동시 같이 전송되어야 한다,
- 화면에서 검색 버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동한다,
- 한글의 경우 GET 방식으로 이동하는 경우 문제가 생길 수 있으므로 주의해야한다.
목록 화면에서의 검색 처리
목록 화면인 list.jsp에서는 검색 조건과 키워드가 들어 갈 수 있게 HTML 을 수정해야한다. views 폴더 내의 list.jsp를 수정해서 페이지 처리 바로 위쪽에 아래의 내용들을 추가한다.
list.jsp
// list.jsp
... 생략 ...
</c:forEach>
</table>
<div class='row'>
<div class="col-lg-12">
<form id='searchForm' action="/board/list" method='get'>
<select name='type'>
<option value="">--</option>
<option value="T">제목</option>
<option value="C">내용</option>
<option value="W">작성자</option>
<option value="TC">제목 or 내용</option>
<option value="TW">제목 or 작성자</option>
<option value="TWC">제목 or 내용 or 작성자</option>
</select>
<input type='text' name='keyword' />
<input type='hidden' name='pageNum' value='${pageMaker.cri.pageNum}'>
<input type='hidden' name='amount' value='${pageMaker.cri.amount}'>
<button class='btn btn-default'>Search</button>
</form>
</div>
</div>
... 생략 ...
화면에서는 다음과 같은 모습으로 보여지게 된다.
수정된 HTML을 보면 페이징 처리를 위해서 만들어둔 <form> 태그에 <select>와 <input> 태그가 추가된 것을 볼 수 있다,
<form> 내 <button>의 기본 동작은 submit이므로 별도의 처리 없이 검색이 되는지 확인한다.
검색 버튼의 이벤트 처리
list.jsp의 검색 버튼의 이벤트 처리 ( 스크립트 부분 젤 밑에 추가)
// list.jsp
... 생략 ...
var searchForm = $("#searchForm");
$("#searchForm button").on("click", function(e){
if(!searchForm.find("option:selected").val()){
alert("검색종류를 선택하세요");
return false;
}
if(!searchForm.find("input[name='keyword']").val()) {
alert("키워드를 입력하세요");
return false;
}
searchForm.find("input[name='pageNum']").val("1");
e.preventDefault();
searchForm.submit();
});
브라우저에서 검색 버튼을 클릭하면 <form> 태그의 전송은 막고, 페이지의 번호는 1이 되도록 처리한다. 화면에서 키워드가 없다면 검색을 하지 않도록 제어한다.
검색 후에는 주소창에 검색 조건과 키워드가 같이 GET 방식으로 처리되므로 이를 이용해서 <select> 태그나 <input> 태그의 내용을 수정해야한다.
list.jsp에서 검색 조건과 키워드 보여주는 부분
// list.jsp
... 생략 ...
</c:forEach>
</table>
<div class='row'>
<div class="col-lg-12">
<form id='searchForm' action="/board/list" method='get'>
<select name='type'>
<option value=""
<c:out value="${pageMaker.cri.type == null?' selected':''}"/>>--</option>
<option value="T"
<c:out value="${pageMaker.cri.type eq 'T'?' selected':''}"/>>제목</option>
<option value="C"
<c:out value="${pageMaker.cri.type eq 'C'?' selected':''}"/>>내용</option>
<option value="W"
<c:out value="${pageMaker.cri.type eq 'W'?' selected':''}"/>>작성자</option>
<option value="TC"
<c:out value="${pageMaker.cri.type eq 'TC'?' selected':''}"/>>제목 or 내용</option>
<option value="TW"
<c:out value="${pageMaker.cri.type eq 'TW'?' selected':''}"/>>제목 or 작성자</option>
<option value="TWC"
<c:out value="${pageMaker.cri.type eq 'TWC'?' selected':''}"/>>제목 or 내용 or 작성자</option>
</select>
<input type='text' name='keyword'
value='<c:out value="${pageMaker.cri.keyword}"/>' />
<input type='hidden' name='pageNum'
value='<c:out value="${pageMaker.cri.pageNum}"/>' />
<input type='hidden' name='amount'
value='<c:out value="${pageMaker.cri.amount}"/>' />
<button class='btn btn-default'>Search</button>
</form>
</div>
</div>
... 생략 ...
검색 조건과 키워드에 대한 처리가 되면 검색 후 페이지를 이동해서 동일한 검색 사항들이 계속 유지되는 것을 볼 수 있다.
페이지 번호를 클릭해서 이동할 때에도 검색 조건과 키워드는 같이 전달되어야 하므로 페이지 이동에 사용한 태그를 수정한다.
list.jsp의 일부
// list.jsp
... 생략 ...
<form id='actionForm' action="/board/list" method='get'>
<input type='hidden' name='pageNum' value='${pageMaker.cri.pageNum}'>
<input type='hidden' name='amount' value='${pageMaker.cri.amount}'>
<input type='hidden' name='type' value='<c:out value="${pageMaker.cri.type}"/>'>
<input type='hidden' name='keyword' value='<c:out value="${pageMaker.cri.keyword}"/>'>
</form>
... 생략 ...
검색 조건과 키워드에 대한 처리가 되면 검색 후 페이지를 이동해서 동일한 검색 사항들이 계속 유지되는 것을 볼 수 있다.
조회 페이지에서 검색 처리
목록 페이지에서 조회 페이지로의 이동은 이미 <form> 태그를 이용해서 처리했기 때문에 별도의 처리가 필요하지 않다. 다만 조회 페이지는 아직 Criteria의 type과 keyword에 대한 처리가 없기 때문에 이부분을 수정해 줄 필요가 있다.
view/board/get.jsp의 일부
// get.jsp
... 생략 ...
<form id='operForm' action="/board/modify" method="get">
<input type='hidden' id='bno' name='bno' value='<c:out value="${board.bno}"/>'>
<input type='hidden' name='pageNum' value='<c:out value="${cri.pageNum}"/>'>
<input type='hidden' name='amount' value='<c:out value="${cri.amount}"/>'>
<input type='hidden' name='keyword' value='<c:out value="${cri.keyword}"/>'>
<input type='hidden' name='type' value='<c:out value="${cri.type}"/>'>
</form>
... 생략 ...
수정/삭제 페이지에서 검색 처리
조회 페이지에서 수정/삭제 페이지로 이동은 GET 방식을 통해서 이동하고, 이동 방식 역시 <form> 태그를 이용하는 방식이므로 기존의 <form> 태그에 추가적인 type과 keyword 조건만을 추가한다.
view/board/modify.jsp 의 일부
// modify.jsp
... 생략 ...
<form role="form" action="/board/modify" method="post">
<!-- 추가 -->
<input type='hidden' name='pageNum' value='<c:out value="${cri.pageNum}"/>'>
<input type='hidden' name='amount' value='<c:out value="${cri.amount}"/>'>
<input type='hidden' name='type' value='<c:out value="${cri.type}"/>'>
<input type='hidden' name='keyword' value='<c:out value="${cri.keyword}"/>'>
... 생략 ...
수정/삭제 처리는 BoardController 에서 redirect 방식으로 동작하므로 type과 keyword 조건을 같이 리다이렉트 시에 포함 시켜야만 한다.
BoardController의 일부
// BoardController.java
... 생략 ...
@PostMapping("/modify")
public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
log.info("modify : " + board);
if(service.modify(board)) {
rttr.addFlashAttribute("result", "success");
}
rttr.addAttribute("pageNum", cri.getPageNum());
rttr.addAttribute("amount", cri.getAmount());
rttr.addAttribute("type", cri.getType());
rttr.addAttribute("keyword", cri.getKeyword());
return "redirect:/board/list";
}
@PostMapping("/remove")
public String remove(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
log.info("remove..." + bno);
if(service.remove(bno)) {
rttr.addFlashAttribute("result", "success");
}
rttr.addAttribute("pageNum", cri.getPageNum());
rttr.addAttribute("amount", cri.getAmount());
rttr.addAttribute("type", cri.getType());
rttr.addAttribute("keyword", cri.getKeyword());
return "redirect:/board/list";
}
리다이렉트는 GET 방식으로 이루어지기 때문에 추가적인 파라미터를 처리해야 한다.
modify.jsp 에서는 다시 목록으로 이동하는 경우에 필요한 파라미터만 전송하기 위해 <form> 태그의 모든 내용을 지우고 다시 추가하는 방식을 이용했으므로 keyword와 type역시 추가하고 JavaScript 코드를 수정해야한다.
modify.jsp
// modify.jsp // 스크립트 부분 수정
... 생략 ...
<script type="text/javascript">
$(document).ready(function(){
var formObj = $("form");
$('button').on("click", function(e){
e.preventDefault();
var operation = $(this).data("oper");
console.log(operation);
if(operation === 'remove') {
formObj.attr("action", "/board/remove");
} else if (operation === 'list'){
// move to list
formObj.attr("action", "/board/list").attr("method", "get");
var pageNumTag = $("input[name='pageNum']").clone();
var amountTag = $("input[name='amount']").clone();
var keywordTag = $("input[name='keyword']").clone();
var typeTag = $("input[name='type']").clone();
formObj.empty();
formObj.append(pageNumTag);
formObj.append(amountTag);
formObj.append(keywordTag);
formObj.append(typeTag);
}
formObj.submit();
});
});
</script>
수정 / 조회 화면에서 어떤 작업을 하던지 다시 목록 페이지로 검색 조건이 유지되는지 확인한다,
검색한 상태에서 특정 페이지의 게시물을 수정하면 검색 조건은 유지한 채 목록 페이지로 이동하는지를 테스트한다.
UriComponentsBuilder를 이용하는 링크 생성
웹페이지에서 매번 파라미터를 유지하는 일이 번거롭고 힘들다면 한 번쯤 UriComponentsBuilder라는 클래스를 이용해 볼 필요가 있다.
org.springframework.web.util.UriComponentsBuilder는 여러 개의 파라미터들을 연결해서 URL의 형태로 만들어주는 기능을 가지고 있다.
URL을 만들어주면 리다이렉트를 하거나, <form> 태그를 사용하는 사황을 많이 줄여줄 수 있다.
검색 조건을 유지하는 org.codehows.domain.Criteria 클래스에 링크를 생성하는 기능을 추가한다.
Criteria 클래스의 일부
// Criteria.java
... 생략 ...
public String getListLink() {
UriComponentsBuilder builder = UriComponentsBuilder.fromPath("")
.queryParam("pageNum", this.pageNum)
.queryParam("amount", this.getAmount())
.queryParam("type", this.getType())
.queryParam("keyword", this.getKeyword());
return builder.toUriString();
}
}
UriComponentsBuilder는 queryParam() 이라는 메서드를 이용해서 필요한 파라미터들을 손쉽게 추가 할 수 있다.
예)
Criteria cri = new Criteria();
cri.setPageNum(3);
cri.setAmount(20);
cri.setKeyword("새로");
cri.setType("TC");
위와 같은 데이터를 가진 Criteria의 getListLink()의 결과는 '?pageNum=3&amount=20&type=TC&keyword=%EC%83%88%EB%A1%9C' 와 같이 GET 방식에 적합한 URL 인코딩된 결과로 만들어진다.
getListLink()를 이용하면 BoardController의 modify()와 remove()를 다음과 같이 간단하게 정리할 수 있다.
BoardController의 일부
// BoardController.java
... 생략 ...
@PostMapping("/modify")
public String modify(BoardVO board, Criteria cri, RedirectAttributes rttr) {
log.info("modify : " + board);
if(service.modify(board)) {
rttr.addFlashAttribute("result", "success");
}
return "redirect:/board/list" + cri.getListLink();
}
@PostMapping("/remove")
public String remove(@RequestParam("bno") Long bno, Criteria cri, RedirectAttributes rttr) {
log.info("remove..." + bno);
if(service.remove(bno)) {
rttr.addFlashAttribute("result", "success");
}
return "redirect:/board/list" + cri.getListLink();
}
UriComponentsBuilder로 생성된 URL은 화면에서도 유용하게 사용될 수 있는데, 주로 JavaScript를 사용할 수 없는 상황에서 링크를 처리해야하는 상황에서 사용된다.