스프링 2.5 @MVC 컨트롤러 테스트관련 궁금한거..

@Controller
@RequestMapping(“board/*.do”)
@SessionAttributes(value=”board”)
public class BoardController {

    @Autowired
    private BoardService boardService;

    @Autowired
    private BoardValidator validator;

    @RequestMapping
    public void list(ModelMap model){
        model.addAttribute(boardService.getAll());
    }

    @RequestMapping(method=RequestMethod.GET)
    public void add(ModelMap model){
        model.addAttribute(new Board());
    }

    @RequestMapping(method=RequestMethod.POST)
    public String add(@ModelAttribute(“board”) Board board, BindingResult result, SessionStatus status){
        validator.validate(board, result);
        if(result.hasErrors())
            return “board/add”;
        else{
            this.boardService.saveOrUpdate(board);
            status.setComplete();
            return “redirect:board/list.do”;
        }
    }

    @RequestMapping
    public void delete(int id){
        boardService.deleteById(id);
    }

}

위 코드는 간단한 스프링 2.5 @MVC 컨트롤러입니다. CRUD 중에서 U 관련 코드는 생략했습니다.

이 컨트롤러를 작성하면서 확인하고 싶었던 걸 정리하면 다음과 같습니다.
1. board/list.do 라는 요청 결과를 보여줄 뷰는 WEB-INF/board/list.jsp 파일이 맞는지..(ViewResolver 확인)
2. board/list.do 라는 요청을 했을 때 결과 뷰에 넘어가는 모델 객체 중에 List<Board>가 있는지. 그리고 그 모델 객체의 이름이 boardList가 맞는지.(ModelAndView 확인)
3. board/add.do GET 요청이 오면 저 컨트롤러의 add(ModelMap) 메소드가 호출되는지.(RequestMapping 확인)

이 때 2번은 쉽게 테스트가 가능합니다. 인자로 넘겨주는 ModelMap 객체를 하나 만들어서 넘겨주고 해당 메소드를 호출 해보고 ModelMap 객체를 뒤져보면 테스트 하고자 하는 데이터를 찾을 수 있습니다.

그러나.. 1번은 ModelAndVIew 객체를 반환하도록 컨트롤러 코드를 고치지 않는 이상 어떻게 테스트 할 수 있을지 감이 안잡힙니다. 스프링 MVC CoC를 적극 활용하려고 작성한 코드인데 명시적으로 뷰이름을 설정해버리면 그런 의도가 무색해지니까요..

3번도 마찬가지 입니다. 이건 더 감이 안 잡힙니다. 테스트를 할 때 만드는 ApplicationContext가 WebApplicationContext도 아니라서 스프링이 기본으로 등록해주는 빈(request handler, adapter 등)들이 없을 것이기 때문에 스프링 테스트만 가지고는 테스트가 어려울 것 같다는 생각이 듭니다.

어쩌면 컨트롤러 테스트가 아닐 수도 있겠습니다. 스프링 프레임워크가 제공하는 기능에 대한 테스트일 수도 있으니 스프링 코드에서 뷰 리졸버와 요청 맵핑 클래스에 대한 테스트로 만족하고 넘어갈 수도 있겠지만.. 글쎄요. 그래도 왠지 테스트가 하고 싶네요. 혹시 HttpUnit으로 이런걸 할 수 있는건가요. 써보지 않았는데.. 함 살펴봐야겠습니다.

오랜만에 스프링 MVC 다시 정리

오늘 오후 네 시에 스터디가 있어서 오랜만에 13장을 다시 정리해봤습니다. 그 중 몇 개만 정리해둡니다.

MultiActionController 사용 방법은 두 가지
– 상속
– 위임

WebApplicationContext가 관리하는 빈
– 컨트롤러(controller)
– 핸들러 맵핑(handler mappings)
– 뷰 리졸버(view resolver)
– 로케일 리졸버(locale resolver)
– 테마 리졸버(theme resolver)
– 멀티파트 파일 리졸버(multipart file resolver)
– 예외 처리 리졸버(Handler exception resolver)

애노테이션 기반 컨트롤러 설정시 필요한 빈(자동 등록해줌)
– DefaultAnnotaionHandlerMapping
– AnnotationMethodHandlerAdapter

@RequestMapping 사용 방법
– 클래스 레벨
– 메소드 레벨(MAC와 비슷한 효과)
– 클래스 + 메소드 레벨 혼합(클레스 레벨에 Ant 패턴 사용해서 거르고, 메소드 레벨로 세부적으로.)

요청 처리 메소드 인자
– Servlet API(Session 사용시 Thread-safety 문제가 생기면, AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정.)
– WebRequest, NativeWebRequest
– Locale
– InputStream/Reader, OutputStream/Writer
– @RequestParam
– Map, Model, ModelMap
– Command/form objects
– Errors/BindingResult
– SessionStatus

요청 처리 메소드 반환 타입
– ModelAndView
– Model (뷰 이름은 CoC 사용)
– Map (위와 동일)
– View (모델은 커맨드 객체와 @ModelAttribute를 사용한 메소드가 반환하는 객체)
– String (위와 동일)
– void (응답을 response 객체를 사용해서 직접 처리하거나, CoC 사용)
– Other return type (해당 객체를 model attribute로 뷰에서 사용가능)

@RequestParam
– 요청 매개변수 바인딩

@ModelAttribute
– 메서드 매개변수 레벨: 모델 속성을 특정 메서드 매개변수로 맵핑할 때 사용.
– 메서드 레벨: 화면에서 사용할 implicite object를 제공할 때 사용.

@SessionAttributes
– @ModelAttribute의 이름 목록을 지니고 있다. 해당 모델 객체들을 세션에 저장하여 여러 요청에서 공통으로 사용.

@InitBinder
– 커스텀 프로퍼티 에디터 등록.

Spring Faces 소개하기, 파트 1

참조, 요약, 번역: http://www.jsfcentral.com/articles/intro_spring_faces_1.html

JavaServer Faces와 스프링의 연동을 돕는 Spring Faces라는 모듈이 있는데, 이 모듈을 Spring Web Flow 2에 도입했다. 본 기사는 Spring Faces에 대한 첫 번째 기사로, JSF 기준과 Spring 기준으로 각각 두 프레임워크를 통합하는 방법을 살펴볼것이다.

스프링은 예전부터 기본적인 JSF 연동을 자체 웹 모듈에서 지원해왔다. 기존 JSF 웹 계층과 스프링이 관리하는 비즈니스 계층 사이를 연결해주는 형태였다. 이 방법을 ‘JSF-기준 연동’이라고 한다. 이 방법으로도 잘 사용해왔지만, 스프링 웹 팀은 그 둘 사이에 뭔가 잘 안 맞는 것.. 너무 많은 구성물과 너무 많은 개념적인 오버헤드가 생겨서 관리하기 쉽지 않다는 것을 느꼈다.

스프링 Faces는 시야를 바꿔서 통합에 대한 접근 방법을 전환했다. 즉 스프링 기반 웹 애플리케이션에 JSF의 장점을 끌어오는 것을 목표로 했다. 이것은 ‘스프링-기준 연동’이라는 접근 방법으로 JSF UI 컴포넌트 모델을 사용하여 웹 애플리케이션 개발을 하는 것이다. 본 기사에서는 이 접근 방법의 장점과 왜 이것을 JSF과 스프링 개발자에게 권하는지 살펴보자.

JSF와 스프링 – 완벽한 조화인가?

JSF는 그 자체로 rich 웹 애플리케이션을 개발할 때 매우 많은 장점을 제공한다. 특히 복잡한 UI를 가지고 상태를 유지하는(stateful) 프로그래밍 모델을 필요한 곳에서 말이다. 이 UI 컴포넌트 모델들은 견고하며 강력하다. 제대로 적용하면, 웹 브라우져서에서 복잡한 컨트롤을 만들어야 하는 복잡도를 낮출 수 있다. 풍족한 UI를 선언적으로 만들 수 있고, 통합 EL을 사용하여 개별 컨트롤을 직접 도메인 모델에 유연하게 바인딩할 수 있기 때문에, 보다 빠르게 복잡한 웹 UI를 개발할 수 있다. 이 모든 것을 간단한 POJO 프로그래밍 모델로 달성할 수 있다.

스프링을 JSF를 앞단에 둔 애플리케이션의 도메인 계층에 적용하는 것이 자연스럽고 이상적이다. POJO 프로그래밍 모델을 이용할 수 있기 때문이다. JSF의 풍부하고 잘 정의된 확장 포인트를 사용하면, 스프링이 관리하는 비즈니스 계층을 JSF 앞단에 간단하게 연결할 수 있다.

EL을 사용한 JSF-기준 연동

JSF-기준 연동은, JSF 컨트롤러 모델이 애플리케이션을 주고하고 JSF가 관리하는 빈을 스프링 비즈니스 계층과 연결한다. 스프링은 몇몇 연동 클래스를 제공하여 faces-config.xml에서 스프링 빈을 참조할 수 있게 하는 것이다. 가장 흔한 방법으로는 DelegatingVariableResolver 를 사용하면, EL을 사용하여 스프링 빈을 참조할 수 있다. EL을 사용해서 스프링 빈을 JSF가 관리하는 빈에 주입하는 것이다. 예를 들어, WebApplicationContext에 다음과 같은 빈이 있다고 치자.

<bean id=”mySpringService” class=”com.foo.MySpringServiceImpl”>

EL을 사용해서 faces-config.xml에 정의한 <managed-bean>에서 이 빈을 JSF가 관리하는 빈에 주입할 수 있다.

<managed-bean>
  <managed-bean-name>myJsfManagedBean</managed-bean-name>
  <managed-bean-class>com.foo.MyJsfManagedBean</managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
  <managed-property>
    <property-name>mySpringService</property-name>
    <value>#{mySpringService}</value>
  </managed-property>
</managed-bean>

즉 이렇게 하면, 스프링이 관리하는 싱글톤 서비스를 JSF가 매번 생성하는 객체에 주입할 수 있다. 스프링 2.0부터, request와 session 스콥을 사용할 수 있기 때문에, JSF 관리하는 빈을 모두 없애고, EL로 스프링 컨트롤러를 사용할 수도 있다.

JSF-기준 접근방법으로 충분한가?

스프링의 간단한 JSF 연동 클래스로, 부드럽게 스프링이 관리하는 서비스를 JSF 기반 앞 단과 통합할 수 있었다. 하지만, 이것으로 충분한가? 나는 그렇지 않다고 확신한다. 이 방법은 JSF 컨트롤러 모델을 사용하고 있는데, 여기에 몇 가지 단점들이 존재한다.

  • 순수-MVC 접근방법에 따라, 모든 것이 뷰를 렌더링하는것으로 주도된다. 랜더링 전에 모델을 초기화 하는 편리한 포인트가 없다.
  • 성가신 네비게이션 모델은 여러분이 실수를 해도 거의 피드백이 없다.(오타가 있어도 말이다.) 그리고 규칙이 추가되거나 변경될 때마다 애플리케이션을 다시 시작해야 한다.
  • 검증 모델이 충분하지 않다. 서버쪽에서의 필드 수준 검증을 중심으로 하고 있다. 컴포넌트 트리를 순회하지 않고도 클라이언트쪽 필드 검증과 서버쪽 모델 검증을 수행할 수 있는 편리한 방법이 필요하다.
  • URL 맵핑이 좀 유연하지 않고, 리다이렉트 지원이 충분치 않다.
  • request와 session 사이의 보다 세밀한 스콥프가 필요하다. 특히 페이지 내에서 여러번 이벤트를 발생시키는 Ajax 기반 뷰의 경우에 그렇다.
  • 예외 처리 기능이 매우 제한적이다. 특히 뷰 랜더링할 때 그렇다.
  • 컨트롤러 로직이 JSF 빈으로 분산되어 있다. 단위 테스트하기가 힘들고 변경할 때마다 애플리케이션을 다시 시작해야 한다.

JSF의 UI 컴포넌트 모델은 견고하다. 그 행위 들은 잘 정의되어 있는 JSF 컴포넌트 라이프사이클 경계 내에서 실행되고 캡슐화되어 있다. 하지만 컨트롤러 모델은 다소 제한적이다. 특히 스프링, 스프링 MVC, 스프링 Web Flow의 유연함과 막강함을 사용하고자 하는 사람들에게 더 제한적으로 느껴진다. 또한 이 접근 방법은 개념적인 오버헤드를 발생시킨다. faces-config.xml, JSF가 관리하는 빈, JSF 뷰 템플릿 그리고 스프링 기반 비즈니스 계층의 모든 컴포넌트를 계속해서 관리해야 한다.

처음에는, 스프링 Web Flow 1.x도 JSF-기준 방법으로 연동을 시도했었다. 하지만, 이 방법이 너무 제한적었다. 여전히 많은 JSF 구성요소를 사용했고 제대로 동작하려면 faces-config.xml에 뭔가를 추가해야 했다. agile이라고 할 수 없었다.

만약에 JSF의 UI 컴포넌트 모델을 스프링 환경에 연동할 수 있으면 어떨까 고민을 했다. 스프링 프로그래밍 모델을 사용하고 보다 스프링을 기준으로 한 접근 방법으로 말이다. 만약 스프링이 전체 요청을 관리하면, 여러분은 보다 편리하게 전체 애프리케이션 설정을 관리할 수 있고 편하게 프로그래밍을 할 수 있을 것이다. 또한 스프링 MVC의 유연한 라우팅 기반 시설과 스프링 Web Flow 2.x의 agile statefule 컨트롤러 모델을 사용할 수 있겠다. 이는 stateful JSF 뷰와 잘 어울린다. 바로 이러한 동기로 인해 스프링 Faces가 생겼고 스프링 포트폴리오에 추가됐다.

스프링 Faces의 스프링-기준 JSF

스프링 Faces는 JSF와 스프링 연동에 있어서 훨씬 규범적인(prescriptive)-미리 무언가를 예상하고 마련해 두는- 접근방법을 채택했다. JSF의 막강함은 그가 제공하는 여러 확장 포인트인데, 스프링 Faces는 이 모든 장점을 수용하여 JSF를 보다 자연스럽게 스프링에 녹아들게 했다. 환경에 대해 몇가지 가정을 세웠다. 일단 스프링 MVC DispatcherServlet을 사용하여 요청 처리를 라우팅하고, 스프링 Web Flow 2.x의 기초 컨트롤러 모델을 사용한다. 또, 스프링 JavaScript 모듈을 사용하여 경량 JSF Ajax 컴포넌트를 제공한다고 가정했다. (스프링 Faces는 어떠한 JSF 컴포넌트 라이브러리도 사용할 수 있도록 설계했기 대문에, 이들 컴포넌트는 전부 옵션이기 필수가 아니다.)

사용자 삽입 이미지그림 1. 스프링 Faces 아키텍처에 대한 하이 레벨 뷰

본 기사 남은 부분에서, 스프링 Web Flow 2 배포에 포함되어 있는 Spring Travel에 스프링 Faces를 적용해볼 것이다.

전체 스프링 Faces 예제 소스는 /project/spring-webflow-samples/booking-faces/에 있다.

이미 동작하고 있는 예제가 필요하다면, 여기를 클릭하라.

스프링 Faces 예제의 구조

먼저 살펴볼 것은  /src/main/webapp/WEB-INF/faces-config.xml 다. 이 안에 설정한 것은 오직 FaceletViewHandler 뿐이고, 스프링 Faces가 보다 미리 마련해두는 방식을 채택했기 때문에, 모든 JSF 연동에 필요한 구성물들은 자동으로 클래스패스에 있는 spring-faces.jar에 포함되어 있다.

또한 다음 파일들을 살펴보고 라우팅에 대한 기본 설정에 익숙해져야 할 것이다.

  • /src/main/webapp/WEB-INF/web.xml
  • /src/main/webapp/WEB-INF/config/webmvc-config.xml
  • /src/main/webapp/WEB-INF/config/webflow-config.xml

이 설정에 대해선느 스프링 Web Flow 2 레퍼런스 가이드에 나와있으니 자세히 설명하진 않겠다. 다만 이 설정의 결과만 설명하겠다.

/spring 이하의 모든 요청은 스프링 DispatcherServlet이 처리하도록 되어 있다. DispatcherServlet은 나머지 URL을 보고 stateless JSF 뷰를 보여줘야 하는지, SWF를 사용해서 stateful JSF 뷰를 보여줘야 하는지 판단하다.

예를 들어,

/spring/intro -> renders the stateless Facelets template at /WEB-INF/intro.xhtml

/spring/main -> hands control over to the flow defined at /WEB-INF/flows/main/main.xml

/spring/booking?hotelId=5 -> hands control over to the flow defined at /WEB-INF/flows/booking/booking.xml, passing the hotelId parameter as input to the flow

그림 2에 있는 /flows 디렉토리 구조를 보면, 스프링 Faces 컨틀롤러와 뷰 구성물을 논리적으로 재사용 가능한 모듈로 조직화 하도록 한 것을 볼 수 있다.

사용자 삽입 이미지그림 2. 모듈들의 논리적인 조직화

스프링 Web Flow와 Agile JSF 컨트롤러

스프링 Faces의 핵심은 스프링 Web Flow 연동에서 온다. 그리고 고수준의 flow DSL을 사용할 수 있으며, 이것으로 정확하고 엘레강트한 구조로 이벤트 처리, 모델 조화, 뷰 네비게이션을 제공할 수 있다. 플로우 정의는 동적이며 애플리케이션 재실행 없이 핫-릴로딩이 가능하다. 웹 컨테이너에 배포할 필요가 없기 때문에 완전한 단위 테스트도 가능하다. 보다 기민한 JSF 개발이 가능하다. 세밀한 스콥(flash, view, flow 스콥)을 사용하여 여러 요청을 넘나들며 도메인 모델을 직접 사용할 수 있다. 따라서 여러분은 도메인 모델에 보다 많은 가치를 부여하는데 힘을 쓸 수 있고, 모델을 매 요청에 저장해두고 가져오는 등의 기반 시설에 대해 걱정하는 시간을 줄일 수 있다. 이런 stateful 특징은 풍부한 JSF 뷰와 잘 어울리며 여러 이벤트로 뷰가 자주 변경되는 상황에 적당하다.

스프링 Faces를 사용하면, JSF postback 라이프사이클은 SWF 제어 하에 실행된다. PhaseListener를 통해 JSF 라이프사이클에 플로우를 실행하는 SWF 1.x의 제한적인 연동에 비하면, 스프링 Faces JSF 확장 포인트는 SWF 구조를 기반으로 만들었다. 이로인한 아키텍처 적인 장점은 다음과 같다.

  • JSF 뷰는 SWF의 자동 POST + Redirect + GET 행위와 자연스럽게 동작하며, step-behind-URL을 없앴고, 일반적인 JSF 애플리케이션에 있던 브라우저 리프래시 경고 문제를 제거했다.
  • FacesMessage는 스프링의 MessageContext로 바인딩되어 스프링의 강력한 i18n 기능을 사용할 수 있다. 메시지들은 flash 스콥으로 저장되며 따라서 리다이렉트를 해도 보존된다.
  • 컴포넌트 트리 상태는 자동적으로 플로우 실행 상태와 동기화한다. pluggable storage 매키니즘을 적용하여 다양한 클러스터링 시나리오를 다룰 수 있다.

끝. 다음 기사에는 예제를 좀 더 자세히 살펴보겠다.

저자에 대하여

Jeremy Grelle는 SpringSource의 수석 소프트웨어 엔지니어이며 스프링 Faces 프로젝트 기술 리더다.

Spring 2.5 @Controller 사용시 BindingResult 주의 할 것.

스프링 레퍼런스에 BindingResult에 대한 언급은 단 한 줄.

org.springframework.validation.Errors / org.springframework.validation.BindingResult  validation results for a preceding command/form object (the immediate preceding argument).

이게 끝입니다. 한 줄이라고 무시하면 안 됩니다. 진짜 중요한 한 줄입니다.

public String update(@ModelAttribute(“model”) Foo model, BindingResult result, Bar bar)

public String update(@ModelAttribute(“model”) Foo model, Bar bar, BindingResult result)

이 두 줄의 코드는 어쩌면 아무런 차이가 없을 수도 있지만 Validator를 만들어 보시면 그 차이를 알 수 있습니다. 무슨 차이인지는 비밀입니다. ㅋㅋ 이미 스프링 레퍼런스에 다 설명이 나와있어서 비밀이랄 것도 없지만 말이죠.ㅋ

힌트 1.
validator.validate(model, result);

힌트 2.
public void validate(Object obj, Errors errors) {
    …
}

ControllerClassNameHandlerMapping 잘 되네~

@Controller
public class MemberController {

    @Autowired
    private MemberService memberService;

    @RequestMapping
    public ModelAndView list(){
        return new ModelAndView(“member/list”)
            .addObject(“members”, memberService.getAll());
    }
}

요렇게만 해두면, /member/list.xxx 라는 요청이 오면 알아서(Conversion) 저 메소드가 처리하도록 합니다. ㄴXxx-servlet.xml에는 XML에는

<bean class=”org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping” />

애만 등록해주면 됩니다. 저 한 줄의 XML로 URL 맵핑을 손수 적는 수고를 덜 수 있습니다.

계속해서 진화하는 Spring MVC 어디까지 갈텐가~~ 멋있어 멋있어 끝까지 가는거야~~

2008/04/08 – [모하니?/Coding] – ControllerClassNameHandlerMapping가 찾아 준댔는데…
ps : 새벽에 괜히 삽질만 하고… 잠이나 계속 잘껄..