[Info]Tags categorized posts and contents patterns..

[AJAX] Ajax Code E xamples.. [Book] About the book.. [CSS] CSS Code E xamples.. [DB] Sql Code E xamples.. [DEV] All development stor...

2016년 7월 8일 금요일

[JAVA] 16장 웹 MVC 프레임워크 #3..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

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

16.11 예외 처리
16.11.1 HandlerExceptionResolver
스프링의 HandlerExceptionResolver 구현체가 컨트롤러가 실행되는 중 발생한 의도치 않은 예외를 다룬다. HandlerExceptionResolver는 웹 어플리케이션 디스크립터인 web.xml에 정의할 수 있는 예외 매핑과 꽤 유사하다. 하지만 HandlerExceptionResolver가 더 유연한 방법을 제공한다. 예를 들어 HandlerExceptionResolver는 예외가 던져졌을 때 어떤 핸들러를 실행할 것인지에 대한 정보를 제공한다. 게다가 예외 처리의 프로그래밍적인 방법은 요청을 다른 URL로 포워딩하기 전에(서블릿에 특화된 예외매핑을 사용할 때와 같은 결과이다.) 적절하게 응답할 수 있는 더 많은 옵션을 제공한다.

resolveException(Exception, Handler) 메서드를 구현하는 것과 ModelAndView를 반환하는 것과만 관련된 HandlerExceptionResolver 인터페이스를 구현하는 데 추가적으로 SimpleMappingExceptionResolver를 사용할 수도 있다. 이 리졸버는 던져진 모든 예외의 클래스명을 받아서 뷰 이름에 매핑할 수 있게 해준다. 이는 기능적으로는 서블릿 API의 예외 매핑기능과 동일하지만 여러 핸들러로 더욱 세밀한 예외 매핑을 구현할 수도 있게 한다.

기본적으로 DispatcherServlet이 DefaultHandlerExceptionResolver를 등록한다. 이 리졸버는 특정 응답 상태코드를 설정해서 해당 표준 스프링 MVC 예외를 처리한다.

예외HTTP 상태 코드
ConversionNotSupportedException500 (Internal Server Error)
HttpMediaTypeNotAcceptableException406 (Not Acceptable)
HttpMediaTypeNotSupportedException415 (Unsupported Media Type)
HttpMessageNotReadableException400 (Bad Request)
HttpMessageNotWritableException500 (Internal Server Error)
HttpRequestMethodNotSupportedException405 (Method Not Allowed)
MissingServletRequestParameterException400 (Bad Request)
NoSuchRequestHandlingMethodException404 (Not Found)
TypeMismatchException400 (Bad Request)









16.11.2 @ExceptionHandler
HandlerExceptionResolver 인터페이스의 대안은 @ExceptionHandler 어노테이션이다. 컨트롤러 메서드가 실행되는 동안 특정 타입의 예외가 던져졌을 때 어떤 메서드를 호출할 것인지 지정하려고 컨트롤러내에서 @ExceptionHandler 메서드 어노테이션을 사용한다. 예를 들면 다음과 같이 사용한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Java

@Controller
public class SimpleController {

  // 다른 컨트롤러 메서드는 생략한다

  @ExceptionHandler(IOException.class)
  public String handleIOException(IOException ex, HttpServletRequest request) {
    return ClassUtils.getShortName(ex.getClass());
  }
}

여기서는 java.io.IOException가 던져졌을 때 'handlerIOException' 메서드를 호출할 것이다.

@ExceptionHandler 값을 예외 타입의 배열로 설정할 수도 있다. 목록에 있는 타입의 예외가 던져지면 해당 @ExceptionHandler 어노테이션이 붙은 메서드가 호출될 것이다. 어노테이션 값을 설정하지 않으면 메서드 인자로 나열한 예외 타입을 사용한다.

@RequestMapping 어노테이션이 붙은 표준 컨트롤러 메서드와 아주 비슷하게 @ExceptionHandler 메서드의 메서드 인자와 반환값은 아주 유연하다. 예를 들어 서블릿 환경에서 HttpServletRequest에 접근할 수 있고 포틀릿 환경에서 PortletRequest에 접근할 수 있다. 반환값은 뷰 이름이나 ModelAndView 객체로 해석되는 String이 될 수 있다. 더 자세한 내용은 API 문서를 참고해라.

16.12 설정보다는 관례(Convention over configuration)에 대한 지원
많은 프로젝트에서 수립된 관례를 따르고 수긍할만한 기본값을 갖는 것이 프로젝트가 필요로 하는 것이고 스프링 웹 MVC는 이제 설정보다는 관례(convention over configuration)를 명시적으로 지원한다. 즉, 작명 관례 같은 것을 수립하면 핸들러 매핑, 뷰 리졸버, ModelAndView 인스턴스 등을 설정하는데 필요한 설정의 상당부분을 제거할 수 있다. 이는 빠른 프로토타이핑과 관련에서 아주 좋은 것이고 프로덕션에도 가져가야 할 코드 일관성(항상 좋은 것이다)을 유지할 수도 있다. 설정보다 관례의 지원은 MVC의 세가지 핵심 영역인 모델, 뷰, 컨트롤러를 처리한다.

16.12.1 ControllerClassNameHandlerMapping 컨트롤러
ControllerClassNameHandlerMapping 클래스는 요청 URL과 이러한 요청을 처리하는 ControllerClassNameHandlerMapping 인스턴스간의 매핑을 결정하는데 관례를 사용하는 HandlerMapping 구현체이다.

다음의 간단한 Controller 구현체를 보자. 특히 클래스의 이름을 주의깊게 봐라.


1
2
3
4
5
6
7
8
Java

public class ViewShoppingCartController implements Controller {

  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
    // 이 예제에서는 구현체가 크게 중요하지 않다...
  }
}

다음은 이에 대응하는 스프링 웹 MVC 설정파일의 일부이다.

1
2
3
4
5
6
7
Xml

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

<bean id="viewShoppingCart" class="x.y.z.ViewShoppingCartController">
  <!-- 필요한 의존성을 주입한다... -->
</bean>

ControllerClassNameHandlerMapping 는 어플리케이션 컨텍스트에 정의된 다양한 핸들러(또는 Controller) 빈을 모두 찾아내서 핸들러 매핑을 정의하려고 이름에서 Controller를 제거한다. 그러므로 ViewShoppingCartController는 /viewshoppingcart* 요청 URL에 매핑된다.

핵심 아이디어에 빨리 익숙해지도록 예제를 좀 더 보자. (카멜케이스의 Controller 클래스명과는 대조적으로 URL에서는 모두 소문자이다.)

  • WelcomeController는 /welcome* 요청 URL에 매핑된다
  • HomeController는 /home* 요청 URL에 매핑된다
  • IndexController는 /index* 요청 URL에 매핑된다
  • RegisterController는 /register* 요청 URL에 매핑된다
MultiActionController 핸들러 클래스의 경우 생성되는 매핑이 약간 더 복잡하다. 다음 예제에서 Controller 이름은 MultiActionController 구현체가 된다고 가정한다.
  • AdminController는 /admin/* 요청 URL에 매핑된다
  • CatalogController는 /catalog/* 요청 URL에 매핑된다
컨트롤러 구현체를 작성할 때 xxxController 같은 작명 관례를 따른다면 ControllerClassNameHandlerMapping이 엄청 길어질 수 있는 SimpleUrlHandlerMapping(또는 비슷한)을 정의하고 유지하는 지루한 작업에서 구해줄 것이다.

ControllerClassNameHandlerMapping 클래스가 AbstractHandlerMapping 기반 클래스를 확장하므로 HandlerInterceptor 인스턴스와 다른 다수의 HandlerMapping 구현체에서 하는 모든 것을 정의할 수 있다.

16.12.2 모델 ModelMap (ModelAndView)
ModelMap 클래스는 일반적인 작명 관례를 따르는 View에 나타날 추가적인 객체를 만들수 있는 기본적으로 중요한 Map이다. 다음의 Controller 구현체를 보자. 관련된 어떤 이름도 지정하지 않고도 이 객체는 ModelAndView에 추가된다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Java

public class DisplayShoppingCartController implements Controller {

  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {

    List cartItems = // CartItem 객체의 List를 가져온다
    User user = // 쇼핑을 하는 User를 가져온다

    ModelAndView mav = new ModelAndView("displayShoppingCart"); <-- 논리적인  이름

    mav.addObject(cartItems); <--  봐라. 이름이 없고 그냥 객체다
    mav.addObject(user); <-- 여기서도 마찬가지다!

    return mav;
  }
}

ModelAndView 클래스는 ModelMap 클래스를 사용한다. ModelMap은 객체가 맵에 추가되었을 때 객체에 대한 키를 자동으로 생성하는 커스텀 Map 구현체이다. 추가된 객체( User같은 스칼라(scalar) 객체의 경우)에 대한 이름을 결정하는 전력은 객체의 클래스의 짧은 클래스명을 사용하는 것이다. 다음 예제는 ModelMap 인스턴스에 넣은 스칼라 객체에 대해 생성된 이름이다.
  • 추가한 x.y.User 인스턴스에는 user라는 이름이 생성될 것이다.
  • 추가한 x.y.Registration 인스턴스에는 registration이라는 이름이 생성될 것이다.
  • 추가한 x.y.Foo 인스턴스에는 foo라는 이름이 생성될 것이다.
  • 추가한 java.util.HashMap 인스턴스에는 hashMap이라는 이름이 생성될 것이다. hashMap는 직관적이지 않으므로 이 경우에는 이름을 명시하기를 바랄 것이다.
  • null을 추가하면 IllegalArgumentException가 던져질 것이다. 추가하는 객체가 null이 될 수 있다면 이름을 명시하기를 원할 것이다.
뭐? 자동으로 복수형을 만들지 않는다고?
스프링 웹 MVC의 설정보다 관례 지원은 자동 복수형을 지원하지 않는다. 즉, Person객체의 List를 ModelAndView에 추가할 수 없고 생성된 이름이 people이 되지 않는다.

약간의 토론 끝에 “최소 놀람의 법칙”이 이기는 것으로 끝나서 이렇게 결정되었다.

Set 나 List를 추가한 후에 이름을 생성하는 전략은 컬랙션 내부를 들여다보고 컬렉션의 첫번재 객체의 짧은 클래스 이름을 가져와서 이름에 List를 추가해서 사용하는 것이다. 배열은 배열의 내용을 들여다 필요는 없지만 배열에도 동일하게 적용된다. 다음은 컬렉션의 이름 생성 의미를 더 명확하게 해 줄 예제들이다.

  • 0개 이상의 x.y.User 요소로 이루어진 x.y.User[] 배열은 userList라는 이름이 생성될 것이다.
  • 0개 이상의 x.y.User 요소로 이루어진 x.y.Foo[] 배열은 fooList라는 이름이 생성될 것이다.
  • 하나 이상의 x.y.User 요소로 이루어진 java.util.ArrayList은 userList라는 이름이 생성될 것이다.
  • 하나 이상의 x.y.Foo 요소로 이루어진 java.util.HashSet는 fooList라는 이름이 생성될 것이다.
  • 비어 있는 java.util.ArrayList는 전혀 추가되지 않을 것이다. (사실 addObject(..) 호출은 본질적으로 조작할 수 없게(no-op) 될 것이다.)

16.12.3 뷰 - RequestToViewNameTranslator
논 리적인 뷰 이름을 명시적으로 제공하지 않은 경우 RequestToViewNameTranslator 인터페이스가 논리적인 뷰 이름을 결정한다. 딱 하나의 구현체인 DefaultRequestToViewNameTranslator 클래스가 있다.

DefaultRequestToViewNameTranslator는 이 예제처럼 요청 URL을 논리적인 뷰 이름으로 매핑한다.


 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
Java

public class RegistrationController implements Controller {

  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
    // 요청을 처리한다...
    ModelAndView mav = new ModelAndView();
    // 필요에 따라 모델에 데이터를 추가한다...
    return mav;
    // View가 없거나 논리적인 뷰 이름이 설정되었다
  }
}

Xml

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

  <!--  알려진 이름의  빈이 우리를 위해서  이름을 생성한다 -->
  <bean id="viewNameTranslator" class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator"/>

  <bean class="x.y.RegistrationController">
    <!-- 필요에 따라 의존성을 주입한다 -->
  </bean>

  <!-- 요청 URL을 컨트롤러 이름에 매핑한다 -->
  <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

  <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
  </bean>
</beans>

handleRequest(..) 메서드의 구현부에서 반환되는 ModelAndView에 어떻게 뷰가 없거나 논리적인 뷰가 설정되는 지를 봐라. DefaultRequestToViewNameTranslator는 요청 URL에서 논리적인 뷰 이름을 생성하는 작업을 한다. 앞의 RegistrationController의 경우 (ControllerClassNameHandlerMapping와 결합해서 사용하는) 에는 http://localhost/registration.html 요청 URL에서 DefaultRequestToViewNameTranslator가 registration라는 논리적인 뷰 이름을 생성하게 된다. 그 다음에 InternalResourceViewResolver가 이 논리적인 뷰 이름을 /WEB-INF/jsp/registration.jsp 뷰로 처리한다.


Tip
DefaultRequestToViewNameTranslator 빈을 명시적으로 정의할 필요는 없다. DefaultRequestToViewNameTranslator의 기본 설정으로 좋다면 스프링 웹 MVC DispatcherServlet이 이 클래스를 인스턴스화하도록 할 수 있다.(명시적으로 설정하지 않은 경우)

물론 기본 설정을 변경해야 한다면 자신만의 DefaultRequestToViewNameTranslator 빈을 명시적으로 설정해야 한다. 설정할 수 있는 다영한 프로퍼티에 대한 자세한 내용은 DefaultRequestToViewNameTranslator 클래스의 광범위한 Javadoc을 참고해라.

16.13 ETag 지원
ETag(엔 티티 태그)는 HTTP/1.1 호환 웹서버가 반환하는 HTTP 응답헤더로 해당 URL의 내용의 변경사항이 있는지 결정하는데 사용한다. ETag는 Last-Modified 헤더보다 더 정교한 후계자정도로 생각할 수 있다. 서버가 ETag 헤더와 함께 응답을 반환했을 때 클라이언트는 이어진 GET 요청에서(If-None-Match헤더에서) 이 헤더를 사용할 수 있다. 내용이 변경되지 않았다면 서버는 304: Not Modified를 반환한다.

서블릿 필터 ShallowEtagHeaderFilter가 ETag 지원을 제공한다. ShallowEtagHeaderFilter는 평범한 서블릿 필터이므로 어떤 웹프레임워크와도 조합해서 사용할 수 있다. ShallowEtagHeaderFilter 필터는 얕은(shallow) ETag(깊은 ETag와는 반대로 자세한 것은 나중에 설명한다.)라는 것을 생성한다. ShallowEtagHeaderFilter 필터는 렌더링된 JSP의 내용을 캐싱하고 그 내용으로 MD5 해시를 만들어서 응답에 ETag 헤더로 반환한다. 다음에 클라이언트가 같은 리소스를 요청할 때 If-None-Match 값으로 이 해시를 사용한다. ShallowEtagHeaderFilter 필터는 이를 감지하고 뷰를 다시 렌더링해서 두 해시를 비교한다. 이 두 해시가 같다면 304를 반환한다. 이 필터는 뷰를 계속 렌더링하므로 처리 비용이 줄어들지 않는 것이다. 렌더링한 응답을 네트워크로 다시 보내지 않으므로 여기서 줄어드는 것은 대역폭(bandwidth)뿐이다.

web.xml에 ShallowEtagHeaderFilter를 설정한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Xml

<filter>
  <filter-name>etagFilter</filter-name>
    <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>etagFilter</filter-name>
  <servlet-name>petclinic</servlet-name>
</filter-mapping>

16.14 스프링 MVC 설정하기
Section 16.2.1, “WebApplicationContext의 전용 빈 타입”와 Section 16.2.2, “기본 DispatcherServlet 설정”에서 스프링 MVC의 전용 빈과 DispatcherServlet이 사용하는 기본 구현체에 대해서 설명했다. 이번 섹션에서는 스프링 MVC를 설정하는 추가적인 두가지 방법을 배울 것이다. 다시 얘기하자만 MVC Java config와 MVC XML 네임스페이스이다.

MVC Java config와 MVC 네임스페이스는 DispatcherServlet 기본값을 덮어쓰는 유사한 기본 설정을 제공한다. 목표는 대부분의 어플리케이션이 같은 설정을 생성하는 것을 줄이고 스프링 MVC가 간단한 시작점이 되고 사용하는 설정에 대한 사전지식이 전혀 없거나 약간만 필요하도록 설정을 고수준으로 생성하는 것이다.

자신의 선호에 따라 MVC Java config나 MVC 네인스페이스 중에서 선택할 수 있다. MVC Java config로 생성된 스프링 MVC 빈을 직접 세밀하게 커스터마이징하는 것 뿐만 아니라 사용하는 설정을 보기 쉽다는 등의 더 자세한 내용은 아래에서 볼 것이다. 하지만 처음부터 시작해 보자.

16.14.1 MVC Java Config나 MVC XML 네임스페이스 활성화하기
MVC Java config를 활성화 하려면 @Configuration 클래스중 하나에 @EnableWebMvc 어노테이션을 추가해라.


1
2
3
4
5
6
Java

@EnableWebMvc
@Configuration
public class WebConfig {
}

동일한 내용을 XML로 설정하려면 mvc:annotation-driven 요소를 사용해라.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Xml

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

  <mvc:annotation-driven />
<beans>

위의 코드는 @RequestMapping , @ExceptionHandler 등과 같은 어노테이션을 사용해서 어노테이션이 붙은 컨트롤러 메서드로 요청을 처리하는 지원에 RequestMappingHandlerMapping, RequestMappingHandlerAdapter, ExceptionHandlerExceptionResolver (among others)를 등록한다.

이는 다음을 활성화 한다.

  1. 데이터 바인딩에 사용한 JavaBeans PropertyEditors에 추가적으로 ConversionService 인스턴스를 통한 스프링 3 방식의 타입 변환.
  2. ConversionService로 @NumberFormat 어노테이션을 사용해서 숫자 필드의 포매팅을 지원한다
  3. 클래스패스에 Joda Time 1.3 이상의 버전이 있다면 @DateTimeFormat 어노테이션을 사용해서 Date, Calendar, Long, Joda Time 필드의 포매팅을 지원한다.
  4. 클래스패스에 JSR-303 프로바이더가 있으면 @Valid로 @Controller 입력의 유효성검사를 지원한다.
  5. HttpMessageConverter가 @RequestMapping나 @ExceptionHandler 메서드에서 @RequestBody 메서드 파라미터와 @ResponseBody 메서드 반환값을 지원한다.
다음은 mvc:annotation-driven로 설정되는 HttpMessageConverter의 전체 목록이다.
  • ByteArrayHttpMessageConverter는 바이트 배열을 변환한다.
  • StringHttpMessageConverter는 문자열을 변환한다.
  • ResourceHttpMessageConverter는 모든 미디어 타입의 org.springframework.core.io.Resource를 변환한다.
  • SourceHttpMessageConverter는 javax.xml.transform.Source를 변환한다.
  • FormHttpMessageConverter는 폼 데이터를 MultiValueMap<String, String>로 변환하거나 그 반대로 변환한다.
  • Jaxb2RootElementHttpMessageConverter는 자바 객체를 XML로(혹은 그 반대로) 변환한다. (클래스패스에 JAXB2가 있는 경우 추가된다.)
  • MappingJacksonHttpMessageConverter는 JSON을 변환한다.(클래스패스에 Jackson이 있는 경우 추가된다.)
  • AtomFeedHttpMessageConverter는 Atom 피드를 변환한다. (클래스패스에 Rome이 있는 경우 추가된다.)
  • RssChannelHttpMessageConverter는 RSS 피드를 변환한다. (클래스패스에 Rome이 있는 경우 추가된다.)

16.14.2 제공된 설정의 커스터마이징
자바에서 기본 설정을 커스터마이징하려면 그냥 WebMvcConfigurer 인터페이스를 구현하거나 WebMvcConfigurerAdapter 클래스를 상속받아서 필요한 메서드를 오버라이드(이쪽 방법을 더 선호할 것이다.)한다. 다음은 오버라이드할 수 있는 메서드의 예시이다. 전체 메서드의 목록은 WebMvcConifgurer를 보고 자세한 내용은 Javadoc를 봐라.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  protected void addFormatters(FormatterRegistry registry) {
    // 포매터나 컨버터를 추가한다
  }

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    // 사용할 HttpMessageConverter의 목록을 설정한다
  }
}

<mvc:annotation-driven />의 기본 설정을 커스터마이징하려면 어떤 속성과 하위요소를 지원하는지 확인해 봐라. 사용할 수 있는 속성과 하위요소를 찾으려면 Spring MVC XML 스키마를 보거나 IDE에서 코드 자동완성 기능을 사용할 수 있다. 아래 예제에서 사용할 수 있는 것들중 일부를 보여주고 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Xml

<mvc:annotation-driven conversion-service="conversionService">
  <mvc:message-converters>
    <bean class="org.example.MyHttpMessageConverter"/>
    <bean class="org.example.MyOtherHttpMessageConverter"/>
  </mvc:message-converters>
</mvc:annotation-driven>

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
  <property name="formatters">
    <list>
      <bean class="org.example.MyFormatter"/>
      <bean class="org.example.MyOtherFormatter"/>
    </list>
  </property>
</bean>

16.14.3 인터셉터 설정
HandlerInterceptors나 WebRequestInterceptors를 모든 요청이나 특정 URL 경로패턴에 한정해서 적용되도록 설정할 수 있다.

자바로 인터셉터를 등록하는 예제:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LocalInterceptor());
    registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
  }
}

<mvc:interceptors> 요소를 사용해서 XML로 등록하는 예제:

1
2
3
4
5
6
7
8
9
Xml

<mvc:interceptors>
  <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
  <mvc:interceptor>
    <mapping path="/secure/*"/>
    <bean class="org.example.SecurityInterceptor" />
  </mvc:interceptor>
</mvc:interceptors>

16.14.4 뷰 컨트롤러 설정
이는 호출하면 바로 뷰로 포워딩되는 ParameterizableViewController를 정의하는 숏컷이다. 뷰가 응답을 생성하기 전에 실행할 자바 컨트롤러 로직이 없는 정적인 경우에 이를 사용한다.

자바로 "/"에 대한 요청을 "home"라는 뷰로 보내는 예제:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("home");
  }
}

<mvc:view-controller> 요소를 사용해서 XML로 설정하는 예제:

1
2
3
Xml

<mvc:view-controller path="/" view-name="home"/>

16.14.5 리소스 제공(Serving) 설정
이 옵션은 ResourceHttpRequestHandler가 Resource 위치에 있는 목록에서 제공하도록 특정 URL 패턴을 따르는 정적 리소스 요청을 허용한다. 이는 클래스 패스의 경로를 포함해서 웹 어플리케이션 루트가 아닌 다른 위치에서 정적 리소스를 제공하는 편리한 방법을 제공한다. 만료시간 헤더(Page Speed나 YSlow같은 최적화 도구는 1년을 권장한다.)를 설정하는데 cache-period 프로퍼티를 사용할 것이므로 클라이언트가 더 효율적으로 사용할 수 있을 것이다. 핸들러가 Last-Modified 헤더도 적절히 평가할 것이므로(존재한다면) 304 상태코드를 알맞게 반환해서 클라이언트가 이미 캐싱항 리소스에 대한 불필요한 오버헤드를 줄일 것이다. 예를 들어 /resources/** URL 패턴으로 웹 어플리케이션 내의 public-resources 디렉토리에서 리소스를 제공하려면 다음과 같이 사용할 것이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**").addResourceLocations("/public-resources/");
  }
}

XML로는 다음과 같이 사용한다.

1
2
3
Xml

<mvc:resources mapping="/resources/**" location="/public-resources/"/>

브라우저 캐시가 사용할 최대 기간을 보장하고 브라우저의 HTTP 요청을 줄이려고 1년 만료 헤더와 함께 이러한 리소스를 제공하려면 다음과 같이 설정한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**").addResourceLocations("/public-resources/").setCachePeriod(31556926);
  }
}

XML로는 다음과 같이 설정한다.

1
2
3
Xml

<mvc:resources mapping="/resources/**" location="/public-resources/" cache-period="31556926"/>

mapping 속성은 SimpleUrlHandlerMapping가 사용할 수 있는 Ant 패턴이어야 하고 location 속성은 하나이상의 리소스 디렉토리 위치를 지정해야 한다. 목록을 콤마로 구분해서 여러 리소스 위치를 지정할 수도 있다. 해당 요청의 리소스가 존재하는지 여부를 지정한 순서대로 지정된 위치를 확인한다. 예를 들어 웹 어플리케이션 루트와 클래스 패스에 어떤 jar에서라도 알려진 /META-INF/public-web-resources/ 경로 모두에서 리소스를 제공하려면 다음과 같이 설정한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
      .addResourceLocations("/", "classpath:/META-INF/public-web-resources/");
  }
}

XML에서는 다음과 같이 설정한다.

1
2
3
Xml

<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/public-web-resources/"/>

새로운 버전의 어플리케이션을 배포했을 때 변경될 수도 있는 리소스를 제공하는 경우 클라이언트가 새로운 버전으로 배포된 어플리케이션의 리소스를 강제로 요청하도록 리소스 요청에 사용하는 매핑 패턴에 버전 문자열을 포함시키는 것을 권장한다. 이러한 버전 문자열은 파라미터화될 수 있고 SpEL로 접근할 수 있으므로 새로운 버전을 배포할 때 단일 지점에서 쉽게 관리할 수 있다.

예제처럼 프로덕션에서 Dojo 자바스크립트 라이브러리의 성능 최적화가 된 커스텀 빌드버전 (권장사항대로)을 사용하는 어플리케이션을 생각해 보자. 그리고 이 빌드는 일반적으로 웹 어플리케이션내에서 /public-resources/dojo/dojo.js 경로에 배포한다. Dojo의 다른 부분들이 어플리케이션의 새로운 각 버전에 대한 커스컴 빌드에 포함될 것이므로 클라이언트 웹 브라우저가 새로운 버전의 어플리케이션이 배포될 때마다 커스텀 빌드 dojo.js 리소스를 강제대로 새로 다운로드 하도록 해야 한다. 다음과 같이 어플리케이션의 버전을 프로퍼티 파일에서 관리하는 것이 이를 위한 가장 간단한 방법이다.


1
2
3
Java

application.version=1.0.0

그 다음 프로퍼티 파일의 값을 util:properties 태그를 사용하는 빈처럼 SpEL에서 접근가능하도록 한다.

1
2
3
Xml

<util:properties id="applicationProps" location="/WEB-INF/spring/application.properties"/>

이제 SpEL로 접근가능한 어플리케이션 버전을 resources 태그를 사용할때 포함시킬 수 있다.

1
2
3
Xml

<mvc:resources mapping="/resources-#{applicationProps['application.version']}/**" location="/public-resources/"/>

자바에서는 @PropertySouce 어노테이션을 사용해서 정의된 모든 프로퍼티에 접근하도록 Environment 추상화를 주입할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Java

@EnableWebMvc
@Configuration
@PropertySource("/WEB-INF/spring/application.properties")
public class WebConfig extends WebMvcConfigurerAdapter {

  @Inject Environment env;

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources-" + env.getProperty("application.version") + "/**")
      .addResourceLocations("/public-resources/");
  }
}

마지막으로 적절한 URL로 리소스를 요청하기 위해 스프링의 JSP 태그의 장점을 취할 수 있다.

1
2
3
4
5
6
7
8
9
Xml

<spring:eval expression="@applicationProps['application.version']" var="applicationVersion"/>

<spring:url value="/resources-{applicationVersion}" var="resourceUrl">
  <spring:param name="applicationVersion" value="${applicationVersion}"/>
</spring:url>

<script src="${resourceUrl}/dojo/dojo.js" type="text/javascript"> </script>

16.14.6 mvc:default-servlet-handler
이 태그는 여젼히 컨테이너의 기본 서블릿이 정적 리소스 요청을 처리하도록 하면서 DispatcherServlet을 "/"에 매핑할 수 있게 한다.(그래서 컨테이너의 기본 서블릿의 매핑을 오버라이딩한다) "/**" URL 매핑으로 DefaultServletHttpRequestHandler를 설정하고 다른 URL 매핑에 상대적으로 낮은 우선순위를 설정한다.

이 핸들러는 모든 요청을 기본 서블릿으로 보낼 것이다. 그러므로 다른 모든 URL HandlerMappings의 순서에서 마지막에 있게 하는 것이 중요하다. <mvc:annotation-driven>을 사용하거나 아니면 자신만의 커스터마이징된 HandlerMapping 인스턴스를 설정한 경우에 DefaultServletHttpRequestHandler의 order 프로퍼티 값(Integer.MAX_VALUE)보다 낮은 값으로 설정해야 한다.

기본 설정을 사용해서 이 기능을 활성화 하려면 다음과 같이 한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
  }
}

XML에서는 다음과 같이 설정한다.

1
2
3
Xml

<mvc:default-servlet-handler/>

"/" 서블릿 매핑을 오버라이딩할 때의 주의할 점은 기본 서블릿의 RequestDispatcher를 경로가 아니라 이름으로 획득해야 한다는 것이다. DefaultServletHttpRequestHandler는 시작할 때 대부분의 주요 서블릿 컨테이너(Tomcat, Jetty, Glassfish, JBoss, Resin, WebLogic, WebSphere를 포함해서)의 알려진 이름 목록을 사용해서 컨테이너의 기본 서블릿을 자동으로 탐지하려고 시도할 것이다. 다른 이름으로 기본 서블릿을 커스텀해서 설정했거나 기본 서블릿 이름을 알지 못하는 다른 서블릿 컨테이너를 사용했다면 기본 서블릿의 이름을 다음 예제에서 처럼 명시적으로 제공해야 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable("myCustomDefaultServlet");
  }
}

XML에서는 다음과 같이 설정한다.

1
2
3
Xml

<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>

16.14.7 부가적인 Spring Web MVC 자료
스프링 웹 MVC에 대한 자세한 내용은 다음의 링크와 문서를 봐라.

  • 스프링 MVC로 웹어플리케이션을 어떻게 만드는지에 대한 뛰어난 글과 튜토리얼이 많이 있다. Spring 문서 페이지에서 읽어봐라.
  • Seth Ladd 등이 쓴 “Expert Spring Web MVC and Web Flow” (Apress 출판)는 스프링 웹 MVC의 좋은 점에 대한 뛰어난 책이다.

16.14.8 MVC Java Config를 사용한 고급 커스터마이징
앞의 예제들에서 볼 수 있었듯이 MVC Java config와 MVC 네임스페이스는 사용하는 빈에 대한 깊은 지식 없이도 고수준의 결과물을 제공한다. 대신 어플리케이션에 필요한 것에 집중할 수 있게 해준다. 하지만 어떤 부분에서는 더 세밀한 제어를 하거나 사용하는 설정을 이해하기 원할 것이다.

더 세밀한 제어를 하기 위한 첫번째 단계는 당신의 위해서 생성된 의존 빈을 보는 것이다. MVC Java config에서는 WebMvcConfigurationSupport의 @Bean 메서드와 Javadoc을 볼 수 있다. 이 클래스의 설정은 @EnableWebMvc 어노테이션으로 자동 임포트된다. 실제로 @EnableWebMvc를 열어보면 @Import문을 볼 수 있다.

더 세밀한 제어를 하기 위한 다음 단계는 WebMvcConfigurationSupport에 생성된 빈 중 하나에서 프로퍼티를 커스터마이징하거나 자신의 인스턴스를 제공하도록 하는 것이다. 이를 위해서는 두 가지가 필요하다. 임포트하지 않도록 @EnableWebMvc 어노테이션을 제거하고 WebMvcConfigurationSupport를 직접 확장한다. 다음은 그 예제이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Java

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

  @Override
  public void addInterceptors(InterceptorRegistry registry){
    // ...
  }

  @Override
  @Bean
  public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {

    // 생성하거나 "super"가 아답터를 생성하게 한다
    // 그 다음 그 프로퍼티중 하나를 커스터마이징한다
  }
}

이 방법으로 빈을 수정하는 것은 이번 섹션의 앞에서 보여주었던 고수준의 결과물을 사용하는 것을 막지 않는다.

16.14.9 MVC 네임스페이스를 사용한 고급 커스터마이징
MVC 네임스페이스에서는 생성된 설정 이상의 세밀한 제어가 약간 더 어렵다.

세밀한 제어가 필요하다면 제공하는 설정을 교체하기 보다는 타입으로 커스터마이징하려는 빈을 탐지하는 BeanPostProcessor을 설정하고 필요에 따라 그 프로퍼티를 수정하는 것을 고려해봐라. 다음은 그 예제이다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

@Component
public class MyPostProcessor implements BeanPostProcessor {

  public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
    if (bean instanceof RequestMappingHandlerAdapter) {
      // 아답터의 프로퍼티를 수정한다
    }
  }
}

자동 탐지되도록 <component scan />에 MyPostProcessor를 포함시키거나 원한다면 XML 빈 선언에 명시적으로 MyPostProcessor를 선언할 수 있다.

[JAVA] 16장 웹 MVC 프레임워크 #2..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

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

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의 인스턴스이다.

볼 수 있듯이 스프링 아답터 클래스인 HandlerInterceptorAdapter는 HandlerInterceptor 인터페이스를 확장하기 쉽게 해준다.


Tip
위의 예제에서 어노테이션이 붙은 컨트롤러 메서드가 처리하는 모든 요청에 설정한 인터셉터를 적용할 것이다. 인터셉터 적용을 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이다.
ResourceBundleViewResolverResourceBundle 의 빈 정의를 사용하는 ViewResolver의 구현체로 번들에 기반한 이름(bundle base name)으로 지정한다. 보통 클래스 패스에 있는 프로퍼티 파일에 번들을 정의한다. 기본 파일명은 views.properties이다.
UrlBasedViewResolver명시적으로 패밍을 정의하지 않고 논리적인 뷰 이름을 URL로 직접 처리하는 ViewResolver 인터페이스의 간단한 구현체. 임의의 매핑을 하지 않고 직곽적인 방법으로 논리적인 이름이 뷰 리소스의 이름과 일치할 때 적합한다.
InternalResourceViewResolverInternalResourceView(사 실상 서블릿과 JSP)와 JstlView와 TilesView같은 하위클래스를 지원하는 UrlBasedViewResolver의 편리한 하위클래스. setViewClass(..)를 사용해서 이 리졸버가 생성하는 모든 뷰에 대한 뷰 클래스를 지정할 수 있다. 자세한 내용은 UrlBasedViewResolver 클래스의 Javadoc을 참고해라.
VelocityViewResolver / FreeMarkerViewResolverVelocityView(사실상 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) 메서드를 사용할 수 있다.

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헤더를 다음과 같이 바꾼다.

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 목록을 명시적으로 설정하지 않았다면 자동적으로 어플리케이션 컨텍스트에 정의한 뷰 리졸버를 사용한다.

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에서 이미 사용할 수 있는 정보로 크게 감소시킨다. 그러므로 리다이렉트 시나이로에서 주로 플래시 속성을 사용하기를 권장한다.

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 프로퍼티
프로퍼티기본값설명
cookieNameclassname + LOCALE쿠키의 이름
cookieMaxAgeInteger.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 구현체

클래스설명
FixedThemeResolverdefaultThemeName 프로퍼티로 설정한 고정된 테마를 선택한다.
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로 변환한다.