초간단 CURD, 검색, 페이징, 정렬 구현 완료
Opensprout Nexus에서 제공하는 스프링 최신 버전(3.0 기반)을 사용했고 스프링 번들 저장소에 있는 하이버네이트 라이브러리를 사용했습니다. 그밖에는 뭐 서블릿 API, JSP, JSTL, taglib 등을 사용했고 로깅은 slf4j-log4j를 사용했습니다. DB는 별도의 설치가 필요없게 derby를 사용했고 c3po 풀링 라이브러리를 사용했습니다.
빌드는 메이븐으로 했고 War 배포 방식은 inplace로 했으며 라이브러리를 가져올 저장소는 모두 pom.xml에 등록해 두었습니다.(war 플러긴을 못 가져오는 버그가 발생하고 있다는데 조만간 해결되리라 생각합니다.)
도메인은 Member 하나이고, 하이버네이트 애노테이션이 추가되서 POJO라고 보기는 좀 힘들겠습니다. 하이버네이트 설정을 XML로 바꿀까 생각중입니다. 아니면 스프링처럼 JavaConfig 같은 스타일을 사용할 수는 없을지 궁금하네요. 지원해준다면 기꺼이 그렇게 바꾸고 싶습니다. 도메인 클래스는 POJO로 유지하고 하이버네이트 설정은 밖으로 분리할 수 있으니까요. 대신 그 설정 파일이 자바 파일이라면 이름 변경과 같은 리팩터링에도 유기적으로 반응할테니 JavaConfig와 유사한 장점을 지닐 수 있지 않을까 싶습니다.
Controller는 스프링 3.0의 URI 템플릿 기능을 사용해봤습니다. 재밌더군요. list와 add를 제외한 view, updat, delete에 적용해봤습니다. 기본 CRUD만 구현했을 때에는 컨트롤러가 썰렁했는데, 검색, 페이징, 정렬 기능이 추가될 때 마다 코드가 조금씩 늘어나기 시작해서 지금은 이런 모습이 되었습니다.
package whiteship.member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import whiteship.domain.Member;
import whiteship.member.support.OrderParam;
import whiteship.member.support.SearchParam;
import whiteship.paging.PageParam;
@Controller
@SessionAttributes("member")
public class MemberController {
@Autowired
MemberService service;
@Autowired
MemberValidator validator;
// Create
@RequestMapping(value = "/member/add", method = RequestMethod.GET)
public String addForm(Model model) {
Member member = new Member();
model.addAttribute(member);
return "member/add";
}
@RequestMapping(value = "/member/add", method = RequestMethod.POST)
public String addForm(Member member, BindingResult result,
SessionStatus status) {
validator.validate(member, result);
if (result.hasErrors()) {
return "member/add";
} else {
service.add(member);
status.isComplete();
return "redirect:/member/list.do";
}
}
// Read
@RequestMapping("/member/list")
public ModelMap list(PageParam pageParam, SearchParam searchParam,
OrderParam orderParam) {
ModelMap modelMap = new ModelMap();
modelMap.addAttribute(service
.getMemberListByPageAndSearchAndOrderParam(pageParam,
searchParam, orderParam));
modelMap.addAttribute(pageParam);
modelMap.addAttribute(searchParam);
modelMap.addAttribute(orderParam);
return modelMap;
}
@RequestMapping("/member/{id}")
public String view(@PathVariable int id, Model model, PageParam pageParam,
SearchParam searchParam, OrderParam orderParam) {
model.addAttribute(service.getMemberById(id));
model.addAttribute(pageParam);
model.addAttribute(searchParam);
model.addAttribute(orderParam);
return "member/view";
}
// Update
@RequestMapping(value = "/member/update/{id}", method = RequestMethod.GET)
public String updateForm(@PathVariable int id, Model model,
PageParam pageParam, SearchParam searchParam, OrderParam orderParam) {
model.addAttribute(service.getMemberById(id));
model.addAttribute(pageParam);
model.addAttribute(searchParam);
model.addAttribute(orderParam);
return "member/update";
}
@RequestMapping(value = "/member/update/{id}", method = RequestMethod.POST)
public String updateForm(PageParam pageParam, SearchParam searchParam,
OrderParam orderParam, Member member, BindingResult result,
SessionStatus status) {
validator.validate(member, result);
if (result.hasErrors()) {
return "member/update";
} else {
service.update(member);
status.isComplete();
return redirectURLWithPageAndSearchAndOrderParam(pageParam,
searchParam, orderParam);
}
}
// Delete
@RequestMapping("/member/delete/{id}")
public String delete(@PathVariable int id, PageParam pageParam,
SearchParam searchParam, OrderParam orderParam) {
service.deleteById(id);
return redirectURLWithPageAndSearchAndOrderParam(pageParam, searchParam, orderParam);
}
private String redirectURLWithPageAndSearchAndOrderParam(
PageParam pageParam, SearchParam searchParam, OrderParam orderParam) {
return redirectURLWithPageAndSearchParam(pageParam, searchParam)
+ "&field=" + orderParam.getField() + "&direction="
+ orderParam.getDirection();
}
private String redirectURLWithPageAndSearchParam(PageParam pageParam,
SearchParam searchParam) {
return pagedListURL(pageParam) + "&name=" + searchParam.getName()
+ "&email=" + searchParam.getEmail();
}
private String pagedListURL(PageParam pageParam) {
return "redirect:/member/list.do?size=" + pageParam.getSize()
+ "&page=" + pageParam.getPage();
}
}
List에서 페이징, 검색, 정렬 정보를 가지고 View로 간 다음 Update와 Delete 또는 뒤로가기(List) 기능에 그 정보들을 사용하거나 넘겨 줍니다. Update와 Delete에서는 해당 정보들을 이요하고, 처리를 끝 낸다음 페이징, 검색, 정렬 정보를 유지한채 List로 다시 넘어갑니다. 따라서 삭제 하기 전에 했던 페이징, 검색, 정렬 정보를 그대로 유지한 화면으로 돌아가게 됩니다.
이게 핵심이었습니다. 이 것 때문에 복잡해진 겁니다. 매개변수를 계속 물고 다녀야 하고, URL은 계속 복잡해지고, 페이징, 검색, 정렬 기능이 추가될 때마다 혹은 검색 변수가 추가될 때 마다 URL이 바뀌게 되는데 URL이 사방에 있기 때문에 수정 작업을 할 곳이 한 두곳이 아닙니다.
1. 일단 컨트롤러에서 URL 만드는 부분을 수정해야 합니다.
2. list.jsp
3. view.jsp
4. update.jsp
총 네 개의 파일을 수정해야 하고, 특히 페이징이 들어있는 list.jsp 파일의 경우 10~12줄 가량의 코드를 손봐야 합니다.
<%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Whiteboard2</title>
</head>
<body>
<div>
<a href="/member/add.do">회원 추가</a>
</div>
<div>
<c:if test="${empty memberList}">
회원 목록이 없습니다.
</c:if>
<c:if test="${! empty memberList}">
<form:form method="GET" commandName="searchParam">
이름: <form:input path="name" />
이메일: <form:input path="email" />
<input type="submit" value="검색" />
</form:form>
페이지 사이즈: ${pageParam.size}<br/>
현재 페이지: ${pageParam.page}<br/>
총 갯수: ${pageParam.totalRowsCount}<br/>
현재 페이지 첫 번째 목록 인덱스: ${pageParam.firstRowNumber}<br/>
<c:if test="${pageParam.totalRowsCount > pageParam.size}">
<a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">처음</a> |
<c:if test="${pageParam.beginPage - 10 > 0}">
<a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">이전</a> |
</c:if>
<c:forEach begin="${pageParam.beginPage}" end="${pageParam.endPage}" varStatus="current">
<c:choose>
<c:when test="${current.count == pageParam.page}">
<a href="/member/list.do?page=${current.count}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}"><strong>${current.count}</strong></a> |
</c:when>
<c:otherwise>
<a href="/member/list.do?page=${current.count}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">${current.count}</a> |
</c:otherwise>
</c:choose>
</c:forEach>
<c:if test="${pageParam.beginPage + 10 < pageParam.totalPage}">
<a href="/member/list.do?page=${current.count + 10}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">다음</a> |
</c:if>
<a href="/member/list.do?page=${pageParam.totalPage}&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">마지막</a>
</c:if>
<table>
<tr>
<th>
<c:choose>
<c:when test="${orderParam.field == 'email' && orderParam.direction == 'asc'}">
<a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=email&direction=desc">이메일V</a>
</c:when>
<c:otherwise>
<a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=email&direction=asc">이메일^</a>
</c:otherwise>
</c:choose>
</th>
<th>
<c:choose>
<c:when test="${orderParam.field == 'name' && orderParam.direction == 'asc'}">
<a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=name&direction=desc">이름V</a>
</c:when>
<c:otherwise>
<a href="/member/list.do?page=1&size=${pageParam.size}&name=${searchParam.name}&email=${searchParam.email}&field=name&direction=asc">이름^</a>
</c:otherwise>
</c:choose>
</th>
</tr>
<c:forEach var="member" items="${memberList}">
<tr>
<td><a href="/member/${member.id}.do?size=${pageParam.size}&page=${pageParam.page}&name=${searchParam.name}&email=${searchParam.email}&field=${orderParam.field}&direction=${orderParam.direction}">${member.email}</a></td>
<td>${member.name}</td>
</tr>
</c:forEach>
</table>
</c:if>
</div>
</body>
</html>
이게 현재 list.jsp 파일입니다. 이클립스에서 보면 96줄에 해당합니다.
자 이제는 이것을 어떻게 하면 깔끔하고 편리하게 다듬을지 고민할 시간입니다. 좋은 의견 있으신 분들은 댓글 주시면 감사하겠습니다~ ^^
ps: 티스토리에서 아직도 코드 하이라이팅 플러긴 같은거 안 나왔죠?? 티스토리 서비스 한지가 몇 년 짼데 아직도 안 나와~~~ ㅠ.ㅠ