[GAE 시리즈] 7. 스프링 @MVC

GAE 시작하기 메뉴얼을 따라하면서 들었던 생각은 스프링을 어서 도입해봐야겠다는 것이었다. 특히 PMF 라는 클래스를 만들때 간절했다. JDO의 PersistenceManagerFacotry를 싱글톤으로 사용하려고 만든 클래스인데.. 전혀 좋은 코드가 아니었다. 그뿐아니라 자바 코드와 HTML이 섞여있는 guestbook.jsp도 마찬가지이고, HttpServlet을 직접 상속해서 구현한 GurestbookServlet과 SignGuestbookServlet도 스프링 @MVC 컨트롤러로 고치고 싶었다.

그래서 일단해야 할 일은 스프링의 초간단 @MVC 컨트롤러를 추가하고 그게 동작하는지 확인하는 일이었다. 이전에 라이브러리는 넣어둔 상태라 간단하게 설정만 조금 추가하면 됐다.
web.xml
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext*.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
spring-servlet.xml
<context:component-scan base-package=”whiteship” use-default-filters=”false”>
<context:include-filter type=”annotation”
expression=”org.springframework.stereotype.Controller” />
</context:component-scan>
applicationContext.xml
<context:component-scan base-package=”whiteship”>
<context:exclude-filter type=”annotation”
expression=”org.springframework.stereotype.Controller” />
</context:component-scan>
그리고 컨트롤러
@Controller
@RequestMapping(“/hello”)
public class GreetingController {
    @RequestMapping(“/{name}”)
    public String hello(@PathVariable String name, Model model){
        model.addAttribute(“name”, name);
        return “/WEB-INF/views/hello.jsp”;
    }
}
그리고 뷰
<%@ page language=”java” contentType=”text/html; charset=UTF-8″ pageEncoding=”UTF-8″%>
<%@ page isELIgnored=”false” %>
<html>
  <head><title>Simple jsp page</title></head>
  <body>잘잤니~ ${name}</body>
</html>
끝이다. 잘 돌아간다.

Toby’s 스프링 3.0 스프링 웹 기술과 스프링 MVC를 읽고서…

난 그동안 스프링 MVC를 잘 몰랐다. 다행이다. 이제는 좀 알것같다. 대충 머리속에 그려진다. 지난번 소녀시대 스프링 @MVC를 발표할 때 아주 중요한 스프링 MVC 구성요소 몇가지를 소개했었다. 물론 그것들 보다는 소녀시대 이름만 외우고 가신 분들이 많겠지만;; 이 책에서는 모든 구성요소를 다 설명해주며 그 구성요소군에 속해있는 여러 요소들의 특징과 사용법도 설명해주고 있다. 물론 지면 관계상 뷰 기술중 대부분은 생략이지만 간단한 확장 방법을 통해서 충분히 다른 것들의 사용법도 예상할 수 있었다.

가장 기가막힌 부분은 스프링 Controller(API)와 핸들러 어댑터를 확장해서 새로운 형태의 컨트롤러를 만들어 사용하는 방법이었다. 이 부분은 정말 스프링 MVC 정복기라고 부르고 싶은 부분이다. 물론 호불호가 갈릴만한 부분이기도 하다.
하지만 그런 지식과 실천은 꼭 필요하다. 왜냐면 스프링이 제공하는 웹 계층 구현방법이 너무 다양하기 때문이다. 그것을 정책으로 통일시킬 수 있겠지만 더 좋은건 그 정책을 코드에 반영하는 것이 필요하고 그것을 기반으로 코드를 작성하면 규모가 큰 프로젝트라 하더라도 코드의 일관성을 유지할 수 있을 것이다. 프레임워크 개발이 필요한 이유가 바로 이때문이 아닐까? 프레임워크를 사용하는 이유는 둘 중 하나인것 같다. 프레임워크가 강요하는 정책을 따르기로 결정했거나, 그 프레임워크를 기반으로 프로젝트 고유의 정책과 특성을 반영한 추상계층을 만들어 낼 수 있기 때문인 것 같다. 스프링은 기본적인 방법들은 제공하지만 어떤 방법을 강요하지는 않는다. 따라서 개발자마다 코딩 스타일이 많이 다를 수 있는데 그것을 해결할 수 있는 방법을 소개해준다.
또한 스프링 3.0 최신 기술 중에 컨텐츠 네고 기법을 사용한 뷰 리졸버 설명이 기가 막힌다. 전혀 간단하지 않은 동작 원리를 쉽게 풀어 설명해주어 이해하기 편했다. 이 부분은 레퍼런스나 API에서도 설명이 빈약해서 직접 소스코드를 분석해가며 쓰셨다고 하니 그저 감사할 따름이다.
마지막으로 이 책에서 정의한 프레임워크 개발에 대한 글을 (저자의 허락-’살짝쿵 스포일러를 넣어도 대
‘-하에) 옮겨본다. 

프레임워크 개발이란 이미 있는 기술을 조합해서 어떻게 쓸지 결정하고 툴이나 공통 모듈 정도를 만들어 놓는 것이 아니다. 프레임워크란 애플리케이션의 코드가 효율적인 방법으로 개발되서 사용될 수 있도록 새로운 틀(framework)을 만드는 작업이다.”

ps: 이번 장을 보면서 새로운 형태의 OSAF 3.0을 개발해야겠고 다짐했다.

Toby’s 스프링 3.0 스프링 웹 기술과 스프링 MVC를 읽으며..

아직 다 못봣다. 1/3정도 봤는데 기본적인 스프링 MVC 구성요소와 흐름을 상세하게 설명해주고 있다. 지난 KSUG 발표때 소녀시대 이용해서 설명했던 바로 그 부분이다. 그 뒤에는 스프링 MVC 테스트가 나온다. 캬.. 이 부분이 대박이다.

스프링 웹 계층 테스트는 잘 만들질 않는다. 귀찮다. Mock머시기 클래스의 객체들 만들고 컨트롤러 실행시켜서 ModelAndView 받아서 확인하는 아주 간단한 테스트 조차 너무 귀찮다. 그런데 그런 것 말고 어떤 URL을 스프링이 처리하는지.. 안하는지 보려면 DispatcherServlet까지도 테스트 범위에 들어가야 한다. 그리고 그 결과를 가져와야 하는데 그게 막상 해보면 알겠지만 좀 귀찮은 것이 아니다. 그래서 그냥 스프링이 알아서 테스트 했겠지 생각하고 넘어가기 일수다.
하지만 그러면 안된다. 제대로 확인도 안해보고 막연한 상상에 기대어 살면 안된다. @RequestMapping 같이 생소한 방법을 사용할 땐 더더욱 그렇다. 그래서 테스트가 필요하다. 단, 편하게 할 수 있어야 되는데 바로 그 “편하게 MVC 테스트하는 방법”을 제공해 준다. 여기서 소개한 방법을 응용해서 얼마든지 자신이 확인하고 싶은 것을 확인할 수 있다. 그럼 더이상 막연한 상상에 기대어 코딩하는 일은 없어질 것이다. 좀 더 확신을 가지고 마음 편한 상태에서 코딩할 수 있게 될 것 같다.
물론,, 테스트 안해도 맘편하다면.. 어쩔 수 없다.ㅋ

[Spring 3.0 & Atlassian] RestTemplate으로 Confluence의 Space 목록 가져오기

public class ConfluenceRestTest {
    public static final String SPACE_LIST_URL = “http://dev.springsprout.org/wiki/rest/prototype/1/space?os_authType=basic”;
    /**
     */
    @Test
    public void testGetWikiSpaceList() {
        RestTemplate restTemplate = new RestTemplate();
        String result = restTemplate.execute(SPACE_LIST_URL, HttpMethod.GET,
                new JsonRestRequestCallBack(),
                new BodyToStringRestResponseExtractor());
        System.out.println(result);
    }
    static class JsonRestRequestCallBack implements RequestCallback {
        public void doWithRequest(ClientHttpRequest clientHttpRequest) throws IOException {
            HttpHeaders headers = clientHttpRequest.getHeaders();
            headers.add(“Authorization”, “Basic XXXXXXXX(base64 encoded username:passwd)”);
            headers.add(“Accept”, “application/json”);
        }
    }
    static class BodyToStringRestResponseExtractor implements ResponseExtractor<String> {
        public String extractData(ClientHttpResponse clientHttpResponse) throws IOException {
            return convertStreamToString(clientHttpResponse.getBody());
        }
        ….
    }
}
봄싹 위키를 컨플루언스로 바꾸었기 때문에 기존 메뉴를 컨플루언스에 있는 정보를 끌어다 보여주는 것으로 바꾸려고 합니다. 그래서 파일럿(?) 겸사해서 코딩을 해봤는데 잘 됐네요.
{“expand”:”space”,”space”:[{"name":"개발툴","key":"DTL","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/DTL"},"description":"개발툴 관련 내용을 정리하는 공간입니다."},{"name":"베타리딩","key":"BTR","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/BTR"},"description":"봄싹 베타리딩 관련 공간입니다."},{"name":"봄싹 Modules","key":"BSM","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/BSM"},"description":"봄싹 Module과 관련된 정보를 정리하는 공간입니다."},{"name":"봄싹 개발","key":"SSD","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/SSD"},"description":"봄싹에서 진행하는 개발과 관련된 정보를 정리하는 공간입니다."},{"name":"스프링 3.0 레퍼런스 번역","key":"SRK","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/SRK"},"description":"스프링 3.0 레퍼런스를 번역하는 공간입니다."},{"name":"스프링 시큐리티 3.0 레퍼런스 번역","key":"springsecurity","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/springsecurity"},"description":""},{"name":"스프링소스 블로그 번역","key":"SBK","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/SBK"},"description":"스프링소스 블로그를 번역하는 공간입니다."},{"name":"연습장","key":"ds","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/ds"},"description":"봄싹 위키 사용법을 익히는 공간입니다."},{"name":"자바","key":"JAVA","link":{"rel":"self","href":"http://dev.springsprout.org/wiki/rest/prototype/1/space/JAVA"},"description":"자바 학습 공간입니다."}]}
막상 해보고 나니 고민이 생기네요. “저 작업을 뷰에서 할 것이냐 서버단에서 할 것이냐?”
뷰에서 하면 저 페이지로 이동 할 때 마다 요청을 보낼테니 Confluence 응답이 늦어지면 문제고 Confluence 입장에서 부담이 될 수도 있고, Confluence가 죽었을땐 위키 페이지까지 마비가 되지만 구현하는 입장에선 뷰 코드만 지지고 볶으면 되니까 간단할 것 같은데..
서버단에서 하면 DB에 저장해두거나 캐싱을 해둘 수 있으니 Confluence의 부담도 덜고 Confluence가 죽어도 위키 정보는 보여줄 수 있지만 구현하면서 이것 저것 신경쓸게 많아지네;; 정보를 읽어올 주기, 배치로 돌릴 것이냐? JSON말고 XML로 받아온다음 OXM을 이용해서 XML->Object로 바로 변환해버리는 ResponseExtracter를 만들까나.. 캐싱은 이미 Ehcache를 하이버 땜시 쓰고 있으니 캐싱도 해보고?
흠.. 서버단에서 할까나 어쩔까나.. Confluence가 죽어있으면 어차피 봄싹 Wiki에서 목록 본다음에 링크 클릭해도 이동을 못하니까 그냥.. 뷰에서 읽을까나.. +_+ 그럼 나 머하러 RestTemplate 사용한거지.. @_@.. 기왕에 썼으니까 OXM도 적용해서 서버 단에서 해버렷?ㅋ 

스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 1

간만에 11시에 집에 왔습니다. 음… 오랜만이네요.

오늘은 jQuery Ajax로 자동완성 기능을 만들다가 왔습니다. 스프링 JSONView를 이용해서 스프링이 보내준 JSON을 가지고 자동완성을 만드는 겁니다.

1. MappingJacksonJsonView 라는 클래스가 있는데, 좀 전까지만 해도 BindingJacksonJsonVIew였습니다. 아직은 배포된 버전에 이 클래스가 없고, 개발중인 프로젝트에 fisheye에 접속해서 보면 소스를 볼 수 있습니다. 또는 톱님이 배포하시는 최신 스프링을 사용하시면 이용할 수 있을 겁니다.

2. 일단 필요한 라이브러리를 구합니다. 최신 스프링 or 저 파일만 구하면 됩니다. 또 Jackson 관련 라이브러리를 추가합니다.

3. 컨트롤러를 만들고, ModelAndView를 반환합니다. ModelMap에는 컬렉션을 주던, 필요한 모델 객체를 주던 주면 되고, viewname에는 MappingJacksonJsonView의 빈 이름을 설정해 줍니다.

4. xxx-servlet.xml에서 MJJV 빈을 등록하고, 빈 이름은 jsonView라고 했습니다. 3번 컨트롤러에서도 jsonView란 이름을 사용했죠. 그리고 뷰리졸버를 하나 추가했습니다. BeanName머시기 뷰리졸버가 있죠. 그걸 추가했습니다.

5. jQuery.getJson(url, param, function)를 이용해서 저 컨트롤러에 요청을 보내고, 컨트롤러가 돌려주는 값을 확인해 봤습니다.

usernames:List<String> => {“userNames”:["aaa", "vvv", "dddd"]}
users:List<User> => {“users”:[["1", "기선", "whiteship@email.com"],["2", "toby", "toby@email.com"]]}
user:User => {“user”:[name:"기선", "id":1, "email":email]}
user1:User, user2:User=> {“user1″:[name:"기선", "id":1, "email":email],”user2″:[name:"기선", "id":1, "email":email]}

이런 형태의 결과가 나왔습니다.(기억으로 적은거라 틀릴 수도 있습니다.) 주로 사용하게 될 첫 번째 형태를 보니, 맵(문자열-배열) 형태의 JSON으로 보입니다. 필드명이 없어서 인덱스로 접근해야 한다는게 불편해 보입니다. 세번째 결과를 보면 객체 하나를 줬을 때는 필드명까지 읽어서 넣어주는데 말이죠. 컬렉션도 그렇게 해주면 좋을텐데.. 좀 아쉽습니다.

6. 다음은 jQuery Ajax로 JSON 데이터를 받아왔으니, 이제 자동완성 기능만 구현하면 됩니다.

6-1. 텍스트 박스에 어떤 값을 입력하면, 서버로 요청을 보내고 그 결과를 리스트로 뿌리고, 그 중에 하나를 선택하면 텍스트 박스에 채워주고, 텍스트 박스 옆에 기타 자세한 정보를 뿌려줍니다.

6-2. 텍스트 박스에 값을 입력해서 검색 리스트가 나왔을 때, 키보드의 화살표로 상하 이동이 가능해야 합니다.

6-3. 캐시 설정을 통해 같은 검색어의 경우 매번 요청을 보내는 것이 아니라, 특정 시간동안은 캐시에서 데이터를 읽어서 보여줍니다. 캐시 유효 시간을 설정할 수 있어야겠네요.

대략 이정도 기능인데, 여기서 좀 시간이 많이 걸리고 있네요. 내일은 꼭 마무리 해야겠습니다.
비도 오는데 허리가 쑤실만큼 앉아있었지만 결과는 좀 허무하네요.
에잇… 내일은 파이팅.