자바, 스프링, 데이터베이스부터 쿠버네티스까지 — 견고한 백엔드 시스템 구축을 위한 배움과 경험의 기록입니다.
게시물 관리에서 마지막은 다양한 검색 처리이다.
검색 기능은 다시 검색 조건과 키워드로 나누어 생각해 볼 수 있다.검색 조건은 일반적으로
작성하거나 를 이용하는 경우가 많다.과거에는 를 이용하는 경우가 더 많았지만
최근에는 일반 웹사이트에서 일반 사용자들의 경우에는
를 이용하는 형태가 대부분이다.
### 검색 기능과 SQL
게시물의 검색 기능은 다음과 같이 분류가 된다.
- 제목/내용/작성자와 같이 단일 항목 검색
- 제목or내용,제목or작성자,내용or작성자,제목or내용or작성자와 같은 다중 항목 검색
검색 항목은 제목/내용/작성자와 같은 단일 항목 검색과 제목 or 내용과 같이 복합적인 항목으로 검색하는 방식이 존재한다.게시물의 검색이 붙으면 가장 신경 쓰이는 부분은 역시 SQL쪽이다.오라클은 페이징 처리에 인라인뷰를 이용하기 때문에 실제로 검색 조건에 대한 처리는 인라인뷰의 내부에서 이루어져야 한다.
단일 항목의 검색은 검색 조건에 따라서 칼럼이 달라지고 Like 처리를 통해서 키워드를 사용하게 된다.
만일 2페이지에 해당하는 데이터를 제목으로 검색하고 키워드는 Test라고 한다면 다음과 같이 작성될 수 있다.

단일 항목은 인라인뷰 안쪽에서 필요한 데이터를 가져올 때 검색 조건이 적용되어야 하기때문에
where문 뒤에 검색 조건이 추가되고 rownum조건이 뒤따르게 하면 문제가 없다.
#### 다중 항목 검색
문제는 2개 이상의 조건이 붙는 다중 항목의 검색이다.
```sql
select
*
from
(
select /*+index_desc(tbl_board pk_board)*/
rownum rn,bno,title,content,writer,regdate,updatedate
from
tbl_board
where
title like '%새로%' or content like '%새로%'
and rownum <=20
)
where rn>10;
```
title like '%Test%' or content like '%Test%' 구문 자체는 이상이 없지만
실제로 동작시켜 보면 10개의 데이터가 아니라 많은 양의 데이터가 나오는 것을 볼 수 있다.
(필자는 '새로'라는 키워드로 검색하였다.)

이렇게 많은 양의 데이터가 나온 이유는 위 SQL문에서 and 연산자가 or 연산자보다 우선 순위가 높기 때문에 rownum이 20보다 작거나 같으면서(and) 내용에 '새로'라는 문자열이 있거나(or) 제목에 '새로'라는 문자열이 있는 게시물을들 검색하게 된다.
제목에 '새로'라는 문자열이 있는 경우는 많기 때문에 위의 그림과 같이 많은 양의 데이터를 가져오게 된다.
and와 or가 섞여있는 SQL을 작성할 때에는 우선 순위 연산자인 ()를 이용해서 or 조건들을 처리해야 한다.

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

### MyBatis의 동적 SQL
SQL문에서 느끼는 점은 검색 조건이 변하면 SQL의 내용 역시 변하기 때문에 XML이나 어노테이션과 같이 고정된 문자열을 작성하는 방식으로는 제대로 처리할 수 없다는 사실이다.다행히 MyBatis는 동적(Dynamic) 태그 기능을 통해서 SQL을 파라미터들의 조건에 맞게 조정할 수 있는 기능을 제공한다.MyBatis의 동적 태그는 약간의 구문을 이용해서 전달되는 파라미터를 가공해서 경우에 따라 다른 SQL을 만들어서 실행할 수 있다.
#### MyBatis의 동적 태그들
MyBatis는 기존의 iBatis에서 발전하면서 복잡했던 동적 SQL을 작성하는데 태그들이 많이 정리되어서 다음과 같이 몇가지의 태그들만을 이용한다.
- if
- choose(when,otherwise)
- trim(when,set)
- foreach
**
if는 test라는 속성과 함께 특정한 조건이 true가 되었을 때 포함된 SQL을 사용하고자 할 때 작성한다.
예를 들어 단일 항목으로 제목,내용,작성자에 대해서 검색해야 하는 상황이라고 가정해보자
- 검색조건이 T이면 제목이 키워드인 항목을 검색
- 검색조건이 C이면 내용이 키워드인 항목을 검색
- 검색조건이 W이면 작성자이 키워드인 항목을 검색
위와 같은 경우 MyBatis에서는 XML에서 다음과 같이 작성할 수 있다.
```sql
(title like '%' ||#{keyword}||'%')
(content like '%' ||#{keyword}||'%')
(writer like '%' ||#{keyword}||'%')
```
if안에 들어가는 표현식은 OGNL 표현식이라는 것을 이용한다.
**
if와 달리 choose는 여러 상황들 중 하나의 상황에서만 동작한다.Java언어의 if~else 나 JSTL의 와 유사하다.
```sql
(title like '%' ||#{keyword}||'%')
(content like '%' ||#{keyword}||'%')
(writer like '%' ||#{keyword}||'%')
(title like '%' ||#{keyword}||'%' or content like '%' ||#{keyword}||'%')
```
는 모든 위의 모든 조건을 충족하지 않을 경우에 사용한다.(마치 else와 같다.)
*,,*
trim,where,set은 단독으로 사용되지 않고 ,와 같은 태그들을 내포하여 SQL들을 연결해 주고
앞,뒤에 필요한 구문들(and,or,where등)을 추가하거나 생략하는 역할을 한다.SQL을 작성하다 보면 상황에 따라서
where나 and,or등이 문제가 되는 상황이 발생할 수도 있다.예를 들어 where rownum<=20은 문제가 없지만
겸색 조건이 들어가면 문제가 될 수 있다.

위의 그림과 같이 만일 검색 조건이 없다면 and라는 키워드는 들어갈 필요가 없지만
검색 조건이 추가되면 and가 필요한 상황이 된다.where,trim,set은 이러한 상황에서 필요한 키워드를 붙이거나 빼는 상황에서 사용된다.
의 경우 태그 안쪽에서 SQL이 생성될 때는 where 구문이 붙고 그렇지 않는 경우에는 생성되지 않는다.
```sql
select * from tbl_board
bno=#{bno}
```
위와 같은 경우는 bno 값이 null인 경우에는 where 구문이 없어지고 bno값이 존재하는 경우에만 where bno=숫자 와 같이 생성된다.
| | |
| --- | --- |
| bno값이 존재하는 경우 | select \* from tbl\_board where bno = 33 |
| bno가 null인 경우 | select \* from tbl\_board |
은 하위에서 만들어지는 SQL문을 조사하여 앞 쪽에 추가적인 SQL을 넣을 수 있다.
```sql
select * from tbl_board
bno=#{bno}
rownum = 1
```
trim은 prefix,suffix,prefixOverrides,suffixOverrides 속성을 지정할 수 있다.
| | |
| --- | --- |
| bno값이 존재하는 경우 | select \* from tbl\_board where bno = 33 and rownum = 1 |
| bno가 null인 경우 | select \* from tbl\_board where rownum = 1 |
**
foreach는 List,배열,맵 등을 이용해서 루프를 처리할 수 있다
주로 in 조건에서 많이 사용하지만 경우에 따라서 복잡한 where 조건을 만들때에도 사용할 수 있다.
예를들어 제목("T")은 "TTTT"로 내용("C")은 "CCCC"라는 값을 이용한다면 Map의 형태로 작성이 가능하다.
```java
Map<String,String> map = new HashMap();
map.put("T","TTTT");
map.put("C","CCCC");
```
작성된 Map을 파라미터로 전달하고 foreach를 이용하면 다음과 같은 형식이 가능하다.
```sql
select * from tbl_board
content = #{val}
title = #{val}
writer = #{val}
```
foreach를 배열이나 List를 이용하는 경우에는 item 속성만을 이용하면 되고
Map형태로 key와 value를 이용해야 할 때는 index와 item 속성을 둘 다 이용한다.
전달된 값에 따라서 다음과 같이 처리된다.
```sql
select * from tbl_board
where(content=?
or title =?)
```
### 검색 조건 처리를 위한 Criteria의 변화
페이징 처리에 사용했던 Criteria의 의도는 단순히 pageNum과 amount라는 파라미터를 수집하기 위해서이다.
페이징 처리에 검색 조건 처리가 들어가면 Criteria 역시 변화가 필요하다.
검색 조건을 처리하기 위해서는 검색 조건과 검색에 사용하는 키워드가 필요하므로
기존의 Criteria를 확장할 필요가 있다.확장 방법으로는 상속 방법을 이용하거나 직접 Criteria 클래스를 수정하는 방식을 생각해 볼 수 있는데 직접 Criteria 클래스를 수정한다.
Criteria.class
```java
package com.osk2090.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
```sql
<![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
]]>
title like '%'||#{keyword}||'%'
content like '%'||#{keyword}||'%'
writer like '%'||#{keyword}||'%'
<![CDATA[
rownum <= #{pageNum} * #{amount}
)
where rn > (#{pageNum} - 1) * #{amount}
]]>
```
검색 조건이 3가지이므로 총 6가지의 조합이 가능하지만 각 문자열을 이용해서 검색 조건을 결합하는 형태로 하면 3개의 동적 SQL 구문만으로도 처리를 할 수 있다.
를 이용해서 검색 조건들을 처리하는데 typeArr이라는 속성을 이용한다.
MyBatis는 원하는 속성을 찾을 때 getTypeArr()과 같이 이름에 기반을 두어서 검색하기 때문에 Criteria에서 만들어둔 getTypeArr()결과인 문자열의 배열이 의 대상이 된다.(MyBatis는 엄격하게 Java Beans의 규칙을 따르지 않고
get/set 메서드만을 활용하는 방식이다.)
안쪽의 동적 SQL은 or title ... or content ... or writer 와 같은 구문을 만들어내게 된다.
따라서 바깥쪽에서는 을 이용해서 맨 앞에서 생성되는 or을 없애준다.
동적 SQL은 경우에 따라서 여러 종류의 SQL이 생성될 수 었으므로 제대로 동작하는지 반드시 여러 번의 확인을 거쳐야만 한다.기존에 BoardMapperTests를 만들어 두었으니 이를 이용해서 테스트 코드 작성한다.
BoardMapperTests.class
```java
@Test
public void testSearch() {
Criteria cri = new Criteria();
cri.setKeyword("새로");
cri.setType("TC");
List list = mapper.getListWithPaging(cri);
list.forEach(board -> log.info(board));
}
```
testSearch()는 Criteria 객체의 type과 keyword를 넣어서 원하는 SQL이 생성되는지 확인하기 위함이다.
중요한 것은 실행 결과가 아니라 실행할 때 만들어지는 SQL이다.아래와 같이 각 상황에 맞체 SQL이 올바르게 만들어지는지 반드시 확인해야 한다.
| | |
| --- | --- |
| 검색 조건이 없는 경우 | INFO : jdbc.audit - 1. Connection.prepareStatement(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 rownum <= ? \* ? ) where rn > (? - 1) \* ?) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@5f5b5ca4 |
| 단일 검색 항목(제목) | INFO : jdbc.audit - 1. Connection.prepareStatement(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 ( title like '%'||?||'%' ) AND rownum <= ? \* ? ) where rn > (? - 1) \* ?) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@40620d8e |
| 다중 검색(제목 or 내용) | INFO : jdbc.audit - 1. Connection.prepareStatement(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 ( title like '%'||?||'%' OR content like '%'||?||'%' ) AND rownum <= ? \* ? ) where rn > (? - 1) \* ?) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@40620d8e |
*와 검색 데이터의 개수 처리*
동적 SQL을 이용해서 검색 조건을 처리하는 부분은 해당 데이터의 개수를 처리하는 부분에서도 동일하게 적용되어야만 한다.이 경우 가장 간단한 방법은 동적 SQL을 처리하는 부분을 그대로 복사해서 넣어줄 수 있지만,만일 동적 SQL을 수정하는 경우에는 매번 목록을 가져오는 SQL과 데이터 개수를 처리하는 SQL 쪽을 같이 수정해야 한다.
MyBatis는 이라는 태그를 이용해서 SQL의 일부를 별도로 보관하고 필요한 경우에 include시키는 형태로 사용할 수 있다.
BoardMapper.xml
```sql
title like '%'||#{keyword}||'%'
content like '%'||#{keyword}||'%'
writer like '%'||#{keyword}||'%'
```
태그는 id라는 속성를 이용해서 필요한 경우에 동일한 SQL의 일부를 재사용할 수 있게 한다.
### 화면에서 검색 조건 처리
화면에서 검색은 다음과 같은 사항들을 주의해서 개발해야 한다.
- 페이지 번호가 파라미터로 유지되었던 것처럼 검색 조건과 키워드 역시 항상 화면 이동 시 같이 전송되어야 한다.
- 화면에서 검색 버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동한다.
- 한글의 경우 GET 방식으로 이동하는 경우 문제가 생길 수 있으므로 주의해야 한다.
#### 목록 화면에서의 검색 처리
목록 화면인 list.jsp에서는 검색 조건과 키워드가 들어 갈 수 있게 html을 수정해야 한다.
views 폴더 내의 list.jsp를 수정해서 페이지 처리 바로 위쪽에 아래의 내용들을 추가한다.

수정된 html을 보면 페이징 처리를 위해서 만들어둔