사실 이 작업은 따로 따로 했지만 중간에 정리해 두지 않아서 다시 코드를 빼면서 정리하기는 귀찮아서 한번에 정리한다.

먼저 화면에서 요청을 보내도록 한다.
                $("#smdis-grid").jqGrid({
                    caption: '코드 목록',
                    url:'/base/code/list?' + $("#searchForm").serialize(),
                    colNames:['id', '코드값', '코드명', '설명'],
                    colModel :[
                        {name:'id', index:'id', width:55, hidden:true},
                        {name:'code', index:'code', width:80},
                        {name:'name', index:'name', width:90},
                        {name:'descr', index:'descr', width:90},
                    ],
                    autoencode:true
                });
그리드 구현은 이걸로 끝이다. 
다음은 /base/code/list URL을 처리한 핸들러를 구현한다.
    @RequestMapping
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        model.addAttribute("codeList", codeService.list(pageParam, searchParam));
    }
화면에서 넘어온 파라미터들 중에 CodeSearchParam과 PageParam을 구분해서 받는다. @ModelAttribute가 적용된것이며 스프링 바인딩 기능이 적용되어 요청에 들어있는 파라미터들이 저 객체들의 속성으로 들어오게 된다.
처음엔 잘 넘어오는지 궁금해서 sout을 사용해서 콘솔에 찍어보면서 확인을 했었다. 특히 잘 넘어오더라도 한글이 잘 찍히는지 확인했다. 한글이 ??.?? 이렇게 넘어왔고, 파이어버그로 봤을 때는 화면에서 한글은 잘 넘어간 것 같다. 
톰캣 server.xml을 설정해줘야겠다.
    <Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8" />
이 뒤로는 한글이 잘 넘어왔다.
public class CodeSearchParam {
    private String name;
    private String code;
}
검색 옵션이고, 도메인 코드에 따라 검색 매개변수가 달랄 질 수 있으니 매번 만들어줘야겠다.
public class PageParam {
    //화면으로 넘여줄 값.
    int totalPageSize; // 전체 페이지 갯수
    int listSize; // 전체 목록 갯수
    int currentPageNumber; // 현재 보여줄 페이지
    //화면에서 넘어오는 값
    int page; //요청 받은 페이지
    int rows; //한 화면당 보여줄 줄 수
    String sidx; //정렬 기준 컬럼
    String sord; //정렬 방향
}
이건 여러 컨트롤러에서 공통으로 사용할 수 있는 코드다. 따라서 두 클래스 패키지를 잘 분리해둔다. CodeSearchParam은 base/code/support에 넣고 PageParam은 common/page 에 뒀다. PageParam에 주석으로 속성들을 분리해 뒀지만 사실 별도의 클래스로 나눌까 생각도 해봤다. 그런데 좀 귀찮았다. 어차피 서로 관련있는 정보들이기 때문에 그냥 한 곳에 뒀다.
다음은 서비스 인터페이스에 필요한 걸 정의하고 서비스 구현 클래스에서 메서드를 구현한다.
    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        pageParam.initCurrentPageInfosWith(codeDao.totalSize(searchParam));
        return codeDao.list(pageParam, searchParam);
    }
이전에 만들었던 dao 코드를 써먹을 수 있게됐다. 그런데 변경해야겠다. 전체 사이즈를 구할때는 searchParam만 있으면 되고 실제 list를 가져올 땐 둘 다 넘겨줘야된다.
    public int totalSize(CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        applySearchParam(c, searchParam);
        return (Integer)c.setProjection(Projections.count("id"))
            .uniqueResult();
    }
    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        applySearchParam(c, searchParam);
        if(pageParam != null){
            c.setFirstResult(pageParam.getFirstRowNumber());
            c.setMaxResults(pageParam.getRows());
        }
        return c.list();
    }
    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        if(!searchParam.getCode().isEmpty()){
            c.add(Restrictions.ilike("code", searchParam.getCode(), MatchMode.ANYWHERE));
        }
        if(!searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }
    }
Criteria API를 사용해서 검색 옵션과 페이징 처리를 했다. PageParam 클래스에는
    public int getFirstRowNumber() {
        return Math.max((getPage() - 1) * getRows() ,0);
    }
    public void initCurrentPageInfosWith(int totalListSize) {
        setListSize(totalListSize);
        setTotalPageSize((int)Math.ceil((double)totalListSize /getRows()));
        setCurrentPageNumber(getPage());
    }
이런 코드가 추가됐다. 이제 테스트좀 해볼까. 인텔리J에 단축키를 Ctrl+J로 등록해놨다. 이클립스에서는 별도로 플러그인을 설치해야 이런 기능을 사용할 수 있는데.. 귀찮지 아니한가?
public class PageParamTest {
    @Test
    public void testGetFirstRowNumber() throws Exception {
        PageParam pageParam = new PageParam();
        pageParam.setRows(20);
        pageParam.setPage(0);
        assertThat(pageParam.getFirstRowNumber(), is(0));
        pageParam.setPage(-2);
        assertThat(pageParam.getFirstRowNumber(), is(0));
        pageParam.setPage(1);
        assertThat(pageParam.getFirstRowNumber(), is(0));
        pageParam.setPage(3);
        assertThat(pageParam.getFirstRowNumber(), is(40));
    }
    @Test
    public void testInitCurrentPageInfosWith() throws Exception {
        PageParam pageParam = new PageParam();
        pageParam.setRows(20);
        pageParam.initCurrentPageInfosWith(20);
        assertThat(pageParam.getListSize(), is(20));
        assertThat(pageParam.getTotalPageSize(), is(1));
        pageParam.initCurrentPageInfosWith(41);
        assertThat(pageParam.getListSize(), is(41));
        assertThat(pageParam.getTotalPageSize(), is(3));
    }
}
CodeDao도 테스트 해주지 않으면 왠지 섭섭하다. Criteria API를 잔뜩 썼는데 제대로 쓴 건지 확인차 학습차 확인 해보자.
 
   /**
     * testData2.xml
     *
     * <dataset>
            <code id="1" name="블랙" code="BLK" />
            <code id="2" name="레드" code="RED" />
            <code id="3" name="그린" code="GRN" />
            <code id="4" name="블루" code="BLU" />
            <code id="5" name="옐로" code="YLW" />
            <code id="6" name="골드" code="GLD" />
            <code id="7" name="실버" code="SLV" />
        </dataset>
     */
    @Test
    public void testToTalSize() throws Exception {
        insertXmlData("testData2.xml");
        CodeSearchParam codeSearchParam = new CodeSearchParam();
        codeSearchParam.setCode("L");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));
    }
앗 이런 돌려보니 NullPointerException이다. 
        if(!searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }
여기서 발생했다. 아무래도 null 체크까지 추가해야겠다. @_@
    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        if(searchParam.getCode() != null && !searchParam.getCode().isEmpty()){
            c.add(Restrictions.ilike("code", searchParam.getCode(), MatchMode.ANYWHERE));
        }
        if(searchParam.getName() != null && !searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }
    }
코드 참.. 거시기 하다. @_@ 그래도 일단 테스트부터 성공시키자. 오퀘 테스트가 잘 돌았다. 리팩토링 하자.
public class CriteriaUtils {
    public static void addOptionalLike(Criteria c, String fieldName, String value) {
        if(value != null && !value.isEmpty()){
            c.add(Restrictions.ilike(fieldName, value, MatchMode.ANYWHERE));
        }    
    }
}
이런 유틸 하나를 만들었다.
    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
    }
DAO 코딩이 한결 간편해졌다. 테스트 해보자, 잘 돌아간다. 테스트를 보강하자.
    @Test
    public void testToTalSize() throws Exception {
        insertXmlData("testData2.xml");
        CodeSearchParam codeSearchParam = new CodeSearchParam();
        //code 겁색
        codeSearchParam.setCode("L");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));
        //대소문자 구분 안함.
        codeSearchParam.setCode("l");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));
        //name 검색추가
        codeSearchParam.setName("블");
        assertThat(codeDao.totalSize(codeSearchParam), is(2));
    }
잘 돈다. 이제 사이즈 구하는 쿼리는 안심이다.
    @Test
    public void testList() throws Exception {
        insertXmlData("testData2.xml");
        CodeSearchParam codeSearchParam = new CodeSearchParam();
        PageParam pageParam = new PageParam();
        pageParam.setRows(5); // 한 페이지에 5개씩 보자.
        pageParam.setPage(1); // 첫페이지.
        List<Code> codeList = codeDao.list(pageParam, codeSearchParam);
        assertThat(codeList.size(), is(5));
        System.out.println(codeList);
        pageParam.setPage(2);
        codeList = codeDao.list(pageParam, codeSearchParam);
        assertThat(codeList.size(), is(2));
    }
페이지 처리 테스트 코드인데 사실 좀 부실하다;;  그래도 이정도 해놓고 화면에서 확인해보니 잘 나온다. 오퀘.