본 포스팅은 코드로 배우는스프링 웹프로젝트를 참조하여 작성한 내용입니다. 개인적으로 학습한 내용을 복습하기 위한 내용이기 때문에 내용상 오류가 있을 수 있습니다. 기존의 Spring MVC 관련 포스팅들이 제대로 정리되지 않은 것 같아 처음부터 차분히 정리하면서 포스팅을 진행하고 있습니다.
그리고 본 포스팅의 예제는 STS 또는 Eclipse를 사용하지 않고 IntelliJ를 통해 구현하고 있습니다. 그래서 기존의 STS에서 생성된 Spring 프로젝트의 스프링관련 설정 파일명과 프로젝트 구조가 약간 다를 수 있습니다. IntelliJ를 통한 Spring MVC 프로젝트 생성 포스팅을 참고해주시면 감사하겠습니다.
포스팅하고 있는 현재 프로젝트의 예제가 혹시 필요하신 분은 깃주소(https://github.com/walbatrossw/spring-mvc-ex)를 통해 얻으실 수 있습니다.
Spring MVC 기본 개념 및 테스트 정리 포스팅 링크
Spring-MVC 게시판 예제 이전 포스팅 링크
Spring MVC 게시판 예제 10 - 검색처리, 동적SQL
1. 검색에 필요한 데이터와 SearchCriteria
클래스 작성하기
# 검색에 필요한 데이터
페이징처리와 함께 검색 기능을 구현하기 위해서는 아래와 같이 4가지 정보가 필요하다.
- 현재 페이지 번호(
page
)
- 페이지 당 출력할 게시글의 갯수(
perPageNum
)
- 검색 조건(
searchType
)
- 검색 키워드(
keyword
)
위의 정보 중에서 page
와 perPageNum
의 경우 앞서 페이징 처리를 구현하면서 사용한 Criteria
클래스에 이미 멤버변수로 선언해 사용하고 있다. 하지만 searchType
과 keyword
는 어디에도 사용한 적이 없기 때문에 새로 추가해야만 사용이 가능하다. 그래서 Criteria
클래스에 추가적으로 멤버변수로 선언해 사용하면 된다. 그러나 검색 처리 구현 이전의 코드와의 차이점을 구분하기 위해 Criteria
에 멤버변수를 추가적으로 선언하는 방식 대신 Criteria
클래스를 상속받은 SearchCriteria
라는 클래스를 새로 생성해 새로운 멤버 변수로 작성해준다. 이렇게 함으로써 기존의 코드와 차이점을 구분하는 것 뿐만아니라, 기존코드의 수정을 최소화 할 수 있게된다.
# SearchCriteria
클래스 작성하기
앞서 설명한 것처럼 SearchCriteria
클래스를 src/main/java/기본패키지/commons/paging
패키지에 생성하고, 검색조건과 검색키워드를 멤버변수로 선언해준다.
2. 컨트롤러(ArticlePagingSearchController
) 작성하기
이전 포스팅에서 정리한 것처럼 기존의 컨트롤러에 메서드를 추가하거나, 수정하는 방식이 아닌 새로운 컨트롤러를 생성해서 기존 코드와 차이점을 구분할 수 있도록 src/main/java/기본패키지/article/controller
패키지에 클래스 새로 생성해 아래와 같이 작성해준다.
이전에 작성했던 컨트롤러와 비교해보면 list()
의 매개변수의 타입이 Criteria
에서 SearchCriteria
로 변경되었다는 것 외에는 차이점은 없다.
3. 목록 페이지(list.jsp
) : 검색창 만들기, 페이지이동URI 수정, 검색버튼 동작 처리
/WEB-INF/views/article/search
디렉토리를 생성하고, 기존의 list.jsp
를 복사하여 붙어 넣어준다. 이것도 마찬가지로 기존의 list.jsp
파일을 수정하지 않고, 새로 만들어주는 이유는 기존의 코드와 차이점을 구분하기 위해서이다.
# 검색창 만들기
게시글 페이지 번호 영역 바로 아래에 <div class="box-footer"></div>
을 작성하고, 안에 아래와 같이 코드를 작성한다.
검색 조건은 selectbox
를 통해 사용자가 값을 선택할 수 있도록 구현하는데 값이 가지는 의미는 아래와 같다.
- 검색조건 없음 :
n
- 제목 :
t
- 내용 :
c
- 작성자 :
w
- 제목 or 내용 :
tc
- 내용 or 작성자 :
cw
- 제목 or 내용 or 작성자 :
tcw
그리고 검색된 목록에서 사용자가 특정게시글을 조회하거나 수정/삭제한 뒤 다시 검색한 게시글 목록으로 돌아가기 위해서는 검색한 목록의 정보(검색조건, 검색키워드)를 유지할 필요가 있다. 그래서 정보를 list.jsp
에 다시 세팅해주도록 jstl의 <c:out>
을 이용한다.
# 페이지 이동 URI 수정
기존의 게시글 목록 페이지 번호와 게시글 제목에는 아래와 같이 URI링크가 걸려있는데 검색한 게시글의 목록 정보를 유지하기 위해서는 &searchType=값&keyword=값
과 같은 GET방식으로 URI에 붙이는 작업이 필요하다.
- 게시글 목록 페이지 번호의 링크 :
<a href="/article/paging/search/list?page=2&pagePageNum=10">
- 게시글 제목의 링크 :
<a href="/article/paging/read?page=1&perPageNum=10&articleNo=1000">
PageMaker
클래스에 URI 자동생성 메서드 추가하기
makeSearch()
메서드에 검색조건과 검색키워드를 처리를 추가로 작성해주고, encoding()
메서드에서는 검색키워드를 인코딩처리를 위한 로직을 구현해준다.
list.jsp
의 페이지 번호 링크 수정
위에서 새로 추가한 메서드를 호출하여 목록 페이지의 번호 링크에 검색조건과 검색키워드가 추가된 URI가 생성되도록 아래와 같이 수정해준다.
list.jsp
의 게시글 조회 링크 수정
게시글 제목에 걸린 게시글 조회 링크도 아래와 같이 수정해준다.
# 검색버튼 동작 처리
검색버튼 클릭 이벤트가 발생하면 기존의 URI에 검색조건과 검색키워드를 GET방식으로 붙여 요청하게 처리한다. 그리고 encodeURIComponent()
은 URI로 데이터를 전달하기 위해서 문자열을 인코딩해준다.
# 검색처리를 위한 목록페이지 완성 모습

4. 검색 처리를 위한 영속 계층 구현 : MyBatis 동적 SQL
# ArticleDAO
인터페이스
검색된 목록과 검색된 게시글의 갯수를 리턴하는 추상 메서드를 선언해준다.
# ArticleDAOImpl
클래스
ArticleDAO
인터페이스에 선언된 추상메서드를 오버라이드하여 구현해준다.
# MyBatis의 표현식
화면에서 사용자가 선택한 검색조건에 따라 검색요청을 처리하는 SQL문이 달라지기 때문에 이를 처리하기 위해서는 Mybatis SQL Mapper에서 동적 SQL문을 작성해줘야한다. 동적 SQL문을 작성하기 위해 표현식을 간단하게 정리해보자.
if
: 코드로 작성할 때의 if구문에 대한 처리
choose(when, otherwise)
: switch와 같은 상황에 대한 처리
trim(where, set)
: 로직을 처리하면서 필요한 구문을 변경
foreach
: 컬렉션에 대한 순환처리
# articleMapper.xml
에서 동적 SQL문 작성
<if>
문을 통해 동적 SQL문을 통해 상황에 맞게 검색을 처리하도록 코드를 작성해준다. 그리고 SQL문이 중복되어 사용될 경우 <include>
를 통해 SQL 중복을 제거하고, SQL의 재사용을 가능하게 처리해준다.
# 동적 SQL 테스트
테스트 코드
테스트 결과
INFO : com.doubles.mvcboard.article.ArticleDAOTest - ======================
INFO : jdbc.connection - 1. Connection opened
INFO : jdbc.audit - 1. Connection.new Connection returned
INFO : jdbc.audit - 1. Connection.getAutoCommit() returned true
INFO : jdbc.audit - 1. PreparedStatement.new PreparedStatement returned
INFO : jdbc.audit - 1. Connection.prepareStatement(SELECT
article_no,
title,
content,
writer,
regdate,
viewcnt
FROM tbl_article
WHERE article_no > 0
AND title LIKE CONCAT('%', ?, '%')
ORDER BY article_no DESC, regdate DESC
LIMIT ?, ?) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@2657d4dd
INFO : jdbc.audit - 1. PreparedStatement.setString(1, "999") returned
INFO : jdbc.audit - 1. PreparedStatement.setInt(2, 0) returned
INFO : jdbc.audit - 1. PreparedStatement.setInt(3, 10) returned
INFO : jdbc.sqlonly - SELECT article_no, title, content, writer, regdate, viewcnt FROM tbl_article WHERE article_no
> 0 AND title LIKE CONCAT('%', '999', '%') ORDER BY article_no DESC, regdate DESC LIMIT 0,
10
INFO : jdbc.sqltiming - SELECT article_no, title, content, writer, regdate, viewcnt FROM tbl_article WHERE article_no
> 0 AND title LIKE CONCAT('%', '999', '%') ORDER BY article_no DESC, regdate DESC LIMIT 0,
10
{executed in 10 msec}
INFO : jdbc.audit - 1. PreparedStatement.execute() returned true
INFO : jdbc.resultset - 1. ResultSet.new ResultSet returned
INFO : jdbc.audit - 1. PreparedStatement.getResultSet() returned net.sf.log4jdbc.sql.jdbcapi.ResultSetSpy@475c9c31
INFO : jdbc.resultset - 1. ResultSet.getMetaData() returned com.mysql.jdbc.ResultSetMetaData@5276d6ee - Field level information:
com.mysql.jdbc.Field@71687585[catalog=spring_ex,tableName=tbl_article,originalTableName=tbl_article,columnName=article_no,originalColumnName=article_no,mysqlType=3(FIELD_TYPE_LONG),flags= AUTO_INCREMENT PRIMARY_KEY, charsetIndex=63, charsetName=US-ASCII]
com.mysql.jdbc.Field@1807f5a7[catalog=spring_ex,tableName=tbl_article,originalTableName=tbl_article,columnName=title,originalColumnName=title,mysqlType=253(FIELD_TYPE_VAR_STRING),flags= BINARY, charsetIndex=83, charsetName=UTF-8]
com.mysql.jdbc.Field@1b919693[catalog=spring_ex,tableName=tbl_article,originalTableName=tbl_article,columnName=content,originalColumnName=content,mysqlType=253(FIELD_TYPE_VAR_STRING),flags= BINARY BLOB, charsetIndex=83, charsetName=UTF-8]
com.mysql.jdbc.Field@7fb4f2a9[catalog=spring_ex,tableName=tbl_article,originalTableName=tbl_article,columnName=writer,originalColumnName=writer,mysqlType=253(FIELD_TYPE_VAR_STRING),flags= BINARY, charsetIndex=83, charsetName=UTF-8]
com.mysql.jdbc.Field@4dc27487[catalog=spring_ex,tableName=tbl_article,originalTableName=tbl_article,columnName=regdate,originalColumnName=regdate,mysqlType=7(FIELD_TYPE_TIMESTAMP),flags= BINARY, charsetIndex=63, charsetName=US-ASCII]
com.mysql.jdbc.Field@6a4f1a55[catalog=spring_ex,tableName=tbl_article,originalTableName=tbl_article,columnName=viewcnt,originalColumnName=viewcnt,mysqlType=3(FIELD_TYPE_LONG),flags=, charsetIndex=63, charsetName=US-ASCII]
INFO : jdbc.resultset - 1. ResultSet.getType() returned 1003
INFO : jdbc.resultset - 1. ResultSet.next() returned true
INFO : jdbc.resultset - 1. ResultSet.getInt(article_no) returned 999
INFO : jdbc.resultset - 1. ResultSet.wasNull() returned false
INFO : jdbc.resultset - 1. ResultSet.getString(title) returned 999번째 글 제목입니다...
INFO : jdbc.resultset - 1. ResultSet.wasNull() returned false
INFO : jdbc.resultset - 1. ResultSet.getString(content) returned 999번재 글 내용입니다...
INFO : jdbc.resultset - 1. ResultSet.wasNull() returned false
INFO : jdbc.resultset - 1. ResultSet.getString(writer) returned user09
INFO : jdbc.resultset - 1. ResultSet.wasNull() returned false
INFO : jdbc.resultset - 1. ResultSet.getTimestamp(regdate) returned 2018-03-06 19:01:55.0
INFO : jdbc.resultset - 1. ResultSet.wasNull() returned false
INFO : jdbc.resultset - 1. ResultSet.getInt(viewcnt) returned 0
INFO : jdbc.resultset - 1. ResultSet.wasNull() returned false
INFO : jdbc.resultsettable -
|-----------|-----------------|-----------------|-------|----------------------|--------|
|article_no |title |content |writer |regdate |viewcnt |
|-----------|-----------------|-----------------|-------|----------------------|--------|
|999 |999번째 글 제목입니다... |999번재 글 내용입니다... |user09 |2018-03-06 19:01:55.0 |0 |
|-----------|-----------------|-----------------|-------|----------------------|--------|
INFO : jdbc.resultset - 1. ResultSet.next() returned false
INFO : jdbc.resultset - 1. ResultSet.close() returned void
INFO : jdbc.audit - 1. PreparedStatement.getConnection() returned net.sf.log4jdbc.sql.jdbcapi.ConnectionSpy@3aa078fd
INFO : jdbc.audit - 1. Connection.getMetaData() returned com.mysql.jdbc.JDBC4DatabaseMetaData@d23e042
INFO : jdbc.audit - 1. PreparedStatement.getMoreResults() returned false
INFO : jdbc.audit - 1. PreparedStatement.getUpdateCount() returned -1
INFO : jdbc.audit - 1. PreparedStatement.close() returned
INFO : jdbc.connection - 1. Connection closed
INFO : jdbc.audit - 1. Connection.close() returned
INFO : com.doubles.mvcboard.article.ArticleDAOTest - 999 : 999번째 글 제목입니다...
INFO : com.doubles.mvcboard.article.ArticleDAOTest - ======================
INFO : jdbc.connection - 2. Connection opened
INFO : jdbc.audit - 2. Connection.new Connection returned
INFO : jdbc.audit - 2. Connection.getAutoCommit() returned true
INFO : jdbc.audit - 2. PreparedStatement.new PreparedStatement returned
INFO : jdbc.audit - 2. Connection.prepareStatement(SELECT
COUNT(article_no)
FROM tbl_article
WHERE article_no > 0
AND title LIKE CONCAT('%', ?, '%')) returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@2254127a
INFO : jdbc.audit - 2. PreparedStatement.setString(1, "999") returned
INFO : jdbc.sqlonly - SELECT COUNT(article_no) FROM tbl_article WHERE article_no > 0 AND title LIKE CONCAT('%', '999',
'%')
INFO : jdbc.sqltiming - SELECT COUNT(article_no) FROM tbl_article WHERE article_no > 0 AND title LIKE CONCAT('%', '999',
'%')
{executed in 3 msec}
INFO : jdbc.audit - 2. PreparedStatement.execute() returned true
INFO : jdbc.resultset - 2. ResultSet.new ResultSet returned
INFO : jdbc.audit - 2. PreparedStatement.getResultSet() returned net.sf.log4jdbc.sql.jdbcapi.ResultSetSpy@51891008
INFO : jdbc.resultset - 2. ResultSet.getMetaData() returned com.mysql.jdbc.ResultSetMetaData@2f953efd - Field level information:
com.mysql.jdbc.Field@f68f0dc[catalog=,tableName=,originalTableName=,columnName=COUNT(article_no),originalColumnName=,mysqlType=8(FIELD_TYPE_LONGLONG),flags= BINARY, charsetIndex=63, charsetName=US-ASCII]
INFO : jdbc.resultset - 2. ResultSet.getType() returned 1003
INFO : jdbc.resultset - 2. ResultSet.next() returned true
INFO : jdbc.resultset - 2. ResultSet.getInt(COUNT(article_no)) returned 1
INFO : jdbc.resultset - 2. ResultSet.wasNull() returned false
INFO : jdbc.resultsettable -
|------------------|
|count(article_no) |
|------------------|
|1 |
|------------------|
INFO : jdbc.resultset - 2. ResultSet.next() returned false
INFO : jdbc.resultset - 2. ResultSet.close() returned void
INFO : jdbc.audit - 2. PreparedStatement.getConnection() returned net.sf.log4jdbc.sql.jdbcapi.ConnectionSpy@24c1b2d2
INFO : jdbc.audit - 2. Connection.getMetaData() returned com.mysql.jdbc.JDBC4DatabaseMetaData@7dc19a70
INFO : jdbc.audit - 2. PreparedStatement.getMoreResults() returned false
INFO : jdbc.audit - 2. PreparedStatement.getUpdateCount() returned -1
INFO : jdbc.audit - 2. PreparedStatement.close() returned
INFO : jdbc.connection - 2. Connection closed
INFO : jdbc.audit - 2. Connection.close() returned
INFO : com.doubles.mvcboard.article.ArticleDAOTest - searched articles count : 1
위에서 작성한 동적 SQL문이 제대로 작동하는지 확인하기 위해 테스트 코드를 작성하고, 확인해보자.
5. 비지니스 계층 구현
검색처리를 위한 컨트롤러와 영속계층을 연결해주는 비지니스 로직은 아래와 같이 작성해준다.
# ArticleService
인터페이스
# ArticleServiceImpl
클래스
6. 컨트롤러(ArticlePagingSearchController
) 수정
# 목록 페이지 매핑 메서드 수정
동적 SQL을 통해 검색처리된 게시글의 목록이 출력되게 아래와 같이 코드를 수정해준다.
# 조회 페이지 매핑 메서드 수정
게시글의 검색정보가 유지되도록 read()
메서드의 매개변수 타입을 SearchCriteria
로 변경한다.
# 수정 페이지 매핑, 수정처리 매핑 메서드 수정
게시글의 검색정보가 유지되도록 modifyGET()
메서드의 매개변수 타입을 SearchCriteria
로 변경한다.
수정처리가 완료되고, 목록페이지로 리다이렉트될 때 검색정보가 유지될 수 있도록 아래와 같이 검색조건과 검색키워드를 redirectAttributes.addAttribute()
에 저장해준다.
# 삭제 처리 매핑 메서드 수정
수정처리와 마찬가지로 삭제처리가 완료되고, 목록페이지로 리다이렉트될 때 검색정보가 유지될 수 있도록 아래와 같이 검색조건과 검색키워드를 redirectAttributes.addAttribute()
에 저장해준다.
7. 조회/삭제, 수정 페이지 수정
# 조회 페이지, 삭제처리(read.jsp
)
HTML코드
<input>
타입의 hidden
속성으로 검색조건과 검색키워드를 추가해준다.
JS코드
각각 버튼의 클릭이벤트가 발생하면 해당 페이지로 이동하기 위해 attr()
로 속성을 제어해준다.
# 수정 페이지(modify.jsp
)
HTML코드
조회페이지와 마찬가지로 수정페이지에서도 <input>
타입의 hidden
속성으로 검색조건과 검색키워드를 추가해준다.
JS코드
목록 버튼 클릭이벤트가 발생하면 검색한 목록페이지로 이동하기 위해 유지된 정보값들을 아래와 같이 GET방식으로 URI에 작성해준다.
8. 최종적으로 구현된 검색처리 모습
# 99로 게시글 검색 이후 목록페이지

# 게시글 검색 이후 조회 페이지

# 게시글 검색 이후 수정 페이지

# 수정 처리후 목록페이지로 리다이렉트

# 삭제 처리후 목록페이지로 리다이렉트
