이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.
16.4 핸들러 매핑
스프링의 과거 버전에서는 들어오는 웹 요청을 적절한 핸들러에 매칭하기 위해 사용자들이 웹 어플리케이션 컨텍스트에 하나 이상의 HandlerMapping 빈을 정의해야 했다. 어노테이션이 붙은 컨트롤러의 도입으로 RequestMappingHandlerMapping이 자동으로 모든 @Controller 빈의 @RequestMapping 어노테이션을 검색하므로 보통은 사용자가 HandlerMapping 빈을 정의할 필요가 없다. 하지만 HandlerMapping를 확장한 모든 HandlerMapping 클래스는 동작을 커스터마이징할 때 사용할 수 있는 다음의 프로퍼티를 가진다는 것을 가진다는 것을 기억해 두어야 한다.
interceptors
사용할 인터셉터의 목록. HandlerInterceptor는 Section 16.4.1, “Intercepting requests with a HandlerInterceptor”에서 설명한다.defaultHandler
매칭되는 핸들러를 찾지 못했을 때 사용할 기본 핸들러.order
스프링은 order 프로퍼티 값에 기반해서 컨텍스트에서 사용할 수 있는 모든 핸들러 매핑을 정렬하고 가장 먼저 일치하는 핸들러를 적용한다.alwaysUseFullPath
이 값이 true이면 스프링은 적절한 핸들러를 찾을 때 현재 서블릿 컨텍스트 내에서 전체 경로를 사용한다. false(기본값)이면 현재 서블릿 매핑내에서 경로를 사용한다. 예를 들어 서블릿이 /testing/*로 매핑되어 있고 alwaysUseFullPath 프로퍼티가 true이면 /testing/viewPage.html를 사용하고 이 프로퍼티가 false이면 /viewPage.html를 사용한다.urlDecode
스프링 2.5부터 기본값은 true이다. 인코딩된 경로를 비교하는 것을 좋아한다면 이 플래그를 false로 설정해라. 하지만 HttpServletRequest는 항상 디코딩된 형식으로 서블릿 경로를 노출한다. 인코딩된 경로와 비교할 때는 서블릿 경로가 매치되지 않는 다는 것을 알아두어라.다음 예제는 인터셉터를 설정하는 방법을 보여준다.
1 2 3 4 5 6 7 8 9 | Xml <beans> <bean id="handlerMapping" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"> <property name="interceptors"> <bean class="example.MyInterceptor"/> </property> </bean> <beans> |
16.4.1 Intercepting requests with a HandlerInterceptor
스프링의 핸들러 매핑 메카니즘은 특정 요청에 어떤 기능을 적용하기를 원할 때 유용한 핸들러 인터셉터를 포함한다. 재산을 확인하는(checking for a principal)등의 기능을 적용하는 경우이다.
핸 들러 매핑에 있는 인터셉터들은 org.springframework.web.servlet 패키지의 HandlerInterceptor를 구현해야 한다. 이 인터페이스는 세개의 메서드를 정의하고 있다. preHandle(..)는 실제 핸들러를 실행하기 전에 호출한다. postHandle(..)는 핸들러를 실행한 후에 실행한다. afterCompletion(..)는 요청을 완전히 종료한 후에 호출한다. 이는 세 메서드들은 모든 종류의 전처리와 후처리를 할 수 있는 충분한 유연성을 제공한다.
preHandle(..) 메서드는 불리언 값을 반환한다. 실행 체인의 처리를 멈추거나 계속 진행할 때 이 메서드를 사용할 수 있다. 이 메서드가 true를 반환하면 핸들러 실행 체인이 계속 될 것이다. false를 반환하면 DispatcherServlet는 인터셉터가 직접 요청을 처리한다고 가정하고(예를 들어 적절한 뷰를 렌더링하는 등) 실행 체인의 다른 인터셉터와 실제 핸들러를 더이상 실행하지 않는다.
AbstractHandlerMapping을 확장한 모든 HandlerMapping 클래스가 있는 interceptors를 사용해서 인터셉터를 설정할 수 있다. 다음 예제에서 이를 보여준다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | Xml <beans> <bean id="handlerMapping" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"> <property name="interceptors"> <list> <ref bean="officeHoursInterceptor"/> </list> </property> </bean> <bean id="officeHoursInterceptor" class="samples.TimeBasedAccessInterceptor"> <property name="openingTime" value="9"/> <property name="closingTime" value="18"/> </bean> <beans> Java package samples; public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter { private int openingTime; private int closingTime; public void setOpeningTime(int openingTime) { this.openingTime = openingTime; } public void setClosingTime(int closingTime) { this.closingTime = closingTime; } public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Calendar cal = Calendar.getInstance(); int hour = cal.get(HOUR_OF_DAY); if (openingTime <= hour && hour < closingTime) { return true; } else { response.sendRedirect("http://host.com/outsideOfficeHours.html"); return false; } } } |
이 매핑이 처리하는 모든 요청은 TimeBasedAccessInterceptor가 가로챈다. 현재 시간이 근무시간이 아니라면 사용자를 근무시간에만 웹사이트에 접근할 수 있다는 정적 HTML 파일로 리다이렉트 한다
Note
RequestMappingHandlerMapping를 사용할 때 실제 핸들러는 호출될 특정 컨트롤러 메서드를 식별하는 HandlerMethod의 인스턴스이다.
RequestMappingHandlerMapping를 사용할 때 실제 핸들러는 호출될 특정 컨트롤러 메서드를 식별하는 HandlerMethod의 인스턴스이다.
볼 수 있듯이 스프링 아답터 클래스인 HandlerInterceptorAdapter는 HandlerInterceptor 인터페이스를 확장하기 쉽게 해준다.
Tip
위의 예제에서 어노테이션이 붙은 컨트롤러 메서드가 처리하는 모든 요청에 설정한 인터셉터를 적용할 것이다. 인터셉터 적용을 URL 경로에 따라 제한하려면 MVC 네임스페이스를 사용해서 제한할 수 있다. ???를 참고해라.
위의 예제에서 어노테이션이 붙은 컨트롤러 메서드가 처리하는 모든 요청에 설정한 인터셉터를 적용할 것이다. 인터셉터 적용을 URL 경로에 따라 제한하려면 MVC 네임스페이스를 사용해서 제한할 수 있다. ???를 참고해라.
16.5 뷰 처리
웹 어플리케이션에 대한 모든 MVC 프레임워크는 뷰를 처리하는 방법을 제공한다. 스프링은 특정 뷰 기술을 사용하지 않고도 브라우저에서 모델을 렌더링할 수 있게 하는 뷰 리졸버 (view resolver)를 제공한다. 예를 들어 스프링에서는 JSP, Velocity 템플릿, XSLT 뷰를 사용할 수 있다. 다수의 다른 뷰 기술들을 통합하고 사용하는 방법은 Chapter 17, 뷰 기술를 참고해라.
스프링이 뷰를 처리하는데 중요한 두가지 인터페이스는 ViewResolver와 View이다. ViewResolver는 뷰 이름과 실제 뷰사이에 매핑을 한다. View 인터페이스는 요청을 준비하고 뷰 기술들을 사용해서 요청을 처리한다.
16.5.1 ViewResolver 인터페이스로 뷰 처리하기
Section 16.3, “컨트롤러 구현하기”에서 얘기했듯이 스프링 웹 MVC 컨트롤러의 모든 핸들러 메서드는 명시적이나(String, View, ModelAndView를 반환하는 등) 암시적으로(관례에 기반해서) 논리적인 뷰 이름을 처리해야 한다. 스프링의 뷰는 논리적인 뷰 이름으로 처리되고 뷰 리졸버가 처리한다. 스프링에는 아주 약간의 뷰 리졸버를 가진고 있다. 다음 표에 리졸버의 대부분이 나와있고 표 뒤에 약간의 예제가 있다.
Table 16.3. 뷰 리졸버
ViewResolver | 설명 |
---|---|
AbstractCachingViewResolver | 뷰를 캐시하는 추상 뷰 리졸버. 때로는 뷰를 사용할 수 있기 전에 뷰를 준비해야 한다. 이 뷰 리졸버를 확장해서 캐싱을 제공한다. |
XmlViewResolver | 스프링의 XML 빈 팩토리와 같은 DTD를 사용하고 XML로 작성한 설정파일을 받는 ViewResolver의 구현체. 기본 설정 파일은 /WEB-INF/views.xml이다. |
ResourceBundleViewResolver | ResourceBundle 의 빈 정의를 사용하는 ViewResolver의 구현체로 번들에 기반한 이름(bundle base name)으로 지정한다. 보통 클래스 패스에 있는 프로퍼티 파일에 번들을 정의한다. 기본 파일명은 views.properties이다. |
UrlBasedViewResolver | 명시적으로 패밍을 정의하지 않고 논리적인 뷰 이름을 URL로 직접 처리하는 ViewResolver 인터페이스의 간단한 구현체. 임의의 매핑을 하지 않고 직곽적인 방법으로 논리적인 이름이 뷰 리소스의 이름과 일치할 때 적합한다. |
InternalResourceViewResolver | InternalResourceView(사 실상 서블릿과 JSP)와 JstlView와 TilesView같은 하위클래스를 지원하는 UrlBasedViewResolver의 편리한 하위클래스. setViewClass(..)를 사용해서 이 리졸버가 생성하는 모든 뷰에 대한 뷰 클래스를 지정할 수 있다. 자세한 내용은 UrlBasedViewResolver 클래스의 Javadoc을 참고해라. |
VelocityViewResolver / FreeMarkerViewResolver | VelocityView(사실상 Velocity 템플릿)이나 FreeMarkerView와 이 둘의 커스텀 하위클래스를 각각 지원하는 UrlBasedViewResolver의 편리한 하위 클래스. |
ContentNegotiatingViewResolver | 요청 파일명이나 Accept 헤더에 기반한 뷰를 처리하는 ViewResolver의 구현체. Section 16.5.4, “ContentNegotiatingViewResolver”를 참고해라. |
뷰 기술로 JSP를 사용하는 예제에 UrlBasedViewResolver를 사용할 수 있다. 이 뷰 리졸버는 뷰 이름을 URL로 변환하고 뷰를 렌더링하는 RequestDispatcher로 요청을 처리한다.
1 2 3 4 5 6 7 | Xml <bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> |
논리적인 뷰 이름으로 test를 반환할 때 이 뷰 리졸버는 /WEB-INF/jsp/test.jsp로 요청을 보낼 RequestDispatcher로 요청을 보낸다.
웹 어플리케이션에 다른 뷰 기술들을 섞어서 사용할 때는 ResourceBundleViewResolver를 사용할 수 있다.
1 2 3 4 5 6 | Xml <bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver"> <property name="basename" value="views"/> <property name="defaultParentView" value="parentView"/> </bean> |
ResourceBundleViewResolver 는 기반이 되는 이름(basename)으로 식별되는 ResourceBundle를 검사하고 처리될 각 뷰는 뷰 클래스로 [viewname].(class) 프로퍼티의 값을 사용하고 뷰 URL로 [viewname].url 프로퍼티 값을 사용한다. 예제는 뷰 기술을 다루는 다음 장에서 볼 수 있다. 여기서 보듯이 프로퍼티 파일을 “확장”한 모든 뷰에서 부모 뷰를 식별할 수 있다. 이 방법으로 기본 뷰를 지정할 수 있다. 예를 들면 다음과 같다.
Note
AbstractCachingViewResolver 의 하위클래스들은 처리하는 뷰 인스턴스를 캐싱한다. 캐싱은 뷰 기술의 성능을 향상시킨디ㅏ. cache 프로퍼티를 false로 설정해서 캐시를 끄는 것도 가능하다. 게다가 런타임에서 어떤 뷰를 갱신해야 한다면(예를 들어 Velocity 템플릿을 수정했을 때) removeFromCache(String viewName, Locale loc) 메서드를 사용할 수 있다.
AbstractCachingViewResolver 의 하위클래스들은 처리하는 뷰 인스턴스를 캐싱한다. 캐싱은 뷰 기술의 성능을 향상시킨디ㅏ. cache 프로퍼티를 false로 설정해서 캐시를 끄는 것도 가능하다. 게다가 런타임에서 어떤 뷰를 갱신해야 한다면(예를 들어 Velocity 템플릿을 수정했을 때) removeFromCache(String viewName, Locale loc) 메서드를 사용할 수 있다.
16.5.2 뷰리졸버 캐싱
스 프링은 다중 뷰 리졸버를 지원한다. 그러므로 리졸버를 체인으로 연결할 수 있다. 예를 들어 특정 상황해서 어떤 뷰를 오버라이드 할 수 있다. 필요하다면 어플리케이션 컨텍스트에 하나 이상의 리졸버를 추가하고 순서를 나타내려고 order 프로퍼티를 설정해서 뷰 리졸버를 체인으로 연결한다. 숫자가 높은 order 프로퍼티의 뷰 리졸버가 체인에서 나중에 위치한다는 것을 기억해라.
다 음 예제에서 뷰 리졸버의 체인은 두가지 리졸버로 구성되어 있는데 자동으로 항상 체인에 마지막 리졸버가 되는 InternalResourceViewResolver와 엑셀 뷰를 지정하는 XmlViewResolver이다. InternalResourceViewResolver는 엑셀 뷰를 지원하지 않는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | Xml <bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <bean id="excelViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver"> <property name="order" value="1"/> <property name="location" value="/WEB-INF/views.xml"/> </bean> <!-- views.xml에서 --> <beans> <bean name="report" class="org.springframework.example.ReportExcelView"/> </beans> |
어떤 뷰 리졸버가 뷰로 처리되지 않는다면 스프링은 다른 뷰 리졸버가 있는지 컨텍스트를 검사한다. 다른 뷰 리졸버가 존재한다면 스프링은 뷰를 처리할 때까지 뷰 리졸버를 계속해서 검사한다. 어떤 뷰리졸버도 뷰를 반환하지 않는다면 스프링은 ServletException를 던진다.
뷰 리졸버의 계약에 따라 뷰를 찾을 수 없다는 의미로 null을 반환할 수 있다. 하지만 어떤 경우에는 리졸버가 뷰가 존재하는지 존재하지 않는지 탐지할 수 없으므로 모든 뷰 리졸버가 null을 반환하는 것은 아니다. 예를 들어 InternalResourceViewResolver는 내부적으로 RequestDispatcher를 사용하고 JSP가 존재하는지 찾아내는 유일한 방법은 디스패칭을 하는 것이지만 이 동작은 딱 한번만 실행할 수 있다. VelocityViewResolver와 다른 몇몇 뷰 리졸버에서도 동일하다. 존재하지 않는 뷰를 보고하는지를 확인하려면 뷰 리졸버의 Javadoc을 확인해라. 그러므로 체인의 마지막이 아닌 다른 위치에 InternalResourceViewResolver를 두면 InternalResourceViewResolver는 항상 뷰를 반환할 것이기 때문에 체인을 완전히 검사하지 않게 된다!
16.5.3 뷰를 리다이렉트하기
앞 에서 얘기했듯이 컨트롤러는 보통 뷰 리졸버가 특정 뷰 기술로 처리하는 논리적인 뷰 이름을 반환한다. 서블릿이나 JSP 엔진으로 처리하는 JSP같은 뷰 기술에서 이 해결책은 보통 InternalResourceViewResolver와 InternalResourceView를 결합해서 내부적으로 계속 진행하거나 서블릿 API의 RequestDispatcher.forward(..) 메서드나 RequestDispatcher.include() 메서드로 처리한다. Velocity, XSLT같은 다른 뷰 기술에서는 뷰가 직접 응답 스트립에 내용을 작성한다.
때로는 뷰를 렌더링하기 전에 클라이언트에 HTTP 리다이렉트를 보내기를 원할 수 있다. 예를 들면 어떤 컨트롤러가 POST된 데이터로 호출했을 때 실제 응답은 다음 컨트롤러(예를 들면 성공한 폼 제출에서)에 위임하기를 원할 수 있다. 이러한 경우 보통의 내부적인 포워딩은 다른 컨트롤러도 같은 POST 데이터를 본다는 것을 의미하는데 기대한 다른 데이터와 혼동할 수 있다면 잠재적으로 문제의 소지가 될 수 있다. 결과를 보여주기 전에 리다이렉트를 하는 또다른 이유는 사용자가 폼 데이터를 여러번 제출할 가능성을 없애기 위함이다. 이 시나리오에서 브라우저는 최초 POST를 먼저 보낼 것이다. 그 다음 다른 URL로 리다이렉트하는 응답을 받을 것이다. 마지막으로 브라우저는 리다이렉트 응답에 있는 URL로 GET을 이어서 수행할 것이다. 그러므로 브라우저의 관점에서 현재 페이지는 POST의 결과가 아니라 GET의 결과이다. 결국 사용자가 페이지 갱신을 해서 같은 데이터를 의도치않게 다시 POST 할 수 없게 되는 효과가 있다. 갱신을 하면 최초 POST 데이터를 다시 보내는 것이 아니라 결과페이지의 GET을 다시 수행한다.
16.5.3.1 RedirectView
컨 트롤러 응답의 결과로 리다이렉트를 강제하는 한가비 방법은 컨트롤러가 스프링의 RedirectView의 인스턴스를 생성해서 반환하는 것이다. 이 경우에 DispatcherServlet이 일반적인 뷰 처리 메카니즘을 사용하지 않는다. 오히려 DispatcherServlet에는 이미 (리다이렉트)뷰가 주어졌으므로 뷰가 뷰의 작업을 하도록 지시한다.
RedirectView 는 HTTP 리다이렉트를 클라이언트 브라우저에 반환하는 HttpServletResponse.sendRedirect()를 호출한다. 기본적으로 모든 모델 속성을 리다이렉트 URL의 URI 템플릿 변수로 노출하는 것으로 간주한다. 프리미티브 타입이나 프리미티브 타입의 컬렉션/배열인 남은 속성들은 자동으로 쿼리 파라미터에 추가한다.
모델 인스턴스를 리데이렉트 전용으로 준비했다면 프리미티브 타입 속성을 쿼리 파라미터에 추가하는 것은 원하는 결과일 것이다. 하지만 어노테이션이 붙은 컨트롤러에서 모델은 렌더링을 위해 추가한 다른 속성들을(예: 드롭다운 필드값들) 담고있을 수 있다. 이러한 속성이 URL에 나타나는 것을 피하려면 어노테이션이 붙은 컨트롤러가 RedirectAttributes 타입의 인자를 선언할 수 있다. RedirectAttributes를 사용해서 RedirectView를 사용할 수 있도록 정확한 속성을 지정한다. 컨트롤러 메서드가 리다이렉트를 결정한다면 RedirectAttributes의 내용을 사용한다. 컨트롤러 메서드가 리다이렉트를 결정하지 않는다면 모델의 내용을 사용한다.
현재 요청의 URI 템플릿 변수는 자동으로 리다이렉트 URL을 확상할 때 사용할 수 있게 만들고 Model나 RedirectAttributes를 통해서 명시적으로 추가할 필요가 없다. 예를 들면 다음과 같다.
1 2 3 4 5 6 7 | Java @RequestMapping(value = "/files/{path}", method = RequestMethod.POST) public String upload(...) { // ... return "redirect:files/{path}"; } |
RedirectView 를 사용하고 컨트롤러가 직접 뷰를 생성한다면 컨트롤러에 리다이렉트 URL이 생성되는 것이 아니라 컨텍스트에 뷰 이름과 일치하게 컨텍스트에 설정되도록 컨트롤러에 리다이렉트 URL를 주입하도록 설정하기를 권장한다. 다음 섹션에서 이 과정을 설명한다.
16.5.3.2 redirect: 접두사
RedirectView 가 잘 동작 컨트롤러가 직접 RedirectView를 생성한다면 리다이렉트가 일어난다는 것을 컨트롤러가 인지한다는 것을 피할 수 있는 방법은 없다. 이는 실제로 최선의 선택이 아니고 너무 깊은 커플링이 생긴다. 컨트롤러는 응답이 어떻게 처리되는지 신경쓰지 않아야 한다. 보통 컨트롤러에 주입되는 뷰 이름만 처리해야 한다.
전용 redirect: 접두사로 이 문제를 해결할 수 있다. redirect: 접두사를 가진 뷰 이름을 반환한다면 UrlBasedViewResolver(그리고 모든 하위클래스)는 리다이렉트가 필요하다는 의미의 이 접두사를 인지할 것이다. 뷰 이름에서 나머지 부분은 리다이렉트 URL로 다룰 것이다.
최 종적인 효과는 컨트롤러가 RedirectView를 반환한 것과 같지만 이제 컨트롤러는 논리적인 뷰 이름의 관점에서만 처리할 수 있다. redirect:/myapp/some/resource같은 논리적인 뷰 이름은 현재 서블릿 컨텍스트에 상대적으로 리다이렉트 할 것이고 redirect:http://myhost.com/some/arbitrary/path같은 이름은 절대 URL로 리다이렉트 할 것이다.
16.5.3.3 forward: 접두사
뷰 이름에 전용 forward: 접두사를 사용해서 UrlBasedViewResolver와 하위클래스가 결국 처리하게 하는 것도 가능하다. 이는 뷰 이름의 남은 부분(URL이 될)을 감싸서 InternalResourceView(최종적으로 RequestDispatcher.forward()를 수행할)를 생성한다. 그러므로 이 접두사는 InternalResourceViewResolver와 InternalResourceView(예를 들면 JSP에서)에는 유용하지 않다. 하지만 주로 다른 뷰 기술을 사용한다면 이 접두사는 도움이 될 수 있지만 여전히 서블릿/JSP 엔진이 리소스의 포워딩을 처리하도록 강제하기를 원한다. (대신 여러 뷰 리졸버를 체인으로 연결할 수도 있다.)
redirect: 접두사처럼 forward: 접두사가 붙은 뷰 이름은 컨트롤러에 주입되고 컨트롤러는 응답을 처리하는 관점에서 어떤 특별한 일이 일어나는지 알지 못한다.
16.5.4 ContentNegotiatingViewResolver
ContentNegotiatingViewResolver 는 뷰를 직접 처리하지 않고 다른 뷰 리졸버에 위임한다. 화면을 생성하는 뷰를 선택하는 것은 클라이언트가 요청한다. 서버에서 클라이언트가 표현방법을 요청하는 두가지 전략이 존재한다.
- 리소스마다 다른 URI를 사용해라. 보통은 URI에 다른 파일 확장자를 사용한다. 예를 들어 URIhttp://www.example.com/users/fred.pdf는 fred 사용자의 PDF 표현을 요청하고 http://www.example.com/users/fred.xml는 XML 표현을 요청한다.
- 리소스를 얻는 클라이언트에 같은 URI를 사용하되 Accept HTTP 요청 헤더를 이해할 수 있는 미디어 타입으 로 설정해라. 예를 들어 Accept 헤더가 application/pdf인 http://www.example.com/users/fred의 HTTP 요청은 fred 사용자의 PDF 표현을 요청하고 Accept 헤더가 text/xml인 http://www.example.com/users/fred 요청은 XML 표현을 요청한다. 이 전략을 컨텐트 네고시에이션(content negotiation)라고 부른다.
Note
Accept 헤더에서 한가지 이슈는 웹브라우저에서 HTML에 설정할 수 없다는 것이다. 예를 들어 파이어폭스는 Accept헤더를 다음과 같이 바꾼다.
Accept 헤더에서 한가지 이슈는 웹브라우저에서 HTML에 설정할 수 없다는 것이다. 예를 들어 파이어폭스는 Accept헤더를 다음과 같이 바꾼다.
1 2 3 | C-like Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 |
이때문에 브라우저에 기반해서 웹 어플리케이션을 개발할 때는 각 표현마다 다른 URI를 사용하는 것이 일반적이다.
리 소스의 여러가지 표현을 지원하려고 스프링은 ContentNegotiatingViewResolver를 제공한다. ContentNegotiatingViewResolver는 파일 확장자나 HTTP 요청의 Accept 헤더에 기반해서 뷰를 처리한다. ContentNegotiatingViewResolver는 뷰를 직접 처리하지 않고 대신 ViewResolvers 빈 프로퍼티로 지정한 뷰 리졸버의 목록으로 위임한다.
ContentNegotiatingViewResolver는 각각의 ViewResolvers와 연관된 View가 지원하는 미디어타입 (Content-Type라고도 알려진)과 요청의 미디어타입을 비교해서 요청을 처리하도록 적절한 View를 선택한다. 목록에서 Content-Type과 호환성이 있는 첫번째 View가 클라이언트에 표현을(representation)을 반환한다. ViewResolver 체인에서 호환성있는 뷰가 없다면 DefaultViews 프로퍼티로 지정한 뷰 목록을 참고할 것이다. 후자의 방법은 논리적인 뷰 이름과 상관없이 현재 리소스의 적절한 표현을 렌더링할 수 있는 싱글톤 Views에 적절하다. text/*처럼 Accept는 와일드카드를 포함할 수도 있다. 이 경우에는 Content-Type이 text/xml인 뷰와 호환된다.
파일 확장자에 기반한 뷰 처리를 지원하려면 파일 확장자와 미디어 타입의 매핑을 지정하는 ContentNegotiatingViewResolver 빈 프로퍼티 mediaTypes을 사용해라. 요청 미디어타입을 결정하는데 사용된 알고리즘에 대해서 자세히 알고 싶다면 ContentNegotiatingViewResolver API 문서를 참고해라.
다음은 ContentNegotiatingViewResolver를 설정하는 예제이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | Xml <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="mediaTypes"> <map> <entry key="atom" value="application/atom+xml"/> <entry key="html" value="text/html"/> <entry key="json" value="application/json"/> </map> </property> <property name="viewResolvers"> <list> <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </list> </property> <property name="defaultViews"> <list> <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" /> </list> </property> </bean> <bean id="content" class="com.springsource.samples.rest.SampleContentAtomView"/> |
InternalResourceViewResolver 는 뷰 이름과 JSP 페이지의 변환을 다루는 반면 BeanNameViewResolver는 빈의 이름에 기반해서 뷰를 반환한다. (스프링이 뷰를 검색하고 인스턴스화하는 방법은 "ViewResolver 인터페이스로 뷰 처리하기"를 참고해라.) 이 예제에서 content 빈은 Atom RSS를 반환하는 AbstractAtomFeedView를 상속받은 클래스다. Atom 피드 표현을 생성하는 자세한 내용은 Atom 뷰 부분을 봐라.
위의 설정에서 요청이 .html 확장자라면 뷰 리졸버는 text/html 미디어 타입과 일치하는 뷰를 검색한다. InternalResourceViewResolver는 text/html와 일치하는 뷰를 제공한다. 요청이 .atom 파일 확장자라면 뷰 리졸버는 application/atom+xml 미디어타입과 일치하는 뷰를 찾는다. 이 뷰는 반환된 뷰 이름이 content라면 SampleContentAtomView와 매핑되는 BeanNameViewResolver가 제공한다. 요청의 파일 확장자가 .json이라면 뷰 이름에 상관없이 DefaultViews 목록에서 MappingJacksonJsonView 인스턴스를 선택할 것이다. 아니면 클라이언트 요청이 파일 확장자는 갖지 않고 Accept 헤더를 원하는 미디어타입으로 설정해서 같은 뷰 처리가 일어나도록 할 수 있다.
Note
뷰 리졸버의 ContentNegotiatingViewResolver 목록을 명시적으로 설정하지 않았다면 자동적으로 어플리케이션 컨텍스트에 정의한 뷰 리졸버를 사용한다.
뷰 리졸버의 ContentNegotiatingViewResolver 목록을 명시적으로 설정하지 않았다면 자동적으로 어플리케이션 컨텍스트에 정의한 뷰 리졸버를 사용한다.
http://localhost /content.atom형식의 URI나 application/atom+xml의 Accept 헤더를 가진 http://localhost/content URI에 Atom RSS 피드를 반환하는 컨트롤러 코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Java @Controller public class ContentController { private List<SampleContent> contentList = new ArrayList<SampleContent>(); @RequestMapping(value="/content", method=RequestMethod.GET) public ModelAndView getContent() { ModelAndView mav = new ModelAndView(); mav.setViewName("content"); mav.addObject("sampleContentList", contentList); return mav; } } |
16.6 플래시(flash) 속성 사용하기
플 래시(flash) 속성은 어떤 요청이 다른 요청에서 사용할 수 있도록 속성을 저장하는 방법을 제공한다. 이는 리다리렉트를 할 때(예를 들어 Post/Redirect/Get 패턴) 가장 일반적으로 필요하다. 리다이렉트한 후에 요층을 이용할 수 있도록 리다이렉트하기 전에 임시적으로 플래시 속성을 저장하고(보통은 세션에) 바로 제거한다.
스프링 MVC는 플래시 속성을 지원하는 두가지 주요 추상화를 가진다. FlashMap는 플래시 속성을 보관하는데 사용하고 FlashMapManager는 FlashMap 인스턴스를 저장하고 획득하고 관리하는데 사용한다.
플 래시 속성 지원은 항상 "켜져 있고" 사용하지 않더라도 HTTP 세션 생성을 일으키지 않으므로 명시적으로 활성화할 필요가 없다. 각 요청에는 이전의 요청(존재한다면)에서 전달받은 속성을 가진 "입력" FlashMap과 뒤이은 요청을 위해 저장할 속성을 가진 "출력" FlashMap이 있다. 두 FlashMap 인스턴스는 RequestContextUtils의 정적 메서드로 스프링 MVC 어디서나 접근할 수 있다.
어노테이션이 붙은 컨트롤러는 보통 FlashMap를 직접 사용할 필요가 없다. 대신 @RequestMapping 메서드는 RedirectAttributes 타입의 인자를 받을 수 있고 리다이렉트시에 플래시 속성을 추가하려고 RedirectAttributes를 사용할 수 있다. RedirectAttributes로 추가한 플래시 속성은 자동적으로 "출력" FlashMap으로 전달된다. 유사하게 리다이렉트 후에 "입력" FlashMap의 속성은 대상 URL을 제공하는 컨트롤러의 Model에 자동적으로 추가된다.
요청을 플래시 속성으로 매칭하기
많 은 다른 웹 프레임워크에도 플래시 속성의 개념은 존재하고 때때로 동시성 이슈가 생기는 것으로 밝혀졌다. 이는 정의대로 플래시 속성이 다음 요청까지 저장되기 때문이다. 하지만 바로 "다음" 요청이 플래시 속성을 받는 요청이 아니라 다른 비동기 요청일 수도 있다.(예를 들면 폴링이나 리로스 요청) 이러한 경우에 플래시 속성이 너무 빨리 제거된다.
이러한 이슈의 가능성을 줄이려고 RedirectView는 대상 리다이렉트 URL의 경로와 쿼리 파라미터로 FlashMap 인스턴스에 자동으로 "인증(stamps)"을 한다. 이어서 기본 FlashMapManager는 "입력" FlashMap을 검색할 때 해당 정보와 들어오는 요청을 매칭한다.
이 방법이 동시성 이슈의 가능성을 완전히 제거하지는 못하지만 리다이렉트 URL에서 이미 사용할 수 있는 정보로 크게 감소시킨다. 그러므로 리다이렉트 시나이로에서 주로 플래시 속성을 사용하기를 권장한다.
많 은 다른 웹 프레임워크에도 플래시 속성의 개념은 존재하고 때때로 동시성 이슈가 생기는 것으로 밝혀졌다. 이는 정의대로 플래시 속성이 다음 요청까지 저장되기 때문이다. 하지만 바로 "다음" 요청이 플래시 속성을 받는 요청이 아니라 다른 비동기 요청일 수도 있다.(예를 들면 폴링이나 리로스 요청) 이러한 경우에 플래시 속성이 너무 빨리 제거된다.
이러한 이슈의 가능성을 줄이려고 RedirectView는 대상 리다이렉트 URL의 경로와 쿼리 파라미터로 FlashMap 인스턴스에 자동으로 "인증(stamps)"을 한다. 이어서 기본 FlashMapManager는 "입력" FlashMap을 검색할 때 해당 정보와 들어오는 요청을 매칭한다.
이 방법이 동시성 이슈의 가능성을 완전히 제거하지는 못하지만 리다이렉트 URL에서 이미 사용할 수 있는 정보로 크게 감소시킨다. 그러므로 리다이렉트 시나이로에서 주로 플래시 속성을 사용하기를 권장한다.
16.7 URI 생성하기
스프링 MVC는 UriComponentsBuilder와 UriComponents를 사용해서 URI를 구성하고 인코딩하는 메카니즘을 제공한다.
예를 들면 URI 템플릿 문자열을 확장해서 인코딩할 수 있다.
1 2 3 4 5 6 | Java UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://example.com/hotels/{hotel}/bookings/{booking}").build(); URI uri = uriComponents.expand("42", "21").encode().toUri(); |
UriComponents는 불변이고(immutable) expand()와 encode()는 필요하다면 새로운 인스턴스를 반환는 점을 유념해라.
URI 컴포넌트를 개별적으로 사용해서 확장하고 인코딩할 수도 있다.
1 2 3 4 5 6 7 | Java UriComponents uriComponents = UriComponentsBuilder.newInstance() .scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build() .expand("42", "21") .encode(); |
서블릿 환경에서 ServletUriComponentsBuilder의 하위클래스는 서블릿 요청에서 사용가능한 URL 정보를 복사하는 정적 팩토리 메서드를 제공한다.
1 2 3 4 5 6 7 8 9 10 11 | Java HttpServletRequest request = ... // 호스트, 스키마, 포트 경로, 쿼리스트링을 재사용한다. // 쿼리파라미터 "accountId"를 치환한다 ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request).replaceQueryParam("accountId", "{id}").build() .expand("123") .encode(); |
아니면 컨텍스트 경로에서 사용가능한 정보중 일부만 카피할 수도 있다.
1 2 3 4 5 6 7 | Java // 호스트, 포트, 컨텍스트 경로를 재사용한다 // 경로에 "/accounts"를 추가한다 ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request).path("/accounts").build() |
또는 이름으로 DispatcherServlet을 매핑한 경우(예시. /main/*) 포함된 서블릿 매핑의 리터럴 부분을 가질수도 있다.
1 2 3 4 5 6 7 8 | Java // 호스트, 포트, 컨텍스트 경로를 재사용한다 // 경로에 서블릿 매핑의 리터럴 부분을 추가한다 // 경로에 "/accounts"를 추가한다 ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request).path("/accounts").build() |
16.8 로케일 사용
스 프링 아키텍처의 대부분은 스프링 웹 MVC 프레임워크와 마찬가지로 국제화 (internationalization)를 지원한다. DispatcherServlet은 자동으로 클라이언트의 로케일을 사용해서 메시지를 처리하도록 한다. 이는 LocaleResolver 객체로 이뤄진다.
요청이 들어왔을 때 DispatcherServlet는 로케일 리졸버를 찾고 로케일 리졸버를 발견한다면 로케일을 설정하는데 사용을 시도한다. RequestContext.getLocale() 메서드를 사용해서 로케일 리졸버가 처리한 로케일을 언제든지 얻을 수 있다.
자동 로케일 처리에 대해서 더 얘기하자면 특정 환경에서(예를 들면 요청의 파라미터에 따라서) 로케일을 변경하도록 핸들러 매핑(Section 16.4.1, “Intercepting requests with a HandlerInterceptor” 참고)에 인터셉터를 추가할 수도 있다.
로케일 리졸버와 인터셉터는 org.springframework.web.servlet.i18n 패키지에 정의되어 있고 일반적인 방법으로 어플리케이션 컨텍스트에 설정한다. 다음은 스프링에 포함된 로케일 리졸버들이다.
16.8.1 AcceptHeaderLocaleResolver
이 로케일 리졸버는 클라이언트(예를 들면 웹 브라우저)가 보낸 요청의 accept-language 헤더를 검사한다. 보통 이 헤더 필드는 클라이언트 운영체제의 로케일이다.
16.8.2 CookieLocaleResolver
이 로케일 리졸버는 로케일이 지정되었는 지 확인하려고 클라이언트의 Cookie를 검사한다. 로케일이 지정되었다면 지정된 로케일을 사용한다. 이 로케일 리졸버의 프로퍼티들을 사용해서 maximum age같은 쿠키의 이름을 지정할 수 있다. 아래에서 CookieLocaleResolver를 정의하는 예제를 봐라.
1 2 3 4 5 6 7 8 9 | Xml <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"> <property name="cookieName" value="clientlanguage"/> <!-- 초 단위다. -1로 설정한다면 쿠키를 유지하지 않는다.(브라우저를 닫을때 삭제한다) --> <property name="cookieMaxAge" value="100000"> </bean> |
Table 16.4. CookieLocaleResolver 프로퍼티
프로퍼티 | 기본값 | 설명 |
---|---|---|
cookieName | classname + LOCALE | 쿠키의 이름 |
cookieMaxAge | Integer.MAX_INT | 클라이언트에서 쿠키를 유지할 최대 시간 -1로 지정하면 쿠키를 유지하지 않는다. 클라이언트가 브라우저를 닫을 때까지만 사용할 수 있다 |
cookiePath | / | 사이트의 특정 부분에서만 쿠키가 보이도록 제한한다. cookiePath를 지정하면 쿠키는 해당 경로와 그 하위경로에서만 보일 것이다. |
16.8.3 SessionLocaleResolver
SessionLocaleResolver는 사용자의 요청과 연관된 세션에서 로케일을 얻을 수 있게 한다.
16.8.4 LocaleChangeInterceptor
핸 들러 매핑(Section 16.4, “핸들러 매핑” 참고)에 LocaleChangeInterceptor를 추가해서 로케일을 변경하도록 할 수 있다. 요청의 파라미터를 탐지해서 로케일을 변경할 것이다. 컨텍스트에도 존재하는 LocaleResolver의 setLocale()를 호출한다. 다음 예제는 siteLanguage라는 이름의 파라미터를 가진 *.view의 모든 리소스에 대한 요청이 로케일을 변경하는 것을 보여준다. 그래서 예를 들어 http://www.sf.net/home.view?siteLanguage=nl URL에 대한 요청은 사이트의 언어를 Dutch로 변경할 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | Xml <bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> <property name="paramName" value="siteLanguage"/> </bean> <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/> <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="interceptors"> <list> <ref bean="localeChangeInterceptor"/> </list> </property> <property name="mappings"> <value>/**/*.view=someController</value> </property> </bean> |
16.9 테마(theme) 사용
16.9.1 테마 소개
어플리케이션의 전체 디자인(look-and-feel)을 설정하는데 스프링 웹 MVC 프레임워크 테마를 적용할 수 있다. 테마는 어플리케이션의 시각적인 부분에 영향을 끼치는 정적 리소스들(보통 스타일시트와 이미지)의 모음이다.
16.9.2 테마 정의하기
웹 어플리케이션에서 테마를 사용하려면 org.springframework.ui.context.ThemeSource 인터페이스의 구현체를 설정해야 한다. WebApplicationContext 인터페이스는 ThemeSource를 확장하지만 전용 구현체에 자신의 책임을 위임한다. 기본적으로 이 위임은 클래스패스의 루트에서 프로퍼티 파일들을 로드하는 org.springframework.ui.context.support.ResourceBundleThemeSource 구현체가 될 것이다. 커스텀 ThemeSource 구현체를 사용하거나 ResourceBundleThemeSource의 기반이 되는 이름 접두사(base name prefix)를 설정하기 위해 어플리케이션 컨테스트에 예약된 이름인 themeSource로 빈을 등록할 수 있다. 웹 어플리케이션 컨텍스트는 자동으로 해당 이름의 빈을 탐지해서 사용한다.
ResourceBundleThemeSource를 사용하는 경우 테마는 간단한 프로퍼티 파일에 정의되어 있다. 프로퍼티 파일은 테마를 구성하는 리소스의 목록을 나열한다. 다음은 그 예제이다.
1 2 3 4 | C-like styleSheet=/themes/cool/style.css background=/themes/cool/img/coolBg.jpg |
프 로퍼티의 키는 뷰 코드에서 테마가 적용된 요소를 참조하기 위한 이름이다. JSP라면 spring:message 태그와 아주 유사한 spring:theme의 커스텀 태그를 보통 사용한다. 다음 JSP 코드는 디자인(look and feel)을 커스터마이징하려고 이전 예제에서 정의한 테마를 사용한다.
1 2 3 4 5 6 7 8 9 10 11 | Xml <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/> </head> <body style="background=<spring:theme code='background'/>"> ... </body> </html> |
기본적으로 ResourceBundleThemeSource는 비어있는 기반 이름 접두사(base name prefix)를 사용하므로 클래스패스의 루트에서 프로퍼티 파일을 로드한다. 그러므로 클래스패스의 루트(예를 들면 /WEB-INF/classes)에 있는 디렉토리에 cool.properties 테마 정의를 둘 것이다. ResourceBundleThemeSource는 테마에서 완전한 국제화를 지원하는 표준 자바 리소스번들 로딩 메카니즘을 사용한다. 예를 들어 Dutch 문구와 특수한 배경이미지를 참조하는 /WEB-INF/classes/cool_nl.properties를 가질 수 있다.
16.9.3 테마 리졸버
앞 의 섹션에서처럼 테마를 정의한 후에 어떤 테마를 사용할지 결정하게 된다. DispatcherServlet은 사용할 ThemeResolver 구현체를 찾으려고 themeResolver라는 이름의 빈을 찾을 것이다. 테마 리졸버는 LocaleResolver와 완전히 간은 방법으로 동작한다. 특정 요청에 사용할 테마를 탐지해서 요청의 테마를 대체할 수 있다. 다음은 스프링이 제공하는 테마 리졸버들이다.
Table 16.5. ThemeResolver 구현체
클래스 | 설명 |
---|---|
FixedThemeResolver | defaultThemeName 프로퍼티로 설정한 고정된 테마를 선택한다. |
SessionThemeResolver | 사용자의 HTTP 세션에서 테마를 유지한다. 각 세션에는 딱 한번만 설정되어야 하지만 세션간에 유지되지는 않는다. |
CookieThemeResolver | 선택한 테마를 클라이언트의 쿠키에 저장한다. |
스프링은 요청 프라미터를 가진 모든 요청에서 테마를 변경할 수 있는 ThemeChangeInterceptor도 제공한다.
16.10 스프링의 멀티파트(multipart) (파일 업로드) 지원
16.10.1 소개
스 프링의 내장 멀티파트 지원이 웹 어플리케이션의 파일 업로드를 처리한다. org.springframework.web.multipart 패키지에 정의되어 있는 MultipartResolver 객체를 추가해서 이 멀티파트 지원을 사용할 수 있다. 스프링은 Commons FileUpload나 다른 서블릿 3.0 멀티파트 요청 파싱과 함께 사용하도록 하나의 MultipartResolver 구현체를 제공한다.
일 부 개발자들이 직접 멀티파트를 처리하기를 원하기 때문에 기본적으로 스프링은 멀티파트를 처리하지 않는다. 웹 어플리케이션 컨텍스트에 멀티파트 리졸버를 추가해서 스프링의 멀티파트 처리를 활성화한다. 요청이 멀티파트를 포함하는지 확인하려고 각 용청을 검사한다. 멀티파트를 포함하고 있지 않다면 요청을 원래대로 계속 처리한다. 요청이 멀티파트를 포함하고 있다면 컨텍스트에 정의한 MultipartResolver를 사용한다. 그 다음에 요청의 멀티파트 속성을 다른 속성들처럼 다룬다.
16.10.2 MultipartResolver를 Commons FileUpload와 함께 사용하기
다음 예제는 CommonsMultipartResolver를 어떻게 사용하는지 보여준다.
1 2 3 4 5 6 7 | Xml <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 프로퍼티 중 하나를 사용할 수 있다; 최대파일 크기를 바이트로 표시한다 --> <property name="maxUploadSize" value="100000"/> </bean> |
물론 멀티파트 리졸버가 동작하도록 클래스패스에 적절한 jar를 두어야 할 수도 있다. CommonsMultipartResolver의 경우 commons-fileupload.jar를 사용해야 한다.
스 프링 DispatcherServlet가 멀티파트 요청을 탐지하면 DispatcherServlet가 컨텍스트에 정의된 리졸버를 활성화하고 요청을 전달한다. 그러면 리졸버는 현재 HttpServletRequest를 멀티파트 파일 업로드를 지원하는 MultipartHttpServletRequest로 감싼다. MultipartHttpServletRequest를 사용해서 해당 요청이 담고있는 멀티파트의 정보를 얻을 수 있고 컨트롤러에서 실제로 멀티파일 파일들에 접근할 수 있다.
16.10.3 MultipartResolver를 서블릿 3.0과 함게 사용하기
서 블릿 3.0에 기반한 멀티파트 파싱을 사용하려면 web.xml에서 "multipart-config"로 DispatcherServlet를 표시하거나 프로그래밍적인 서블릿 등록이나 서블릿 클래스에 javax.servlet.annotation.MultipartConfig 어노테이션이 붙을 수 있는 커스텀 서블릿 클래스의 경우에 javax.servlet.MultipartConfigElement로 서블릿을 표시하거나 해야 한다. 서블릿 3.0처럼 서블릿 등록 단계에서 적용해야하는 최대 크기나 저장위치같은 설정을 구성하는 것은 MultipartResolver에서 이러한 설정을 하는 것을 허용하지 않는다.
앞에서 말한 방법으로 서블릿 3.0 멀티파트 파싱을 활성화하고 나면 스프링 구성에 StandardServletMultipartResolver를 추가할 수 있다.
1 2 3 4 | Xml <bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"> </bean> |
16.10.4 폼의 파일 업로드 처리
MultipartResolver 가 자신의 일을 완료한 후에 요청은 다른 요청들처럼 처리된다. 우선 폼을 파입입력으로 생성하고 이는 사용자가 폼을 업로드할 수 있도록 할 것이다. 인코딩 속성(enctype="multipart/form-data")이 브라우저가 어떻게 폼을 멀티파트 요청처럼 인코딩해야 하는지 알도록 해준다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Xml <html> <head> <title>Upload a file please</title> </head> <body> <h1>Please upload a file</h1> <form method="post" action="/form" enctype="multipart/form-data"> <input type="text" name="name"/> <input type="file" name="file"/> <input type="submit"/> </form> </body> </html> |
다음 과정으로 파일업로드를 처리하는 컨트롤러를 생성한다. 메서드 파라미터에 MultipartHttpServletRequest나 MultipartFile를 사용한다는 점만 제외하면 이 컨트롤러는 @Controller 어노테이션이 붙은 일반적인 컨트롤러와와 아주 유사하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | Java @Controller public class FileUploadController { @RequestMapping(value = "/form", method = RequestMethod.POST) public String handleFormUpload(@RequestParam("name") String name, @RequestParam("file") MultipartFile file) { if (!file.isEmpty()) { byte[] bytes = file.getBytes(); // 어딘가에 바이트를 저장한다 return "redirect:uploadSuccess"; } else { return "redirect:uploadFailure"; } } } |
@RequestParam 메서드 파라미터가 폼에 선언된 입력 요소에 어떻게 매핑되는지 봐라. 이 예제에서 byte[]로 하는 것은 아무것도 없지만 실제 사용할 때는 byte[]를 데이터베이스이나 파일시스템 등에 저장할 수 있다.
서블릿 3.0 멀티파트 파싱을 사용하는 경우 메서드 파라미터에 javax.servlet.http.Part를 사용할 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Java @Controller public class FileUploadController { @RequestMapping(value = "/form", method = RequestMethod.POST) public String handleFormUpload(@RequestParam("name") String name, @RequestParam("file") Part file) { InputStream inputStream = file.getInputStream(); // 업로드된 파일의 바이트를 어딘가에 저장한다 return "redirect:uploadSuccess"; } } |
16.10.5 프로그래밍적인 클라이언트의 파일업로드 요청 처리
RESTful 서비스 시나리오에서는 브라우저가 아닌 클라이언트가 멀티파트 요청을 제출할 수도 있다. 앞의 모든 예제와 설정은 여기서도 마찬가지로 적용된다. 하지만 보통 파일와 간단한 폼필드를 제출하는 브라우저와는 다르게 프로그래밍적인 클라이언트는 특정 컨텐트 타입의 훨씬 복잡한 데이터를 보낼 수 있다. 예를 들면 파일과 함께 두번째 부분으로 JSON 포맷의 데이터를 가진 멀티파트 요청 등이 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | C-like POST /someUrl Content-Type: multipart/mixed --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="meta-data" Content-Type: application/json; charset=UTF-8 Content-Transfer-Encoding: 8bit { "name": "value" } --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="file-data"; filename="file.properties" Content-Type: text/xml Content-Transfer-Encoding: 8bit ... File Data ... |
@RequestParam("meta-data") String metadata 컨트롤러 메서드 인자로 "meta-data"라는 이름을 가진 부분에 접근할 수 있다. 하지만 @RequestBody이 HttpMessageConverter의 도움을 받아 멀티파트가 아닌 요청의 바디를 대상 객체로 변환하는 것과 아주 유사하게 요청 바디에서 JSON 포맷의 데이터에서 초기화된 강타입의 객체를 받는 것을 선호할 것이다.
이 용도로 @RequestParam 대신 @RequestPart 어노테이션을 사용할 수 있다. @RequestPart는 멀티파트의 'Content-Type' 헤더를 기억하도록 HttpMessageConverter로 전달된 멀티파트의 내용을 가질 수 있게 한다.
1 2 3 4 5 6 7 | Java @RequestMapping(value="/someUrl", method = RequestMethod.POST) public String onSubmit(@RequestPart("meta-data") MetaData metadata, @RequestPart("file-data") MultipartFile file) { // ... } |
상호교환가능하게 @RequestParam나 @RequestPart로 MultipartFile 메서드 인자에 어떻게 접근할 수 있는지를 봐라. 하지만 이 경우에 @RequestPart("meta-data") MetaData 메서드 인자는 'Content-Type' 헤더에 기반해서 JSON 컨텐츠로 읽어서 MappingJacksonHttpMessageConverter로 변환한다.
댓글 없음:
댓글 쓰기