Keep going

검색처리 본문

Records/Spring Framework

검색처리

코딩천재홍 2021. 3. 15. 23:13

검색 기능은 검색 조건과 키워드로 나누어 생각해 볼 수 있다.

검색 조건은 일반적으로 <select> 태그를 이용해서 작성하거나 <checkbox>를 이용하는 경우가 많다.

최근에는 일반 웹사이트에서 일반 사용자들의 경우에는 <select>를,

관리자용이나 검색 기능이 강한 경우는 <checkbox>를 이용하는 형태가 대부분이다.

 

 

15.1 검색 기능과 SQL

게시물의 검색 기능은 다음과 같이 분류된다.

  • 제목/내용/작성자와 같이 단일 항목 검색
  • 제목 or 내용, 제목 or 작성자, 내용 or 작성자, 제목 or 내용 or 작성자 같은 다중 항목 검색

오라클은 페이징 처리에 인라인뷰를 이용하기 때문에 실제로 검색 조건에 대한 처리는 인라인뷰의 내부에서 이루어져야 한다.

단일 항목의 검색은 검색 조건에 따라서 칼럼이 달라지고 , LIKE 처리를 통해서 키워드를 사용하게 된다.

단일 항목은 인라인뷰 안쪽에서 필요한 데이터를 가져올 때 검색 조건이 적용되어야 하기 때문에 WHERE 문 뒤에 검색 조건이 추가되고, ROWNUM 조건이 뒤따르게 하면 문제가 없다.

 

 

15.1.1 다중 항목 검색

문제는 2개 이상의 조건이 붙는 다중 항목의 검색이다. 

예를 들어, 제목 이나 내용 중에 'TEST'라는 문자열이 있는 게시물들을 검색할 때 SQL 문을 작성해보겠다.

select * from
(
select /*+INDEX_DES(tb1_board po_board) */
 rownum rn, bno, title, content, writer, regdate, updatedate
 from tb1_board
 where title like '%Test%' or content like '%Test%'
     and rownum <=20
)
where rn>10;
cs

위 SQL의 경우, 10개의 데이터가 아니라 많은 양의 데이터가 나온다.

그 이유는 위 SQL 문에서 AND 연산자가 OR 연산자 보다 우선 순위가 높기 때문에 ROWNUM이 20보다 작거나 같으면서 내용에 'TEST'라는 문자열이 있거나 제목에 'TEST'라는 문자열이 있는 게시물들을 검색하게 된다.

 

AND, OR 이 섞어 있는 SQL을 작성할 때에는 우선 순위 연산자인 ( ) 를 이용해서 OR 조건들을 처리해야 한다.

select * from
(
select /*+INDEX_DES(tb1_board po_board) */
 rownum rn, bno, title, content, writer, regdate, updatedate
 from tb1_board
 where 
    (title like '%Test%' or content like '%Test%' )
     and rownum <=20
)
where rn>10;
cs

결과를 보면 원하는 10개의 데이터만 출력되는 것을 볼 수 있다.

 


15.2 MyBatis의 동적 SQL

검색 조건이 변하면 SQL의 내용 역시 변하기 때문에 XML 이나 어노테이션과 같이 고정된 문자열을 작성하는 방식으로는 제대로 처리할 수 없다. 다행히 MyBatis는 동적 태그 기능을 이용해 SQL을 파라미터의 조건에 맞게 조정할 수 있는 기능을 제공한다.

 

 

15.2.1 MyBatis의 동적 태그들

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

 

<if>

if는 test라는 속성과 함께 특정한 조건이 true 가 되었을 때 포함된 SQL을 사용하고자 할 때 작성한다.

 

예를 들어, 단일 항목으로 제목, 내용, 작성자에 대해서 검색해야 하는 상황이라고 가정하겠다.

  • 검색 조건이 'T' 면 제목이 키워드인 항목을 검색
  • 검색 조건이 'C' 면 내용이 키워드인 항목을 검색
  • 검색 조건이 'W'면 작성자가 키워드인 항목을 검색검색 기능은 검색 조건과 키워드로 나누어 생각해 볼 수 있다.
<if test="type=='T'.toString()">
    (title like '%' || #{keyword} || '%')
</if>
<if test="type=='C'.toString()">
    (content like '%' || #{keyword} || '%')
</if>
<if test="type=='W'.toString()">
    (writer like '%' || #{keyword} || '%')
</if>
cs

 

 

<choose>

if와 달리 choose는 여러 상황들 중 하나의 상황에서만 동작한다.

JAVA 언어의 'if ~ else' 나 JSTL의 <choose> 와 유사하다.

<choose>
<when test="type=='T'.toString()">
    (title like '%' || #{keyword} || '%')
</when>
<when test="type=='T'.toString()">
    (title like '%' || #{keyword} || '%')
</when>
<when test="type=='T'.toString()">
    (title like '%' || #{keyword} || '%')
</when>
<otherwise>
    (title like '%' || #{keyword} || '%' OR content like '%' || #{keyword} || '%')
</otherwise>
</choose>
cs

<otherwise> 는 모든 위의 모든 조건이 충족되지 않을 경우에 사용한다.

 

 

<trim>, <where>, <set>

trim, where, set은 단독으로 사용되지 않고 <if>, <choose>와 같은 태그들을 내포하여 SQL 들을 연결해 주고, 앞 뒤에 필요한 구문들(AND, OR, WHERE)을 추가하거나 생략하는 역할을 한다.

태그 안쪽에서 SQL이 생성될 때는 WHERE 구문이 붙고, 그렇지 않는 경우에는 생성되지 않는다.

select * from tb1_board
<where>
    <if test="bno !=null">
    bno = #{bno}
    </if>
</where>
cs

위와 같은 경우는 bno 값이 null 인 경우에는 WHERE 구문이 없어지고, bno 값이 존재하는 경우에만 'WHERE bno =xx'와 같이 생성된다.

 

<trim>은 하위에서 만들어지는 SQL 문을 조사하여 앞 쪽에 추가적인 SQL을 넣을 수 있다.

select * from tbl_board
<where>
    <if test="bno!=null">
    bno=#{bno}
    </if>
    <trim prefix ="and">
    rownum=1
    </trim>
</where>
cs

 

 

<foreach>

foreach는 List, 배열, 맵 등을 이용해서 루프를 처리할 수 있다.

주로 IN 조건에서 많이 사용하지만, 경우에 따라서는 복잡한 WHERE 조건을 만들때에도 사용할 수 있다.

 

예를 들어 제목은 'TTTT', 내용은 'CCCC'라는 값을 이용한다면 Map 의 형태로 작성이 가능하다.

Map<String, STring> map= new HashMap<>();
map.put("T", "TTTT");
map.put("C", "CCCC");
cs

 

작성된 Map을 파라미터로 전달하고, foreach를 이용하면 다음과 같은 형식이 가능하다.

select * from tb1_board
  <trim prefix="where (" suffix=")" prefixOverrides="OR">
      <foreach item="val" index="key" collection="map">
    
        <trim prefix="OR">
            <if test="key == 'C'.toString()">
                content = #{val}
            </if>
            <if test="key == 'T'.toString()">
                title = #{val}
            </if>
            <if test="key == 'W'.toString()">
                   writer = #{val}
            </if>
        </trim>
    </foreach>
</trim>
cs

 

foreach를 배열이나 List를 이용하는 경우에는 item 속성만을 이용하면 되고,

Map의 형태로 key와 value를 이용해야 할 때는 index와 item 속성을 둘 다 이용한다.

select * from tb1_board
 
    where ( content = ?
        OR title = ? )
 
INFO : jdbc.sqlonly - select * from tb1_board where ( content = 'CCCC' OR title = 'TTTT' )
cs

 


15.3 검색 조건 처리를 위한 Criteria의 변화

검색 조건을 처리하기 위해 검색 조건과 검색에 사용하는 키워드가 필요하므로, Criteria를 확장할 필요가 있다.

확장 방법으로는 상속 방법을 이용하거나 직접 Criteria 클래스를 수정하는 방식이 있다.

package org.zerock.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 == nullnew String[] {} : type.split("");
    }
    
}
 
cs

type과 keyword라는 변수를 추가한다.

getter/setter는 Lombok을 통해서 생성하고 getTypeArr은 검색 조건이 T, W, C로 구성되어 있을 때 검색 조건을 배열로 만들어 한 번에 처리하기 위함이다. 

 

 

15.3.1 BoardMapper.xml에서 Criteria 처리

BoardMapper.xml은 기존의 getListWithPaging( )을 수정해서 동적 SQL을 처리한다.

<select id="getListWithPaging" resultType="org.zerock.domain.BoardVO">
    <![CDATA[
        select
            bno, title, content, writer, regdate, updateDate
        from 
        (
            select /*+INDEX_DESC(tb1_board pk_board) */
                rownum rn, bno, title, content, writer, regdate, updateDate
            from
                tb1_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()">
                                   content like '%' || #{keyword} || '%'
                               </when>
                               <when test="type == 'W'.toSTring()">
                                   writer like '%' || #{keyword} || '%'
                               </when>
                           </choose>
                       </trim>
                   </foreach>
               </trim>
       <![CDATA[
            rownum<= #{pageNum} * #{amount}
        )
        where rn > (#{pageNum}-1) *  #{amount}
    ]]>
</select>
cs

검색 조건이 3가지 이므로 총 6가지의 조합이 가능하지만, 각 문자열을 이용해서 검색 조건을 결합하는 형태로 하면 3개의 동적 SQL 구문만으로도 처리를 할 수 있다.

<foreach>를 이용해서 검색 조건들을 처리하는데 typeArr이라는 속성을 이용한다.

MyBatis는 원하는 속성을 찾을 때 getTypeArr( )과 같이 이름에 기반을 두어서 검색 하기 때문에 Criteria에서 만들어둔 getTypeArr( ) 결과인 문자열의 배열이 <foreach>의 대상이 된다.

<choose> 안쪽의 동적 SQL은 OR title.... OR content... OR writer와 같은 구문을 만들어 내개 된다. 따라서 바깥쪽에서는 trim을 이용해 맨 앞에 생성되는 OR를 없애준다.

 

 

<sql> <include>와 검색 데이터의 개수 처리

동적 SQL을 이용해서 검색 조건을 처리하는 부분은 해당 데이터의 개수를 처리하는 부분에서도 동일하게 적용되어야만 한다.

MyBatis는 <sql>이라는 태그를 이용해서 SQL의 일부를 별도로 보관하고, 필요한 경우에 include 시키는 형태로 사용할 수 있다.

<sql id="criteria">
        <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()">
                            content like '%'||#{keyword}||'%'
                        </when>
                        <when test="type == 'W'.toString()">
                            writer like '%'||#{keyword}||'%'
                        </when>
                    </choose>
                </trim>
            </foreach>
        </trim>
    </sql>
    
    <select id="getListWithPaging"
        resultType="org.zerock.domain.BoardVO">
    <![CDATA[
        select
            bno, title, content, writer, regdate, updateDate
        from 
        (
            select /*+INDEX_DESC(tb1_board pk_board) */
                rownum rn, bno, title, content, writer, regdate, updateDate
            from
                tb1_board
            where 
       ]]>
    <include refid="criteria"></include>
       <![CDATA[
            rownum<= #{pageNum} * #{amount}
        )
        where rn > (#{pageNum}-1) *  #{amount}
    ]]>
    </select>
 
 
    <select id="getTotalCount" resultType="int">
        select count(*) from tb1_board where 
        <include refid="criteria"> </include>
         bno > 0
    </select>
cs

 


15.4 화면에서 검색 조건 처리

화면에서 검색은 다음과 같은 사항들을 주의해서 개발해야 한다.

  • 페이지 번호가 파라미터로 유지되었던 것처럼 검색 조건과 키워드 역시 항상 화면 이동 시 같이 전송되어야 한다.
  • 화면에서 검색 버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동한다.
  • 한글의 경우 GET 방식으로 이동하는 경우 문제가 생길 수 있으므로 주의해야 한다.

 

15.4.1 목록 화면에서의 검색 처리

<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>
cs

HTML을 보면 페이징 처리를 위해 만들어둔 form 태그에 select와 input 태그가 추가된 것을 볼 수 있다.

<form>내 <button>의 기본 동작은 submit이므로 별도의 처리 없이 검색이 되는지 확인한다.

 

 

검색 버튼의 이벤트 처리

 

여러 문제들 중에서 검색 버튼을 클릭하면 검색은 1페이지를 하도록 수정하고, 화면에 검색 조건과 키워드가 보이게 처리하는 작업을 우선으로 진행한다.

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();
});
cs

 

브라우저에서 검색 버튼을 클릭하면 <form> 태그의 전송은 막고 페이지 번호가 1이 되도록 처리한다.

화면에서 키워드가 없다면 검색을 하지 않도록 제어한다.

검색 후에는 주소창에 검색 조건과 키워드가 같이 GET 방식으로 처리되므로 이를 이용해서 <select> 태그나 <input> 태그의 내용을 수정해야 한다.

                                    <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>
cs

<select> 태그의 내부는 삼항 연산자를 이용해서 해당 조건으로 검색되었다면 'selected'라는 문자열을 출력하게 해서 화면에서 선택된 항목으로 보이도록 한다.

 

페이지 번호를 클릭해서 이동할 때에도 검색 조건과 키워드는 같이 전달되어야 하므로 페이지 이동에 사용한 <form> 태그를 아래처럼 수정한다.

<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>
cs

검색 조건과 키워드에 대한 처리가 되면 검색 후 페이지를 이동해서 동일한 검색 사항들이 계속 유지되는 것을 볼 수 있다.

 

 

15.4.2 조회 페이지에서 검색 처리

목록 페이지에서 조회 페이지로의 이동은 이미 <form> 태그를 이용해서 처리했기 때문에 별도의 처리가 필요하지 않다.

다만 조회 페이지는 아직 Criteria의 type과 keyword에 대한 처리가 없기 때문에 필요하다.

<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>
cs

 

 

15.4.3 수정/삭제 페이지에서 검색 처리

조회 페이지에서 수정/삭제 페에지로의 이동은 GET 방식을 통해서 이동하고, 이동 방식 역시 <form> 태그를 이용하는 방식이므로 기존의 <form> 태그에 추가적인 type과 keyword 조건만을 추가한다.

<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='keyword' value='<c:out value="${cri.keyword}"/>' >
<input type='hidden' name='type' value='<c:out value="${cri.type}"/>' >
cs

 

수정/삭제 처리는 BoardController에서 redirect 방식으로 동작하므로 type과 keyword 조건을 같이 리다이렉트 시에 포함시켜야만 한다.

@PostMapping("/modify")
    public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
        log.info("modify: " + board);
        if(service.modify(board)) {
            rttr.addFlashAttribute("result""sucess");
        }
        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";
    }
cs

리다이렉트는 GET 방식으로 이루어지기 때문에 추가적인 파라미터를 처리해야 한다.

 

modify.jsp에서는 다시 목록으로 이동하는 경우에 필요한 파라미터만 전송하기 위해서 <form> 태그의 모든 내용을 지우고 다시 추가하는 방식을 이용했으므로 keyword와 type 역시 추가하도록 JavaScript 코드를 수정해야 한다.

<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>
cs

 

 

UriComponentsBuilder를 이용하는 링크 생성

웹 페이지에서 매번 파라미터를 유지하는 일이 번거롭고 힘들 때 유용하다.

org.springframework.web.util.UriComponentsBuilder는 여러 개의 파라미터들을 연결해서 URL의 형태로 만들어주는 기능을 한다.

 

URL을 만들어주면 리다이렉트를 하거나, <form> 태그를 사용하는 상황을 많이 줄여줄 수 있다.

package org.zerock.domain;
 
import org.springframework.web.util.UriComponentsBuilder;
 
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 == nullnew String[] {}: type.split("");
    }
    
    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();
    }
}
 
cs

UriComponentsBuilder는 queryParam( )이라는 메서드를 이용해서 필요한 파라미터들을 손쉽게 추가할 수 있다.

 

getListLink( )를 이용하면 BoardController의 modify( )와 remove( )를 다음과 같이 간단하게 정리할 수 있다.

@PostMapping("/modify")
    public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
        log.info("modify: " + board);
        if(service.modify(board)) {
            rttr.addFlashAttribute("result""sucess");
        }
        
        return "redirect:/board/list"+cri.getListLink();
    }
    
    @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");
        }
        return "redirect:/board/list"+cri.getListLink();
    }
cs

UriComponentsBuilder로 생성된 URL은 화면에서도 유용하게 사용될 수 있는데, 주로 JavaScript를 사용할 수 없는 상황에서 링크를 처리해야 하는 상황에서 사용된다.

 


출처 : 스프링으로 배우는 웹 프로젝트 [구멍가게 코딩단]

'Records > Spring Framework' 카테고리의 다른 글

Ajax 댓글 처리  (0) 2021.03.18
REST 방식으로 전환  (0) 2021.03.16
페이징 화면 처리  (0) 2021.03.11
MyBatis와 스프링에서 페이징 처리  (0) 2021.03.11
오라클 데이터베이스 페이징 처리  (0) 2021.03.05
Comments