Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 16장 웹 MVC 프레임워크 #1

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



Part V. 웹
레퍼런스 문서의 이 부분에서는 스프링 프레임워크의 표현계층(특히 웹기반의 표현계층)에 대한 지원을 다룬다.

스프링 프레임워크의 웹프레임워크인 Spring Web MVC는 앞의 두 개의 장에서 다룬다. 나머지 장에서는 스프링 프레임워크와 다른 웹기술(Struts와 JSF 같은)의 통합에 대해서 다룬다.

이번 섹션에서는 마지막으로 스프링의 MVC 포틀릿 프레임워크를 다룬다.

  • Chapter 16, 웹 MVC 프레임워크
  • Chapter 17, 뷰 기술
  • Chapter 18, 다름 웹 프레임워크와의 통합
  • Chapter 19, Portlet MVC Framework

16. 웹 MVC 프레임워크

16.1 스프링 웹 MVC 프레임워크 소개
스프링 웹 모델-뷰-컨트롤러 (MVC) 프레임워크는 핸들러 매핑, 뷰 처리, 파일 업로드같은 로케일과 테마처리로 요청을 핸들러로 디스패치하는 DispatcherServlet을 중심으로 설계되었다. 기본 핸들러는 넓은 범위로 유연한 처리(handling) 메서드를 제공하는 @Controller와 @RequestMapping 어노테이션에 기반한다. 스프링 3.0부터는 @Controller 메카니즘도 @PathVariable 어노테이션과 다른 기능들을 통해서 RESTful 웹사이트와 어플리케이션을 생성할 수 있게 한다.

"확장에는 열려있다..."
스프링 웹 MVC와 스프링의 일반적인 핵심 설계원리는 “확장에는 열려있고 수정에는 닫혀있다(Open for extension, closed for modification)” 원리이다.

스프링 웹 MVC 핵심 클래스의 일부 메서드들은 final이다. 개발자들은 자신만의 동작을 위해서 이러한 메서드들을 오버라이드할 수 없다. 이는 임의로 할 수 없지만 특히 원리를 명심해 두어야 한다.

이 원리에 대해서 알고 싶다면 Seth Ladd등이 쓴 Expert Spring Web MVC and Web Flow를 참고해라. 특히 첫번째 에디션의 117 페이지에 있는 "A Look At Design" 부분을 살펴봐라. 아니면 다음을 참고해라.

  1. Bob Martin, The Open-Closed Principle (PDF)
스프링 MVC를 사용할 때 final 메서드에는 어드바이스를 추가할 수 없다. 예를 들어 AbstractController.setSynchronizeOnSession() 메서드에는 어드바이스를 추가할 수 없다. AOP 프록시와 왜 final 메서드에는 어드바이스를 추가할 수 없는지는 Section 8.6.1, “AOP 프록시 이해하기”를 참고해라.

스프링 웹 MVC에서 커맨드 객체나 폼을 바인딩하기 위한 객체(form-backing object)에 어떤 객체라도 사용할 수 있다. 프레임워크에 특화된 인터페이스나 기반 클래스를 구현할 필요가 없다. 스프링의 데이터 바인딩은 아주 유연하다. 예를 들어 스프링의 데이터 바인딩은 타입 불일치를 시스템 오류가 아니라 어플리케이션이 평가할 수 있는 유효성검사 오류(validation errors)로 다룬다. 그러므로 폼객체가 유효하지 않은 제출을 처리하고 문자열을 적절하게 변환하려고 타임이 없는 문자열로 비즈니스 객체의 프로피티들을 복사할 필요가 없다. 대신에 때로는 비즈니스 객체로 직접 바인딩하는 것을 더 선호한다.

스프링의 뷰 처리(view resolution)는 엄청나게 유연하다. Controller가 보통 데이터를 가진 모델 Map을 준비하고 뷰 이름을 선택하는 담당을 하지만 Controller 응답 스트림에 직접 작성하고 요청을 완료할 수도 있다. 뷰 이름 처리는 빈 이름, 프로퍼티 파일이나 커스텀 ViewResolver 구현체로 파일의 확장자나 Accept 헤더의 내용협상(content type negotiation)을 통해서 설정할 수 있다. 모델(MVC에서 M)은 뷰 기술을 완전히 추상화할 수 있도록 Map 인터페이스이다. JSP, Velocity, Freemarker같은 템플릿 기반의 렌더링 기술과 직접 통합하거나 XML, JSON, Atom과 다른 타입의 컨텐츠들을 직접 생성할 수 있다. 모델 Map은 JSP 요청 속성, Velocity 템플릿 모델처럼 적절한 형식으로 변환된다.


16.1.1 스프링 웹 MVC의 기능
스프링 웹 플로우(Spring Web Flow)
스프링 웹 플로우 (SWF, Spring Web Flow)는 웹 어플리케이션 페이지 흐름을 관리하는 최상의 솔루션이다.

SWF는 서블릿 환경과 포틀릿 환경에서 모두에서 스프링 MVC, 스트럿츠, JSF같은 프레임워크와 통합한다. 순사하게 요청 모델과는 반대로 대화식 모델의 이점을 갖는 비즈니스 처리과정을 가진다면 SWF가 해결책이 될 것이다.

SWF는 여러 상황에서 재사용할 수 있는 내장된 모쥴처럼 논리적인 페이지 흐름을 갖을 수 있도록 하고 비즈니스 처리과정을 유도하는 제어된 네비게이션드로 사용자를 도와주는 웹 어플리케이션 모듈을 구성하는데 최적이다.

SWF에 대한 자세한 내용은 Spring Web Flow 웹사이트를 참고해라.

스프링의 웹 모듈은 웹을 지원하는 많은 독특한 기능들을 포함한다.

  • 역할의 깔끔한 분리. 컨트롤러, 밸리데이터(validator), 커맨드 객체, 폼 객체, 모델 객체, DispatcherServlet, 핸들러 매핑, 뷰 리졸버등과 같은 각 역할들인 전용 객체들로 실행될 수 있다.
  • JavaBean처럼 프레임워크 클래스와 어플리케이션 클래스의 강력하고 직관적인 설정. 이 설정 기능은 웹 컨트롤러에서 비즈닌스객체와 밸리데이터에 대한 참조같은 컨텍스트를 건너서 쉽게 참조할 수 있는 기능을 포함한다.
  • 적응성(Adaptability), 비침투성, 유연성(flexibility) 해당 시나리오에 필요한 모든 컨트롤러 메서드 시그니처(파라미너 어노테이션(@RequestParam, @RequestHeader, @PathVariable 등등) 중 하나를 사용할 수도 있는)를 정의해라.
  • 중복없이 재사용가능한 비즈니스 코드. 특정 프레임워크 기반 클래스를 확장하려고 해당 클래스를 복제하는 대신에 존재하는 비지니스 객체를 커맨드 객체나 폼 객체로 사용해라.
  • 커스터마이징할 수 있는 바인딩과 유효성검사(validation). 수동으로 파싱해서 비즈니스 객체로 변환하는 문자열만 사용가능한 폼 객체 대신에 문제가 있는 값을 유지하는 어플리케이션 수준의 유효성 검사 오류인 타입 불일치, 로컬라이징된 날짜나 숫자를 바인딩 등등.
  • 커스터마이징할 수 있는 핸들러 매핑과 뷰 처리(view resolution). 핸들러 매핑과 뷰 처리전략은 간단한 URL 기반의 설정부터 정교하고 목적에 맞는 처리 전략까지의 범위를 포함한다. 스프링은 특정 기술에 위임하는 웹 MVC 프레임워크보다 더 유연하다.
  • 유연한 모델 전환자(transfer). 이름/값 Map으로 된 모델 전환자는 어떤 뷰 기술과도 쉽게 통합할 수 있도록 지원한다.
  • 커스터마이징할 수 있는 로케일과 테마 처리. 스프링 태그라이브러리를 쓰던 안쓰던 JSP를 지원하고 JSTL을 지원하고 별도의 중계 등이 필요없이 Velocity를 지원한다.
  • 소위 스프링 태그 라이브러리로 알려진 간단하고 강력한 JSP 태그 라이브러리는 데이터 바인딩과 테마같은 기능을 지원한다. 커스텀 태그는 마크업 코드에서 최대한의 유연성을 제공한다. 태그 라이브러리 디스크립터에 대한 정보는 Appendix F, spring.tld 부록을 참고해라.
  • 스프링 2.0에 추가된 JSP 폼태그 라이브러리는 JSP 페이지에서 폼을 훨씬 쉽게 작성할 수 있게 한다. 태그 라이브러리 디스크립터에 대한 정보는 Appendix G, spring-form.tld 부록을 참고해라.
  • 빈의 생명주기는 현재의 HTTP 요청이나 HTTP Session의 범위가 된다. 이는 스프링 MVC의 기능이 아닌 스프링 MVC가 사용하는 WebApplicationContext 컨테이너의 독특한 기능이다. 이러한 빈의 범위는 Section 4.5.4, “리퀘스트, 세션, 글로벌 세션 범위”에 나와있다.

16.1.2 다른 MVC 구현체의 플러그인 기능(Pluggability)
어떤 프로젝트에서는 스프링이 아닌 MVC 구현체를 더 선호한다. 많은 팀은 기술과 도구에 대해서 기존에 연구했던 것을 사용할 수 있기를 원한다. Struts 프레임워크에 대한 많은 양의 지식과 경험이 이미 존재한다. Struts의 아키텍처적인 결점을 견딜수 있다면 Struts를 웹 계층으로 선택할 수 있고 동일하게 WebWork나 다른 웹 MVC 프레임워크에도 적용된다.

스프링의 웹 MVC를 사용하지는 않지만 스프링이 제공하는 다른 해결책들은 사용하고자 한다면 선택한 웹 MVC 프레임워크와 스프링을 쉽게 통합할 수 있다. 스프링의 ContextLoaderListener로 스프링 루트 어플리케이션 컨텍스트를 구동하고 Struts나 WebWorc 액션내에서 ServletContext 속성(또는 스프링 각각의 헬퍼 메서드)을 통해 루트 어플리케이션 컨텍스트 에 접근해라. "플러그인"이 없으므로 전용 통합도 필요없다. 웹계층의 뷰에서 진입점인 루트 어플리케이션 컨텍스트 인스턴스로 스프링을 그냥 라이브러리처럼 사용할 수 있다.

등록한 빈과 스프링의 서비스는 스프링 웹 MVC가 없어도 사용할 수 있다. 이러한 시나리오에서 스프링은 Struts나 WebWork와 경쟁하지 않는다. 스프링은 빈 설정부터 데이터 접근과 트랜잭션 처리까지 순수한 웹 MVC 프레임워크가 처리하지 않은 많은 영역을 처리할 뿐이다. 그러므로 단지 JDBC나 하이버네이트로 트랜잭션 추상화를 사용하고자 할때도 스프링 중간계층이나 데이터접근 계층으로 어플리케이션을 풍부하게 할 수 있다.


16.2 DispatcherServlet
많은 웹 MVC 프레임워크처럼 스프링의 웹 MVC 프레임워크는 요청 중심(request-driven)이고 요청을 컨트롤러로 디스패치하는 중앙 서블릿을 중심으로 설계되었고 웹 어플리케이션을 개발에 대한 기능들을 제공한다. 하지만 스프링의 DispatcherServlet은 앞에서 얘기한 것 이상은 하지 않는다. DispatcherServlet는 스프링 IoC 컨테이너와 완전히 통합되어 있어서 스프링이 가진 모든 다른 기능을 사용할 수 있게 한다.

스프링 웹 MVC DispatcherServlet의 요청 처리 흐름은 다음 다이어그램에 설명되어 있다. 패턴에 대해서 작 알고 있는 사람은 DispatcherServlet가 “Front Controller” 디자인 패턴(이는 스프링 웹 MVC와 인기있는 다수의 웹 프레임워크가 사용하는 패턴이다)의 표현이라는 것을 알 수 있을 것이다.

스프링 웹 MVC에서 요청의 처리 흐름(고수준)

스프링 웹 MVC에서 요청의 처리 흐름(고수준)


DispatcherServlet는 실제로 Servlet이고(HttpServlet 기반 클래스를 상속받는다) 웹 어플리케이션의 web.xml에 선언되어 있다. DispatcherServlet이 처리하게 하고 싶은 요청을 web.xml 파일에서 URL 매핑을 사용해서 매핑해야 한다. 이는 표준 Java EE 서블릿 설정이다. 다음 예제는 이러한 DispatcherServlet 선언과 매핑을 보여준다.

<web-app>
  <servlet>
    <servlet-name>example</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>example</servlet-name>
    <url-pattern>/example/*</url-pattern>
  </servlet-mapping>
</web-app>

앞의 예제에서 /example로 시작하는 모든 요청을 example라는 이름의 DispatcherServlet 인스턴스가 처리할 것이다. 이는 스프링 웹 MVC를 설정하는 첫번째 과정일 뿐이다. 이제 스프링 웹 MVC 프레임워크(위의 DispatcherServlet가)가 사용하는 다양한 빈을 설정해야 한다.

Section 4.14, “ApplicationContext의 추가적인 기능들”에서 설명했듯이 스프링의 ApplicationContext 인스턴스는 범위를 가질 수 있다. 웹 MVC 프레임워크에서 각 DispatcherServlet는 루트 WebApplicationContext에서 이미 정의된 모든 빈을 상속받은 자신만의 WebApplicationContext를 가진다. 이 상속받은 빈들은 서블릿에 특화된 범위를 덮어쓸 수 있고 주어진 서블릿 인스턴의 빈에 새로운 범위의 빈을 정의할 수 있다.

스프링 웹 MVC의 컨텍스트 계층

스프링 웹 MVC의 컨텍스트 계층


DispatcherServlet을 초기화하면서 스프링 MVC는 웹 어플리케이션의 WEB-INF 디렉토리에서 [servlet-name]-servlet.xml라는 이름의 파일을 검색하고 거기에 정의된 빈을 생성하고 전역 범위에 같은 이름으로 정의된 모든 빈 정의를 덮어쓴다.

다음의 DispatcherServlet 서블릿 설정(web.xml 파일에 있다)을 보자.

<web-app>
  <servlet>
    <servlet-name>golfing</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>golfing</servlet-name>
    <url-pattern>/golfing/*</url-pattern>
  </servlet-mapping>
</web-app>

위의 서블릿 설정에서 어플리케이션에 /WEB-INF/golfing-servlet.xml라는 파일이 있어야 할 것이다. 이 파일은 스프링 웹 MVC에 특화된 컴포넌트(빈)을 모두 담고 있을 것이다. 서블릿 초기화 파라미터로 이 설정 파일의 정확한 위치를 변경할 수 있다.(자세한 내용은 아래를 보아라.)

WebApplicationContext는 웹 어플리케이션이 필요로 하는 추가 기능들을 가진 보통의 ApplicationContext의 확장이다. WebApplicationContext는 테마를 처리하는 기능(Section 16.9, “테마(theme) 사용” 참고)과 서블릿이 연결되어 있다는 것을 인지하고 있다는 점(ServletContext에 대한 링크를 가지고 있다)에서 일반적인 ApplicationContext와는 다르다. WebApplicationContext는 ServletContext에 바인딩되어 있다. WebApplicationContext에 접근해야 한다면 RequestContextUtils 클래스의 정적 메서드를 사용해서 항상 WebApplicationContext를 검색할 수 있다.


16.2.1 WebApplicationContext의 전용 빈 타입
스프링 DispatcherServlet는 요청을 처리하고 적절한 뷰를 렌더링하도록 전문화된 빈을 사용한다. 이러한 빈은 스프링 MVC의 일부이다. WebApplicationContext에서 이러한 빈을 하나 이상 설정해서 사용할 전용 빈을 선택할 수 있다. 하지만 아무것도 설정하지 않는다면 스프링 MVC가 기본 빈의 목록을 유지하므로 처음에는 설정하지 말아야 한다. 자세한 내용은 다음 섹션에서 설명한다. 우선은 아래의 표에 나온 DispatcherServlet가 의존하는 전용 빈 타입의 목록을 보자.


Table 16.1. WebApplicationContext의 전용 빈 타입
빈(Bean) 타입 설명
HandlerMapping 들어오는 요청을 HandlerMapping 구현체가 다양하게 만든 크리테리아에 기반한 전처리자(pre-processors)와 후처리자(post-processors)(핸들러 인터셉터)의 목록에 매핑한다. 대부분의 인기있는 구현체는 어노테이션이 붙은 컨트롤러를 지원하지만 그렇지 않은 구현체들도 존재한다.
HandlerAdapter 핸들러가 실제로 호출되었는지 여부에 상관없이 DispatcherServlet이 요청에 매핑된 핸들러를 호출하도록 돕는다. 예를 들어 어노테이션이 붙은 컨트롤러를 호출하려면 다양한 어노테이션을 처리해야 한다. 그러므로 HandlerAdapter의 주요 목적은 이러한 세부내용에서 DispatcherServlet을 보호하는 것이다.
HandlerExceptionResolver 예외를 뷰에 매핑하고 더 복잡한 예외 처리 코드도 사용할 수 있다.
ViewResolver 논리적인 스프링 기반의 뷰 이름을 실제 View 타입으로 처리한다.
LocaleResolver 국제화된 뷰화면을 제공할 수 있도록 클라이언트가 사용하는 로케일을 처리한다.
ThemeResolver 개인화된 레이아웃을 제공하는 것처럼 웹 어플리케이션이 사용할 수 있는 테마를 처리한다.
MultipartResolver HTML 폼에서 업로드하는 파일을 처리하는 등의 멀티파트(multi-part) 요청을 파싱한다.
FlashMapManager 한 요청에서 다른 요청으로 속성을 전달하는데(보통은 리다이렉트사이에서) 사용할 수 있도록 "입력" FlashMap과 "출력" FlashMap을 저장하고 획득한다.





























16.2.2 기본 DispatcherServlet 설정
이전 섹션에서 얘기했듣이 각 전문화된 빈에 대해서 DispatcherServlet는 기본적으로 사용할 구현체의 목록을 유지하고 있다. 이 정보는 org.springframework.web.servlet 패키지의 DispatcherServlet.properties 파일에 존재한다.

모든 전용 빈은 자신만의 합리적인 기본값을 가진다. 하지만 조만간 이러한 빈이 제공하는 프로퍼티들을 커스터마이징해야 할 것이다. 예를 들어 InternalResourceViewResolver 설정의 prefix 프로퍼티에 뷰 파일의 부모위치를 설정하는 것은 아주 일반적이다.

세부내용과는 관계없이 WebApplicationContext에 InternalResourceViewResolver같은 전용 빈을 일단 설정하면 사용할 기본 구현체의 목록을 효과적으로 오버라이드하고 설정하지 않았다면 전용 빈 타입을 사용한다는 것이 여기서 이해야할 중요한 개념이다. 예를 들어 InternalResourceViewResolver를 설정하면 ViewResolver 구현체의 기본 목록은 무시된다.

Section 16.14, “스프링 MVC 설정하기”에서는 MVC 자바설정과 MVC XML 네임스페이스를 포함해서 스프링 MVC를 설정하는 다른 선택사항들을 배울 것이다. 이는 간단한 시적점이 되고 스프링 MVC가 어떻게 동작하는지에 대해 어느 정도 지식이 있다고 가정한다. 어플리케이션을 설정하는데 어떤 방법을 선택했는지에 상관없이 이번 섹션에서 설명한 기본적인 개념은 도움이 될 것이다.


16.2.3 DispatcherServlet 처리 순서
DispatcherServlet을 설정한 뒤 요청이 해당 DispatcherServlet에 들어오면 DispatcherServlet는 요청을 다음과 같이 처리하기 시작한다.

  1. WebApplicationContext는 요청을 검색해서 처리과정중에 컨트로러와 다른 요소들이 사용할 수 있는 속성처럼 요청을 바인딩한다. 이는 기본적으로 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 키에 바인딩된다.
  2. 로케일 리졸버는 요청을 처리할 때(뷰를 렌더링하고 데이터를 준비하는 등) 진행중인 요소들이 사용할 로케일을 처리할 수 있도록 요청에 바인딩된다. 로케일 처리가 필요없다면 로케일 리졸버는 필요없다.
  3. 테마 리졸버는 뷰같은 요소들이 어느 테마를 사용할지 결정하도록 요청에 바인딩된다. 테마를 사용하지 않는다면 테마 리졸버를 무시할 수 있다.
  4. 멀티파트 파일 리졸버를 지정했다면 요청이 멀티파트인지 검사한다. 멀티파트라면 진행중인 다른 요소들의 추가적인 처리를 위해 요청을 MultipartHttpServletRequest로 감싼다. 멀티파트 처리에 대한 자세한 내용은 Section 16.10, “스프링의 멀티파트(multipart) (파일 업로드) 지원”를 참고해라.
  5. 적절한 핸들러를 검색한다. 핸들러를 찾으면 모델을 준비하거나 랜더링하기 위해서 핸들러와 연결된 실행체인을 순서대로 실행한다.
  6. 모델이 반환되면 뷰를 렌더링한다. 모델이 반환되지 않으면(전처리자나 후처리자가 보안적인 이슈등으로 요청을 가로챘기 때문일 것이다.) 요청이 이미 실행되었기 때문에 뷰도 렌더링하지 않는다.

WebApplicationContext에 선언한 핸들러 예외 리졸버가 요청을 처리하는 중에 던져진 예외를 잡는다. 이러한 예외 리졸버를 사용하면 예외를 처리하는 커스텀 동작을 정의할 수 있도록 한다.

스프링 DispatcherServlet도 서블릿 API에 정의된대로 last-modification-date의 반환을 지원한다. 특정 요청에 마지막 수정일시를 결정하는 과정은 직관적이다. DispatcherServlet가 적절한 핸들러 매핑을 검색하고 찾아낸 핸들러가 LastModified 인터페이스를 구현했는지를 검사한다. 만약 구현했다면 LastModified 인터페이스의 long getLastModified(request) 메서드의 값을 클라이언트에 반환한다.

서블릿 초기화 파라미터(init-param 요소)를 web.xml 파일의 서블릿 선언에 추가해서 개별 DispatcherServlet 인스턴스를 커스터마이징할 수 있다. 지원하는 파라미터 목록은 다음 표를 참고해라.


Table 16.2. DispatcherServlet 초기화 파라미터
파라미터 설명
contextClass WebApplicationContext를 구현한 클래스로 해당 서블릿이 사용하는 컨텍스트를 인스턴스화한다. 기본적으로 XmlWebApplicationContext를 사용한다.
contextConfigLocation 어디서 컨텍스트를 찾을 수 있는지를 나타내려고 컨텍스트 인스턴스에 (contextClass가 지정한) 전달하는 문자열이다. 문자열은 여러 컨텍스트를 지원하려고 여러 문자열(콤마로 구분한다)로 이루어질 수 있다. 여러 컨텍스트를 사용하는 경우에 빈의 위치는 두번 정의되는데 마지막 위치를 사용한다.
namespace WebApplicationContext의 네임스페이스. 기본값은 [servlet-name]-servlet이다.















16.3 컨트롤러 구현하기
컨트롤러는 일반적으로 서비스 인터페이스를 통해서 정의하는 어플리케이션 동작에 대한 접근을 제공한다. 컨트롤러는 사용자 입력을 해석해서 뷰로 사용자에게 나타내는 모델로 변환한다. 스프링은 컨트롤러를 아주 다양하게 생성할 수 있도록 아주 추상적인 방법으로 컨트롤러를 구현한다.

스프링 2.5에서 @RequestMapping, @RequestParam, @ModelAttribute 등과 같은 어노테이션을 사용해서 MVC 컨트롤러에 어노테이션 기반의 프로그래밍 모델을 도입했다. 이 어노테이션 지원은 서블릿 MVC와 포틀릿 MVC 모두에서 사용할 수 있다. 이 방식으로 구현한 컨트롤러는 특정 기반 클래스를 확장하거나 특정 인터페이스를 구현하지 말아야 한다. 게다가 이러한 컨트롤러에 서블릿이나 포틀릿 기능에 대한 접근을 쉽게 설정할 수 있더라도 보통 서블릿이나 포틀릿 API에 직접적인 의존성을 가지지 않아야 한다.

Tip
예제 저장소에서 사용할 수 있는 MvcShowcase, MvcAjax, MvcBasic, PetClinic, PetCare 등 다수의 웹 어플리케이션은 이번 섹션에서 설명한 어노테이션 지원을 사용한다.

@Controller
public class HelloWorldController {

  @RequestMapping("/helloWorld")
  public String helloWorld(Model model) {
    model.addAttribute("message", "Hello World!");
    return "helloWorld";
  }
}

여기서 보듯이 @Controller와 @RequestMapping 어노테이션으로 메서드 이름과 시그니처를 유연하게 할 수 있다. 이 특정 예제에서 메서드는 Model을 받고 뷰 이름으로 String을 반환하지만 이번 섹션의 뒷부분에서 설명하듯이 여러가지 다른 메서드 파라미터와 반환값을 사용할 수 있다. @Controller와 @RequestMapping을 비롯한 다수의 다른 어노테이션들은 스프링 MVC 구현체의 기초를 형성한다. 이번 섹션에서는 이러한 어노테이션을 설명하고 서블릿 환경에서 이러한 어노테이션들을 사용하는 가장 일반적인 방법을 설명한다.


16.3.1 @Controller로 컨트롤러 정의하기
@Controller 어노테이션은 해당 클래스가 컨트롤러의 역할을 한다는 것을 나타낸다. 스프링에서는 어떤 컨트롤러 기반 클래스도 확장할 필요가 없고 서블릿 API를 참조할 필요도 없다. 하지만 필요하다면 서블릿에 특화된 기능을 참조할 수 있다.

@Controller 어노테이션은 어노테이션이 붙은 클래스의 스테레오 타입처럼 동작하고 클래스의 역할을 나타낸다. 디스패처는 매핑된 메서드에 이러한 어노테이션이 붙은 클래스를 찾고 @RequestMapping 어노테이션(다음 섹션 참조)을 탐지한다.

디스패처의 컨텍스트에 표준 스프링 빈 정의를 사용해서 어노테이션이 붙은 컨트롤러 빈을 명시적으로 정의할 수 있다. 하지만 클래스패스에서 컴포넌트 클래스를 탐지와 컴포넌트 클래스들을 위해 빈 정의의 자동등록에 대한 스프링의 일반적인 지원에 맞추어 @Controller 스테레오타입도 자동탐지가 가능하다.

이렇게 어노테이션이 붙은 컨트롤러의 자동탐지를 활성화하려면 설정에 컴포넌트 스캔을 추가해야 한다. 다음 XML 코드처럼 spring-context 스키마를 사용해라.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <context:component-scan base-package="org.springframework.samples.petclinic.web"/>

  <!-- ... -->
</beans>


16.3.2 @RequestMapping으로 요청 매핑하기
/appointments같은 URL을 매핑하려면 전체 클래스나 특정 핸들러 메서드에 @RequestMapping 어노테이션을 사용해라. 보통 클래스수준의 어노테이션은 폼(form) 컨트롤러에 특정 요청 경로(또는 경로 패턴)을 매핑하고 추가적인 메서드 수준의 어노테이션은 특정 HTTP 요청 메서드("GET", "POST" 등)나 HTTP 요청 파라미터 상태로 매핑 범위를 좁힌다.

Petcare 예제에서 가져온 다음 코드는 이 어노테이션을 사용하는 스프링 MVC 어플리케이션의 컨트롤러를 보여준다.

@Controller
@RequestMapping("/appointments")
public class AppointmentsController {

  private final AppointmentBook appointmentBook;

  @Autowired
  public AppointmentsController(AppointmentBook appointmentBook) {
    this.appointmentBook = appointmentBook;
  }

  @RequestMapping(method = RequestMethod.GET)
  public Map<String, Appointment> get() {
    return appointmentBook.getAppointmentsForToday();
  }

  @RequestMapping(value="/{day}", method = RequestMethod.GET)
  public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
    return appointmentBook.getAppointmentsForDay(day);
  }

  @RequestMapping(value="/new", method = RequestMethod.GET)
  public AppointmentForm getNewForm() {
    return new AppointmentForm();
  }

  @RequestMapping(method = RequestMethod.POST)
  public String add(@Valid AppointmentForm appointment, BindingResult result) {
    if (result.hasErrors()) {
      return "appointments/new";
    }
    appointmentBook.addAppointment(appointment);
    return "redirect:/appointments";
  }
}

이 예제에서 @RequestMapping를 여러 곳에서 사용했다. 첫번째는 타입(클래스) 수준에서 사용했고 이 컨트롤러의 모든 핸들링 메서드는 /appointments 경로에 상대적이라는 것을 나타낸다. get() 메서드의 @RequestMapping는 좀 더 세련되었다. 이 @RequestMapping은 GET 요청만 받아들인다. 즉, /appointments에 대한 HTTP GET 요청만 이 메서드를 실행한다. post()도 유사하다. getNewForm()은 HTTP 메서드와 경로를 함께 사용했으므로 appointments/new에 대한 GET 요청을 이 메서드가 처리한다.

getForDay() 메서드는 @RequestMapping의 또다른 사용방법인 URI 템플릿 보여준다. (다음 섹션 참고.)

클래스 수준의 @RequestMapping는 필수가 아니다. 클래스에 @RequestMapping를 사용하지 않으면 모든 경로는 상대경로가 아니라 절대경로가 된다. PetClinic 예제 어플리케이션에서 가져온 다음 예제는 @RequestMapping를 사용한 다중 액션 컨트롤러를 보여준다.

@Controller
public class ClinicController {

  private final Clinic clinic;

  @Autowired
  public ClinicController(Clinic clinic) {
    this.clinic = clinic;
  }

  @RequestMapping("/")
  public void welcomeHandler() {
  }

  @RequestMapping("/vets")
  public ModelMap vetsHandler() {
    return new ModelMap(this.clinic.getVets());
  }
}

인터페이스 메서드에 @RequestMapping 사용하기
컨트롤러 객체에 프록시 생성을 필요로 하는 기능을 적용하는 경우가 어노테이션이 붙은 컨트롤러 클래스를 사용할 때의 일반적인 어려운 부분이다. (예를 들면 @Transactional 메서드) 보통은 JDK 동적 프록시를 사용하려고 컨트롤러에 인터페이스를 도입할 것이다. 이렇게 하려면 @RequestMapping 어노테이션을 인터페이스로 옮겨야 하고 매핑 메카니즘이 프록시가 노출한 인터페이스만 "볼 수 있다". 그렇지 않으면 기능을 적용할 컨트롤러의 설정에 proxy-target-class="true"를 활성화할 수 있다.(이 트랜잭션 시나리오에서는 <tx:annotation-driven />) 이렇게 하는 것은 GCLIB기반의 하위클래스 프록시를 인터페이스 기반 JDK 프록시 대신에 사용해야 함을 의미한다. 다양한 프록시 메카니즘에 대한 자세한 내용은 Section 8.6,  "프록싱 메카니즘"를 참고해라.



16.3.2.1 URI 템플릿 패턴
@RequestMapping 메서드의 URL에서 선택하는 부분에 편리하게 접근하려고 URI 템플릿을 사용할 수 있다.

URI 템플릿은 URI과 유사한 문자열로 하나이상의 변수명을 포함하고 있다. 이러한 변수에 값을 대체했을 때 비로소 탬플릿은 URI가 된다. URI 템플릿에 제안된 RFC은 어떻게 URI를 파라미터화하는지 정의하고 있다. 예를 들어 URI 템플릿 http://www.example.com/users/{userId}는 변수 userId를 포함하고 있다. 이 변수에 fred 값을 할당하면 http://www.example.com/users/fred가 된다.

스프링 MVC에서 URI 템플릿 변수의 값에 바인딩하려고 메서드 인자에 @PathVariable 어노테이션을 사용할 수 있다.

@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
  Owner owner = ownerService.findOwner(ownerId);
  model.addAttribute("owner", owner);
  return "displayOwner";
}

URI 템플릿 "/owners/{ownerId}"는 ownerId라는 변수명을 지정한다. 컨트롤러가 이 요청을 처리할 때 URI의 적합한 부분에서 찾아낸 값을 ownerId의 값에 설정한다. 예를 들어 /owners/fred로 들어오는 요청에서 ownerId의 값은 fred가 된다.

Tip
@PathVariable 어노테이션을 처리하려면 스프링 MVC가 이름과 일치하는 URI 템플릿 변수를 찾아야 한다. 어노테이션에서 이를 지정할 수 있다.


@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
  // 구현부는 생략한다
}

또는 URI 템플릿 변수 이름이 메서드 인자의 이름과 일치한다면 자세한 내용은 생략할 수 있다. 코드를 디버깅 정보없이 컴파일하지 않는 한 스프링 MVC가 메서드 인자 이름와 일치하는 URI 템플릿 변수 이름를 찾아낼 것이다.

@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
  // 구현부는 생략한다
}

메서드는 다수의 @PathVariable 어노테이션을 가질 수 있다.

@RequestMapping(value="/owners/{ownerId}/pets/{petId}", method=RequestMethod.GET)
public String findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
  Owner owner = ownerService.findOwner(ownerId);
  Pet pet = owner.getPet(petId);
  model.addAttribute("pet", pet);
  return "displayPet";
}

URI 템플릿은 타입과 경로의 @RequestMapping 어노테이션으로 만들 수 있다. 그래서 findPet() 메서드는 /owners/42/pets/21같은 URL로 실행할 수 있다.

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {

  @RequestMapping("/pets/{petId}")
  public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
    // 구현부는 생략한다
  }
}

@PathVariable 인자는 int, long, Date 등의 간단한 어떠한 타입이라도 될 수 있다. 스프링이 자동으로 적절한 타입으로 변환하거나 타입변환에 실패한다면 TypeMismatchException를 던진다. 추가적인 데이터 타입을 파싱하도록 등록할 수도 있다. Section 16.3.3.14, “메서드 파라미터와 타입 변환”와 Section 16.3.3.15, “WebDataBinder 초기화 커스터마이징”를 참고해라.


16.3.2.2 정규표현식을 사용한 URI 템플릿 패턴
때로는 URI 템플릿 변수를 정의할 때 더 정확하게 해야할 필요가 있다. "/spring-web/spring-web-3.0.5.jar"같은 URL을 생각해 보자. 이러한 URL을 어떻게 여러 URL을 받아들이도록 하는가?

@RequestMapping 어노테이션은 URI 템플릿 변수에 정규표현식을 지원한다. 문법은 {varName:regex}인데 첫부분은 변수명이고 두번째 부분은 정규표현식이다. 예를 들면 다음과 같다.

@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\d\.\d\.\d}.{extension:\.[a-z]}")
  public void handle(@PathVariable String version, @PathVariable String extension) {
    // ...
  }
}


16.3.2.3 경로(Path) 패턴
URI 템플릿에 추가해서 @RequestMapping 어노테이션도 Ant방식의 경로 패턴을 지원한다.(예를 들면 /myPath/*.do) URI 템플릿과 Ant 방식을 섞어서 사용하는 것도 가능하다.(예를 들면 /owners/*/pets/{petId})


16.3.2.4 소비가능한 미디어 타입(Consumable Media Types)
소비가능한 미디어 타입의 목록을 지정해서 주요한 매핑을 제한할 수 있다. Content-Type 요청 헤더가 지정한 미디어타입과 일치할 때만 요청이 매칭할 것이다. 예를 들면 다음과 같다.

@Controller
@RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {
  // 구현부는 생략한다
}

소비가능한 미디어 타입 표현식에서 text/plain의 Content-Type을 제외한 모든 요청과 일치하도록 !text/plain처럼 부정문을 사용할 수도 있다.

Tip
consumes 조건은 타입수준과 메서드 수준에서 지원한다. 대부분의 다른 조건과는 달리 타입수준에서 사용했을 때 메서드 수준의 소비가능한 타입이 타입 수준의 소비가능한 타입을 확장하는 게 아니라 오버라이드한다.



16.3.2.5 생산가능한 미디어 타입(Producible Media Types)
생산가능한 미디어 타입의 목록을 지정해서 주요 매핑을 제한할 수 있다. Accept 요청헤더가 이러한 값 중 하나와 일치할 때만 요청이 매칭될 것이다. 게다가 produces 상태를 사용하면 produces 조건에 지정한 미디어 타입과 관련된 응답을 생성하는데 사용한 실제 컨텐트 타입을 보장한다. 예를 들면 다음과 같다.

@Controller
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, produces="application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
  // 구현부는 생략한다
}

consumes에서처럼 생산가능한 미디어타입 표현식에는 text/plain의 Accept 헤더값을 가진 요청을 제외한 모든 요청에 매칭되도록 !text/plain처럼 부정문을 사용할 수 있다.

Tip
produces 조건은 타입수준과 메서드 수준에서 지원한다. 다른 대부분의 조건과는 달리 타입수준에서 사용했을 때 메서드수준의 producible 타입은 타입수준의 producible 타입을 확장하는 게 아니라 오버라이드한다.



16.3.2.6 요청 파라미터와 헤더값
"myParam", "!myParam", "myParam=myValue"같은 요청 파라미터 조건으로 요청에 대한 매칭을 제한할 수 있다. 앞에 두가지는 요청파라미터의 존재/부재 여부를 확인하고 세번째는 특정 파라미터 값을 확인한다. 다음은 요청 파라미터 값 조건에 대한 예제이다.

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {

  @RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, params="myParam=myValue")
  public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
    // 구현부는 생략한다
  }
}

요청 헤더의 존재/부재 여부에 대한 검사나 지정한 요청 헤더값에 기반한 매칭도 동일하게 이뤄질 수 있다.

@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {

@RequestMapping(value = "/pets", method = RequestMethod.GET, headers="myHeader=myValue")
  public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
    // 구현부는 생략한다
  }
}

Tip
미디어타입 와일드카드를 사용해서 Content-Type와 Accept 헤더값을 일치하도록 할 수 있기는 하지만 대신에 consumes와 produces 조건을 각각 사용하는 것을 권장한다. 이 둘은 해당 목적에 맞게 만들어진 것이다.



16.3.3 @RequestMapping 핸들러 메서드 정의하기
@RequestMapping 핸들러 메서드는 아주 유연한 시그니처를 가질 수 있다. 지원하는 메서드 인자와 반환값은 다음 섹션에서 설명한다. 대부분의 인자는 BindingResult 인자만 제외하고 임의의 순서로 사용할 수 있다. 이는 다음 섹션에서 설명한다.


16.3.3.1 지원하는 메서드 인자 타입
다음은 지원하는 메서드 인자들이다.

  • 요청객체나 응답 객체(서블릿 API). ServletRequest나 HttpServletRequest같은 특정 요청이나 응답 타입을 어떠한 것이라도 선택할 수 있다.
  • HttpSession타입의 세션 객체 (서블릿 API): 이 타입의 인자는 대응되는 세션의 존재를 강제한다. 그래서 이 인자는 결코 null이 되지 않는다.
    Note
    세션 접근은 특히 서블릿 환경에서는 쓰레드세이프하지 않을 것이다. 여러 요청이 하나의 세션에 동시접근해야 한다면 RequestMappingHandlerAdapter의 "synchronizeOnSession" 플래그를 "true"로 설정하는 것을 고려해 봐라.
  • org.springframework.web.context.request.WebRequest 나 org.springframework.web.context.request.NativeWebRequest. 요청/세션 속성 접근과 마찬가지로 네이티브 서블릿/포틀릿 API에 묶이지 않고 일반적인 요청 파라미터에 대한 접근을 허용한다.
  • 현재 요청의 로케일에 대한 java.util.Locale로 사용가능한 가장 구체적인 로케일 리졸버(사실상 서블릿 환경에 설정한 LocaleResolver)가 결정한다.
  • 요청의 내용에 접근하기 위한 java.io.InputStream / java.io.Reader. 이 값은 서블릿 API가 노출한 것처럼 가공되지 않은(raw) InputStream/Reader이다.
  • 응답의 내용을 생성하기 위한 java.io.OutputStream / java.io.Writer. 이 값은 서블릿 API가 노출한 것처럼 가공되지 않은(raw) OutputStream/Writer이다.
  • java.security.Principal는 현재 인증된(authenticated) 사용자를 담고 있다.
  • URI 템플릿 변수에 접근하는 @PathVariable 어노테이션이 붙은 파라미터. Section 16.3.2.1, “URI 템플릿 패턴”를 참고해라.
  • 특정 서블릿 요청 파라미터에 접근하는 @RequestParam 어노테이션이 붙은 파라미터. 파라미터 값은 선언된 메서드 인자 타입으로 변환된다. Section 16.3.3.3, “요청 파라미터를 @RequestParam를 가진 메서드 파라미터에 바인딩하기”를 참고해라.
  • 특정 서블릿 요청 HTTP 헤더에 접근하는 @RequestHeader 어노테이션이 붙은 파라미터. 파라미터 값들은 선언된 메서드 인자 타입으로 변환된다.
  • HTTP 바디에 접근하기 위해 @RequestBody 어노테이션이 붙은 파라미터. 파라미터 값은 HttpMessageConverter를 사용해서 선언한 메서드 인자 타입으로 변환한다. Section 16.3.3.4, “@RequestBody 어노테이션으로 요청 바디 매핑하기”를 참고해라.
  • "multipart/form-data" 요청 부분의 내용에 접근하기 위해 @RequestPart 어노테이션이 붙은 파라미터. Section 16.10.5, “프로그래밍적인 클라이언트의 파일업로드 요청 처리”와 Section 16.10, “스프링의 멀티파트(multipart) (파일 업로드) 지원”를 참고해라.
  • 서블릿 요청 HTTP 헤더와 내용에 접근하기 위한 HttpEntity<?> 파라미터. HttpMessageConverter를 사용해서 요청 스트림을 엔티티 바디로 변환할 것이다. Section 16.3.3.6, “HttpEntity<?> 사용하기”를 참고해라.
  • 웹 뷰에 노출되는 암묵적인 모델을 풍부하게 하기 위한 java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap.
  • 리다이렉트에 사용하는 속성의 정확한 세트를 지정하는 org.springframework.web.servlet.mvc.support.RedirectAttributes이고 플래시(flash) 속성(리다이렉트된 후에 요청이 속성을 사용할 수 있도록 서버측에 임시적으로 저장된 속성)을 추가하기도 한다. 메서드가 "redirect:" 접두사가 붙은 뷰 이름이나 RedirectView를 반환하면 암시적인 모델 대신에 RedirectAttributes를 사용한다.
  • @InitBinder나 HandlerAdapter 설정에 따라 커스터마이징할 수 있는 타입 변환으로 요청 파라미터를 빈 프로퍼티(setter로)나 필드에 직접 바인딩하는 커맨드 객체나 폼 객체. RequestMappingHandlerAdapter의 webBindingInitializer 프로퍼티를 참고해라. 유효성 검사결과를 가진 이러한 커맨드 객체는 기본적으로 커맨드 클래스 이름을 사용해서 모델 속성으로 노출된다. 예를 들어 "some.package.OrderAddress" 타입의 커멘드 객체라면 "orderAddress" 모델 속성이 된다. ModelAttribute 어노테이션을 사용한 모델 속성 이름을 커스터마이징할 수 있도록 메서드 인자에 사용할 수 있다.
  • 앞의 커맨드 객체나 폼 객체(바로 앞의 메서드 인자)에 대한 유효성검사 결과인 org.springframework.validation.Errors / org.springframework.validation.BindingResult.
  • org.springframework.web.bind.support.SessionStatus 핸들러 타입 수준의 @SessionAttributes 어노테이션이 나타내는 세션 속성을 정리하도록 폼 처리를 완료되었다고 표시하는 상태 핸들.
  • org.springframework.web.util.UriComponentsBuilder 현재 요청의 호스트, 포트, 스키마, 컨텍스트 경로, 서블릿 매핑의 리터럴 부분에 상대적인 URL을 준비하는 빌더.

Errors나 BindingResult 파라미터는 하나의 모델 객체 이상을 가지는 메서드 시그니처처럼 즉시 바인딩되는 모델 객체를 따라야 한고 스프링은 각각에 분리된 BindingResult 인스턴스를 생성할 것이므로 다음 예제는 동작하지 않는다.

Example 16.1. BindingResult와 @ModelAttribute의 유효하지 않은 순서

@RequestMapping(method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet,
  Model model, BindingResult result) { … }

Pet과 BindingResult사이에는 Model 파라미터가 존재한다. 이 예제가 동작하려면 다음과 같이 파라미터의 순서를 변경해야 한다.

@RequestMapping(method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet,
  BindingResult result, Model model) { … }


16.3.3.2 지원하는 메서드 반환 타입
다음은 지원하는 반환 타입이다.

  • 커맨드 객체로 암묵적으로 풍부해지는 모델과 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자(accessor) 메서드의 결과를 가진 ModelAndView 객체.
  • RequestToViewNameTranslator로 암묵적으로 결정되는 뷰 이름과 커맨드 객체로 암묵적으로 풍부해지는 모델과 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자(accessor) 메서드의 결과를 가진 Model 객체.
  • RequestToViewNameTranslator가 암묵적으로 결정하는 뷰 이름과 커맨드 객체로 암묵적으로 풍부해지는 모델과 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자(accessor) 메서드의 결과를 가진 모델을 노출하는 Map 객체.
  • 커맨드 객체로 암묵적으로 풍부해지는 모델과 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자(accessor) 메서드의 결과를 가진 View 객체. Model 인자를 선언해서 핸들러 메서드도 프로그래밍적으로 모델을 풍부하게 할 수 있다.(앞부분 참조)
  • 커맨드 객체로 암묵적으로 풍부해지는 모델과 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자(accessor) 메서드의 결과를 가지고 논리적인 뷰 이름으로 해석되는 String 값. Model를 선언해서 핸들러 메서드도 프로그래밍적으로 모델을 풍부하게 할 수 있다.(앞부분 참조)
  • 메서드가 응답을 직접 처리하거나(해당 목적에 ServletResponse / HttpServletResponse 타입의 인자를 선언해서 응답 내용을 직접 작성함으로써) RequestToViewNameTranslator로 암묵적으로 정의되는 뷰 이름을 추정하면(핸들러 메서드 시그니처에 응답 인자를 선언하지 않는다) void이다.
  • 메서드가 @ResponseBody 어노테이션이 붙어있으면 반환 타입이 응답 HTTP 바디에 쓰여진다. 반환값을 HttpMessageConverter를 사용해서 선언한 메서드 인자 타입으로 변환할 것이다. Section 16.3.3.5, “@ResponseBody 어노테이션으로 응답 바디 매핑하기”를 참고해라.
  • 서블릿 응답 HTTP 헤더와 내용에 접근하는 HttpEntity<?>나 ResponseEntity<?>. 엔티티 바디는 HttpMessageConverter를 사용해서 응답 스트림으로 변환될 것이다. Section 16.3.3.6, “HttpEntity<?> 사용하기”를 참고해라.
  • 다른 모든 반환 타입은 메서드 수준에 @ModelAttribute로 지정한 속성명을 사용해서(또는 반환 타입 클래스명에 기반한 기본 속성명) 뷰에 노출할 단일 모델 속성(attribute)로 간주한다. 모델은 커맨드 객체로 암묵적으로 풍부해지고 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자(accessor) 메서드의 결과가 된다.


16.3.3.3 요청 파라미터를 @RequestParam를 가진 메서드 파라미터에 바인딩하기
요청 파라미터를 컨트롤러의 메서드 파라미터로 바인딩하려면 @RequestParam 어노테이션을 사용해라.

다음 예제에서 사용방법을 보여준다.

@Controller
@RequestMapping("/pets")
@SessionAttributes("pet")
public class EditPetForm {

  // ...

  @RequestMapping(method = RequestMethod.GET)
  public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
    Pet pet = this.clinic.loadPet(petId);
    model.addAttribute("pet", pet);
    return "petForm";
  }

  // ...

이 어노테이션을 사용한 파라미터는 기본적으로 필수 파라미터이지만 @RequestParam의 required 속성을 false로 설정해서 (예시. @RequestParam(value="id", required=false)) 파라미터를 선택사항으로 지정할 수 있다.

대상 메서드 파라미터 타입이 String이 아니라면 자동으로 타입변환을 한다. Section 16.3.3.14, “메서드 파라미터와 타입 변환”를 참고해라.


16.3.3.4 @RequestBody 어노테이션으로 요청 바디 매핑하기
@RequestBody 메서드 파라미터 어노테이션은 메서드 파라미터가 HTTP 요청 바디의 값에 바인딩되어야 한다는 것을 의미한다. 다음은 그 예제이다.

@RequestMapping(value = "/something", method = RequestMethod.PUT)
public void handle(@RequestBody String body, Writer writer) throws IOException {
  writer.write(body);
}

HttpMessageConverter를 사용해서 요청 바디를 메서드 인자로 변환한다. HttpMessageConverter가 HTTP 요청 메시지를 객체로 변환하고 객체에서 HTTP 응답 바디로 변환하는 담당을 한다. RequestMappingHandlerAdapter는 다음의 기본 HttpMessageConverters로 @RequestBody 어노테이션을 지원한다.

  • ByteArrayHttpMessageConverter는 바이트 배열을 변환한다.
  • StringHttpMessageConverter는 문자열을 변환한다.
  • FormHttpMessageConverter는 데이터를 MultiValueMap<String, String>으로 MultiValueMap<String, String>을 데이터로 변환한다.
  • SourceHttpMessageConverter는 javax.xml.transform.Source로(에서) 변환한다.

이러한 컨버터에 대한 자세한 내용은 메시지 컨버터를 참고해라. MVC 네임스페이스를 사용한다면 더 넓은 범위의 메시지 컨버터가 기본적으로 등독된다는 것도 기억해라. 자세한 내용은 ???를 참고해라.

XML을 읽고 쓰고 싶다면 org.springframework.oxm 패키지의 구체적인 Marshaller와 Unmarshaller로 MarshallingHttpMessageConverter를 설정해야 할 것이다. 예를 들면 다음과 같다.

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
  <property name="messageConverters">
    <util:list id="beanList">
      <ref bean="stringHttpMessageConverter"/>
      <ref bean="marshallingHttpMessageConverter"/>
    </util:list>
  </property
</bean>

<bean id="stringHttpMessageConverter" class="org.springframework.http.converter.StringHttpMessageConverter"/>

<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
  <property name="marshaller" ref="castorMarshaller" />
  <property name="unmarshaller" ref="castorMarshaller" />
</bean>

<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>

@RequestBody 메서드 파라미터는 설정한 Validator 인스턴스를 사용해서 유효성검사를 하는 경우 @Valid 어노테이션을 붙힐 수 있다. MVC 네임스페이스를 사용한다면 JSR-303 구형체가 클래스패으세서 사용가능하다고 가정하고 JSR-303 밸리데이터를 자동으로 설정한다. 유효성검사를 실패하면 RequestBodyNotValidException가 발생한다. 예외는 DefaultHandlerExceptionResolver가 처리하고 유효성검사 오류를 담고 있는 메시지와 함께 클라이언트에게 400 오류를 보낸다.

Note
MVC 네임스페이스로 메시지 컨버터와 밸리데이터를 설정하는 자세한 내용은 ???도 참고해 봐라.



16.3.3.5 @ResponseBody 어노테이션으로 응답 바디 매핑하기
@ResponseBody 어노테이션은 @RequestBody와 유사하다. 이 어노테이션은 메서드 위에 붙힐 수 있고 반환값이 HTTP 응답 바디에 바로 쓰여져야 한다는 것을 의미한다. (Model에는 사용할 수 없고 뷰 이름으로 해석되지 않는다.) 예를 들면 다음과 같다.

@RequestMapping(value = "/something", method = RequestMethod.PUT)
@ResponseBody
public String helloWorld()  {
  return "Hello World";
}

위의 예제는 Hello World 텍스트를 HTTP 응답 스트림에 쓸 것이다.

@RequestBody에서처럼 스프링은 HttpMessageConverter를 사용해서 반환 객체를 응답 바디로 변환한다. 이러한 컨버터에 대한 자세한 내용은 이전 섹션과 메시지 컨버터를 참고해라.


16.3.3.6 HttpEntity<?> 사용하기
HttpEntity는 @RequestBody, @ResponseBody와 유사하다. 요청 바디와 응답 바디에 접근하듯이 HttpEntity(응답에 특화된 하위클래스인 ResponseEntity)도 다음과 같이 요청 헤더와 응답 헤더에 접근할 수 있다.

@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
  String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader"));
  byte[] requestBody = requestEntity.getBody();
  // 요청 헤더와 바디로 어떤 작업을 한다

  HttpHeaders responseHeaders = new HttpHeaders();
  responseHeaders.set("MyResponseHeader", "MyValue");
  return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}

위의 예제는 MyRequestHeader 요청 헤더 값을 가져오고 바디를 바이트 배열로 읽는다. 그리고 응답에 MyResponseHeader를 추가하고 응답스트림에 Hello World를 작성한 후 응답 상태코드를 201(Created)로 설정한다.

@RequestBody와 @ResponseBody에서처럼 스프링은 요청 스트림과 응답 스트립을 변환하는데 HttpMessageConverter를 사용한다. 이러한 컨버터에 대한 자세한 내용은 이전 섹션과 메시지 컨버터를 참고해라.


16.3.3.7 메서드에 @ModelAttribute 사용하기
@ModelAttribute 어노테이션은 메서드나 메서드 인자에 사용할 수 있다. 이번 섹션에서는 메서드에서 사용하는 방법을 설명하고 다음 섹션에서는 메서드 인자에서 사용하는 방법을 설명한다.

메서드에 붙은 @ModelAttribute는 하나 이상의 모델 속성(attributes)을 추가하는 것이 해당 메서드의 목적임을 의미한다. 이러한 메서드는 @RequestMapping와 같은 인자 타입을 지원하지만 요청에 직접 매핑할 수는 없다. 대신에 컨트롤러의 @ModelAttribute 메서드는 같은 컨트롤러내의 @RequestMapping 메서드 이전에 호출된다. 다음 두 예제를 보자.

// 하나의 속성을 추가한다
// 이 메서드의 반환값은 "account"라는 이름아래 있는 모델에 추가된다.
// @ModelAttribute("myAccount")로 이름을 커스터마이징 할 수 있다

@ModelAttribute
public Account addAccount(@RequestParam String number) {
  return accountManager.findAccount(number);
}

// 다중 속성 추가한다

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
  model.addAttribute(accountManager.findAccount(number));
  // 계속 추가한다
}

@ModelAttribute 메서드는 모델에 공통적으로 필요한 속성을 추가하려고 사용한다. 예를 들면 상태나 pet 타입으로 드롭다운을 채우거나 HTML 폼에서 데이터를 표현하는데 사용하려고 Account같은 커맨드 객체를 획득하는 경우이다. 후자의 경우는 다음 섹션에서 더 자세히 얘기한다.

@ModelAttribute 메서드에는 두가지 방식이 있다. 첫번째 방법은 메서드가 반환하는 값을 속성으로 추가한다. 두번째 방법은 메서드가 Model을 받고 Model에 다수의 모델 속성을 추가한다. 필요에 따라 이 두가지 방법중에 선택해서 사용할 수 있다.

컨트롤러는 필요한 수만큼 @ModelAttribute 메서드를 가질 수 있다. 이러한 메서드는 모두 같은 컨트롤러의 @RequestMapping 메서드가 이전에 실행된다.

Tip
모델 속성 명을 명시적으로 지정되지 않았을 때는 어떻게 되는가? 이러한 경우 타입에 기반한 기본 이름이 모델 속성에 할당된다. 예를 들어 메서드가 Account 타입의 객체를 반환하면 "account"를 기본이름으로 사용한다. @ModelAttribute 어노테이션의 값으로 이를 변경할 수 있다. 직접 Model에 속성을 추가한다면 적절히 오버로드된 addAttribute(..) 메서드를 사용해라.(속성명이 있거나 없거나)

@ModelAttribute 어노테이션을 @RequestMapping 메서드에도 마찬가지로 사용할 수 있다. 이러한 경우 @RequestMapping 메서드의 반환값을 뷰 이름이 아니라 모델 속성으로 해석한다. 뷰 이름은 반환값이 없는 메서드 보다는 뷰 이름 관례에서 얻어낸다.(Section 16.12.3, “뷰 - RequestToViewNameTranslator” 참고)


16.3.3.8 메서드 인자에 @ModelAttribute 사용하기
이전 섹션에서 설명했듯이 메서드나 메서드 인자에 @ModelAttribute 를 사용할 수 있다. 이번 섹션에서는 메서드 인사에서 사용하는 방법을 설명한다.

메서드 인자에 사용한 @ModelAttribute는 모델에서 인자를 가져와야 함을 의미한다. 모델에서 제공하지 않는다면 먼저 인자가 인스턴스화 되어야 하고 그 다음에 모델에 추가된다. 모델에 존재한다면 일치하는 이름을 가진 모든 요청 파라미터에 필드가 존재해야 한다. 이는 스프링 MVC에서 데이터 바인딩이라고 알려져 있고 각 폼 필드를 개별적으로 수비게 파싱할 수 있는 아주 유용한 메카니즘이다.

@RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute Pet pet) {
}

위의 예제에서 Pet 인스턴스는 어디서 가져오는가? 몇가지 선택사항이 있다.

  • @SessionAttributes를 사용했으므로 이미 모델에 있을 수 있다. - Section 16.3.3.9, “요청간에 HTTP 세션의 모델 속성을 저장하는데 @SessionAttributes 사용하기”를 참고해라.
  • 이전 섹션에서 설명했듯이 같은 컨트롤러의 @ModelAttribute 메서드때문에 이미 모델에 있을 수 있다.
  • URI 템플릿 변수와 타입 컨버터에 기반해서 얻어올 수 있다. (아래에서 더 자세히 설명한다.)
  • 기본 생성자를 사용해서 초기화할 수 있다.
@ModelAttribute 메서드는 @SessionAttributes를 사용해서 요청간에 선택적으로 저장할 수 있는 데이터베이스에서 속성을 가져오는 일반적인 방법이다. 몇몇 경우에 URI 템플릿 변수와 타입 컨버터를 사용해서 속성을 가져오는데 이 방법이 편리하다. 다음은 그 예제이다.

@RequestMapping(value="/accounts/{account}", method = RequestMethod.PUT)
public String save(@ModelAttribute("account") Account account) {
}

이 예제에서 모델 속성의 이름(여기서는 "account")이 URI 템플릿 변수이름과 일치한다. String account 값을 Account 인스턴스로 바꿀 수 있는 Converter<String, Account>를 등록했다면 위의 예제는 @ModelAttribute 메서드 없이도 동작할 것이다.

다음 단계는 데이터 바인딩이다. WebDataBinder 클래스는 요청 파라미터 이름(쿼리스트링 파라미터와 폼필드를 포함해서)을 모델 속성 필드의 이름으로 매칭한다. 타입 변환(문자열에서 대상 필드 타입으로)후에 필드 매칭을 필요한 곳에 적용한다. 데이터 바인딩과 유효성 검사는 Chapter 6, 유효성검사(validation), 데이터 바인딩, 타입 변환에서 다룬다. 컨트롤러 수준의 데이터 바인딩 과정을 커스터마이징하는 것은 Section 16.3.3.15, “WebDataBinder 초기화 커스터마이징”에서 다룬다.

데이터 바인딩 결과에 따라 필요한 필드가 누락되었다거나 타입 변환 오류같은 오류가 있을 수 있다. 이러한 오류를 확인하려면 BindingResult 인자 바로 뒤에 BindingResult 인자를 추가해라.

@RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {

  if (result.hasErrors()) {
    return "petForm";
  }

  // ...
}

BindingResult에서 스프링의 <errors> 폼 태그의 지원으로 오류를 볼 수 있는 동일한 폼을 렌더링하는 것이 일반적인 경우에 오류가 있는지 확인할 수 있다.

데이터 바인딩에 추가적으로 데이터 바인딩 오류를 기록하는데 사용한 같은 BindingResult를 전달하는 자신만의 커스텀 밸리데이터를 사용해서 유효성검사를 실행할 수도 있다. 이 방법으로 한 곳에서 데이터 바인딩과 유효성검사 오류를 확인하고 사용자에게 보고할 수 있다.

@RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {

  new PetValidator().validate(pet, result);
  if (result.hasErrors()) {
    return "petForm";
  }

  // ...
}

또는 JSR-303 @Valid 어노테이션을 추가해서 자동으로 유효성검사가 이뤄지게 할 수 있다.

@RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {

  if (result.hasErrors()) {
    return "petForm";
  }

  // ...
}

유효성검사를 설정하고 사용하는 방법은 Section 6.7, “Spring 3 유효성 검사(Validation)”와 Chapter 6, 유효성검사(validation), 데이터 바인딩, 타입 변환를 참고해라.


16.3.3.9 요청간에 HTTP 세션의 모델 속성을 저장하는데 @SessionAttributes 사용하기
타입 수준의 @SessionAttributes 어노테이션은 특정 핸들러가 사용하는 세션 속성을 선언한다. 이는 보통 이어지는 요청간에 폼에 기반한 빈으로 제공하도록 세션이나 대화식 스토리지에 투명하게 저장되어야 하는 모델 속성의 이름이나 모델 속성의 타입의 목록이다.

다음 코드는 모델 속성명을 지정해서 이 어노테이션의 사용방법을 보여준다.

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {
  // ...
}

Note
컨트롤러 인터페이스(예를 들면 AOP 프록시)를 사용할 때는 모든 매핑 어노테이션 (@RequestMapping와 @SessionAttributes같은)을 구현 클래스가 아니라 컨트롤러 인터페이스에 일관성있게 써야 한다.



16.3.3.10 리다이렉트와 플래시(flash) 속성 지정하기
기본적으로 모든 모델 속성은 리다이렉트 URL의 URI 템플릿 변수로 노출하는 것으로 간주한다. 프리미티브 타입이나 프리미티브 타입의 컬렉션/배열의 남은 속성들은 자동적으로 쿼리 파라미터로 추가한다.

하지만 어노테이션이 붙은 컨트롤러에서 모델은 렌더링을 위해서 처음부터 추가된 부가적인 속성들을 가지고 있을 수 있다.(예를 들면 드롭다운 필드값) 리다이렉트 시나리오에서 사용한 속성을 정확하게 제어하려고 @RequestMapping 메서드가 @RequestMapping 타입의 인자를 선언하고 RedirectView에서 사용하기 위해 속성에 추가하려고 이를 사용할 수 있다. 컨트롤러 메서드가 리다이렉트를 한다면 RedirectAttributes의 내용을 사용한다. 리다이렉트 하지 않는다면 기본 Model의 내용을 사용한다.

RequestMappingHandlerAdapter는 컨트롤러 메서드가 리다이렉트한다면 기본 RequestMappingHandlerAdapter를 절대 사용하지 말아야 한다는 의미로 사용할 수 있는 "ignoreDefaultModelOnRedirect"라는 플래그를 제공한다. 그렇지 않으면 컨트롤러 메서드가 RedirectAttributes 타입의 속성을 선언해야 한다. 선언하지 않았다면 RedirectView에 전달되어야 하는 속성은 없다. MVC 네임스페이스와 MVC Java config (@EnableWebMvc를 통해서) 둘 다 하위 호환성을 유지하기 위해 이 플래스를 false로 설정하고 있다. 하지만 새로운 어플리케이션에서는 이 플래그를 true로 설정하기를 권장한다.

RedirectAttributes 인터페이스도 플래시(flash) 속성을 추가하는데 사용할 수 있다. 대상 리다이렉트 URL이 되는 리다이렉트 속성과는 달리 플래시 속성은 HTTP 세션에 저장된다.(그래서 URL에 나타나지 않는다.) 대상 리다이렉트 URL을 제공하는 컨트롤러의 모델은 자동으로 세션에서 플래시 속성을 제거한 후 이러한 플래시 속성을 받는다. 스프링 MVC 플래시 속성에 대한 일반적인 지원은 Section 16.6, “플래시(flash) 속성 사용하기”를 참고해라.


16.3.3.11 "application/x-www-form-urlencoded" 데이터에서 동작하기
브라우저 클라이언트의 폼 제출(form submission) 요청을 지원하는 @ModelAttribute의 사용은 이전 섹션에서 다루었다. 같은 어노테이션을 브라우저가 아닌 클라이언트의 요청에 사용하는 것도 권장한다. 하지만 HTTP PUT 요청에서 사용할 때 한가지 중요한 차이점이 있다. 브라우저는 HTTP GET이나 HTTP POST로 폼 데이터를 제출할 수 있다. 브라우저가 아닌 클라이언트는 HTTP PUT으로도 폼을 제출할 수 있다. 서블릿 명세가 HTTP PUT이 아니라 HTTP POST에서만 폼 필드 접근을 지원하는 메서드의 ServletRequest.getParameter*() 계열을 필요로 하기 때문에 문제점이 생긴다.

HTTP PUT 요청을 지원하려면 spring-web 모듈이 web.xml에서 설정할 수 있는 HttpPutFormContentFilter 필터를 제공해야 한다.

<filter>
  <filter-name>httpPutFormFilter</filter-name>
  <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>httpPutFormFilter</filter-name>
  <servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>

<servlet>
  <servlet-name>dispatcherServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

위의 필터는 application/x-www-form-urlencoded 컨텐트 타입의 HTTP PUT 요청을 가로채서 요청 바디에서 폼 데이터를 읽고 메서드의 ServletRequest.getParameter*() 계열로 폼 데이터를 사용할 수 있도록 ServletRequest로 감싼다.


16.3.3.12 @CookieValue 어노테이션으로 쿠키(cookie) 값 매핑하기
@CookieValue 어노테이션은 메서드 파라미터를 HTTP 쿠키 값에 바인딩되도록 한다.

http 요청과 함께 받은 다음의 쿠키를 보자.

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

다음 코드 예제는 JSESSIONID 쿠키의 값을 얻는 방법을 보여준다.

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie)  {

  //...
}

대상 메서드 파라미터 타입이 String이 아니면 타입 변환은 자동으로 적용된다. Section 16.3.3.14, “메서드 파라미터와 타입 변환”를 참고해라.

이 어노테이션은 서블릿 환경과 포틀릿 환경에서 어노테이션이 붙은 핸들러 메서드를 지원한다.


16.3.3.13 @RequestHeader 어노테이션으로 요청 헤더 속성 매핑하기
@RequestHeader 어노테이션은 메서드 파라미터를 요청 헤더에 바인딩하도록 한다.

다음은 요청 헤더의 예시이다.

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300

다음 예제 코드는 Accept-Encoding와 Keep-Alive 헤더의 값을 얻는 방법을 보여준다.

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding,
             @RequestHeader("Keep-Alive") long keepAlive)  {

  //...
}

메서드 파라미터가 String가 아니라면 타입변환을 자동으로 적용한다. Section 16.3.3.14, “메서드 파라미터와 타입 변환”를 참고해라.

Tip
콤마로 구분된 문자열을 문자열의 배열/컬렉션이나 타입 변환 시스템이 알고 있는 다른 타입으로 변환할 때 내장(built-in) 지원을 사용할 수 있다. 예를 들어 @RequestHeader("Accept") 어노테이션이 붙은 메서드 파라미터는 String의 타입이지만 String[]나 List<String>도 될 수 있다.

이 어노테이션은 서브릿 환경과 포틀릿 환경에서 어노테이션이 붙은 핸들러 메서드를 지원한다.


16.3.3.14 메서드 파라미터와 타입 변환
요청 파라미터, 경로 변수, 요청 해더, 쿠키 값등 요청에서 추출한 문자열 기반의 값들은 바인딩해야 하는 메서드 파라미터나 필드의 대상 타입으로 변환해야 한다.(예시. 요청 파라미터를 @ModelAttribute 파라미터의 필드에 바인딩하기) 대상 타입이 String이 아니라면 스프링이 자동으로 적절한 타입으로 변환한다. int, long, Date 등의 모든 간단한 타입을 지원한다. WebDataBinder(Section 16.3.3.15, “WebDataBinder 초기화 커스터마이징” 참고)를 사용하거나 FormattingConversionService로 Formatters를 등록해서(Section 6.6, “Spring 3 필드 포매팅” 참고) 변환 과정을 커스터마이징 할 수 있다.


16.3.3.15 WebDataBinder 초기화 커스터마이징
스프링의 WebDataBinder를 통해서 PropertyEditors로 요청 파라미터 바인딩으로 커스터마이징하려면 컨트롤러내의 @InitBinder 어노테이션이 붙은 메서드를 사용하거나 커스텀 WebBindingInitializer를 제공해서 설정을 구체화해서 사용할 수 있다.

@InitBinder로 데이터바인딩 커스터마이징하기
@InitBinder 어노테이션이 붙은 컨트롤러 메서드는 컨트롤러 클래스내에서 직접 웹 데이터 바인딩을 설정할 수 있도록 한다. @InitBinder는 어노테이션이 붙은 핸들러 메서드의 커맨드 객체 인자와 폼 객체 인자를 존재하게 하는데 사용할 WebDataBinder를 추기화하는 메서드를 식별한다.

이러한 init-binder 메서드들은 커맨드/폼 객체와 이에 대응되는 유효성 검사결과 객체를 제외하고 @RequestMapping이 지원하는 모든 인자를 지원한다. init-binder 메서드들은 반환값을 갖지 말아야 한다. 그러므로 보통 void로 선언한다. WebRequest나 java.util.Locale를 섞어서 사용할 때 WebDataBinder를 포함한 대표적인 인자들은 컨텍스트에 특화된 에디터를 등록하는 코드를 작성할 수 있게 한다.

다음 예제는 모든 java.util.Date 폼 프로퍼티에 CustomDateEditor를 설정하는 @InitBinder를 사용하는 방법을 보여준다.

@Controller
public class MyFormController {

  @InitBinder
  public void initBinder(WebDataBinder binder) {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    dateFormat.setLenient(false);
    binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
  }

  // ...
}

커스텀 WebBindingInitializer 설정
데이터 바인딩 초기화를 구체화하려고 WebBindingInitializer 인터페이스의 커스텀 구현체를 제공할 수 있다. AnnotationMethodHandlerAdapter에 대한 커스텀 빈 설정을 제공해서 활성화할 수 있으므로 기본 설정을 오버라이딩한다.

PetClinic 어플리케이션에서 가져온 다음 예제는 WebBindingInitializer 인터페이스의 커스텀 구현체로 여러 PetClinic 컨트롤러가 필요로 하는 PropertyEditors를 설정하는 org.springframework.samples.petclinic.web.ClinicBindingInitializer를 사용한 설정을 보여준다.

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="cacheSeconds" value="0" />
    <property name="webBindingInitializer">
        <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
    </property>
</bean>


16.3.3.16 내용을 캐싱을 쉽게하는 'Last-Modified' 응답헤더의 지원
내용 캐싱을 쉽게 하려고 @RequestMapping 메서드가 서블릿 API의 getLastModified 메서드 계약에 정의된 대로 'Last-Modified' HTTP요청을 지원하기를 바랄 것이다. 이는 해당 요청의 'If-Modified-Since' 요청헤더 값과 비교해서 마지막으로 수정된 long 값을 계산하는 것을 포함하고 304 (Not Modified) 상태코드로 응답을 보낼 가능성이 있다. 어노테이션이 붙은 컨트롤러 메서드를 다름과 같이 작성할 수 있다.

@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {

  long lastModified = // 1. 어플리케이션에 특화된 계산

  if (request.checkNotModified(lastModified)) {
    // 2. 빠른 종료 - 더이상 처리가 필요없다
    return null;
   }

  // 3. 아니면 요청을 추가로 처리하고 컨텐츠를 실제로 준비한다
  model.addAttribute(...);
  return "myViewName";
}

알아두어야 할 핵심요소가 두가지 있는데 request.checkNotModified(lastModified) 호출과 null 반환이다. 전자는 true를 반환하기 전에 응답 상태를 304로 설정한다. 후자는 전자와 함께 사용해서 스프링 MVC가 요청을 더이상 처리하지 않도록 한다.
2013/02/12 23:56 2013/02/12 23:56