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: 티스토리에서 아직도 코드 하이라이팅 플러긴 같은거 안 나왔죠?? 티스토리 서비스 한지가 몇 년 짼데 아직도 안 나와~~~ ㅠ.ㅠ