16.11 예외 처리
16.11.1 HandlerExceptionResolver
스 프링의 HandlerExceptionResolver 구현체가 컨트롤러가 실행되는 중 발생한 의도치 않은 예외를 다룬다. HandlerExceptionResolver는 웹 어플리케이션 디스크립터인 web.xml에 정의할 수 있는 예외 매핑과 꽤 유사하다. 하지만 HandlerExceptionResolver가 더 유연한 방법을 제공한다. 예를 들어 HandlerExceptionResolver는 예외가 던져졌을 때 어떤 핸들러를 실행할 것인지에 대한 정보를 제공한다. 게다가 예외 처리의 프로그래밍적인 방법은 요청을 다른 URL로 포워딩하기 전에(서블릿에 특화된 예외매핑을 사용할 때와 같은 결과이다.) 적절하게 응답할 수 있는 더 많은 옵션을 제공한다.
resolveException(Exception, Handler) 메서드를 구현하는 것과 ModelAndView를 반환하는 것과만 관련된 HandlerExceptionResolver 인터페이스를 구현하는 데 추가적으로 SimpleMappingExceptionResolver를 사용할 수도 있다. 이 리졸버는 던져진 모든 예외의 클래스명을 받아서 뷰 이름에 매핑할 수 있게 해준다. 이는 기능적으로는 서블릿 API의 예외 매핑기능과 동일하지만 여러 핸들러로 더욱 세밀한 예외 매핑을 구현할 수도 있게 한다.
기본적으로 DispatcherServlet이 DefaultHandlerExceptionResolver를 등록한다. 이 리졸버는 특정 응답 상태코드를 설정해서 해당 표준 스프링 MVC 예외를 처리한다.
예외 | HTTP 상태 코드 |
---|---|
ConversionNotSupportedException | 500 (Internal Server Error) |
HttpMediaTypeNotAcceptableException | 406 (Not Acceptable) |
HttpMediaTypeNotSupportedException | 415 (Unsupported Media Type) |
HttpMessageNotReadableException | 400 (Bad Request) |
HttpMessageNotWritableException | 500 (Internal Server Error) |
HttpRequestMethodNotSupportedException | 405 (Method Not Allowed) |
MissingServletRequestParameterException | 400 (Bad Request) |
NoSuchRequestHandlingMethodException | 404 (Not Found) |
TypeMismatchException | 400 (Bad Request) |
16.11.2 @ExceptionHandler
HandlerExceptionResolver 인터페이스의 대안은 @ExceptionHandler 어노테이션이다. 컨트롤러 메서드가 실행되는 동안 특정 타입의 예외가 던져졌을 때 어떤 메서드를 호출할 것인지 지정하려고 컨트롤러내에서 @ExceptionHandler 메서드 어노테이션을 사용한다. 예를 들면 다음과 같이 사용한다.
@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 구현체를 보자. 특히 클래스의 이름을 주의깊게 봐라.
public class ViewShoppingCartController implements Controller {
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
// 이 예제에서는 구현체가 크게 중요하지 않다...
}
}
다음은 이에 대응하는 스프링 웹 MVC 설정파일의 일부이다.
<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에 매핑된다
- AdminController는 /admin/* 요청 URL에 매핑된다
- CatalogController는 /catalog/* 요청 URL에 매핑된다
ControllerClassNameHandlerMapping 클래스가 AbstractHandlerMapping 기반 클래스를 확장하므로 HandlerInterceptor 인스턴스와 다른 다수의 HandlerMapping 구현체에서 하는 모든 것을 정의할 수 있다.
16.12.2 모델 ModelMap (ModelAndView)
ModelMap 클래스는 일반적인 작명 관례를 따르는 View에 나타날 추가적인 객체를 만들수 있는 기본적으로 중요한 Map이다. 다음의 Controller 구현체를 보자. 관련된 어떤 이름도 지정하지 않고도 이 객체는 ModelAndView에 추가된다.
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이 되지 않는다.
약간의 토론 끝에 “최소 놀람의 법칙”이 이기는 것으로 끝나서 이렇게 결정되었다.
스프링 웹 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을 논리적인 뷰 이름으로 매핑한다.
public class RegistrationController implements Controller {
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
// 요청을 처리한다...
ModelAndView mav = new ModelAndView();
// 필요에 따라 모델에 데이터를 추가한다...
return mav;
// View가 없거나 논리적인 뷰 이름이 설정되었다
}
}
<?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의 기본 설정으로 좋다면 스프링 웹 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를 설정한다.
<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 어노테이션을 추가해라.
@EnableWebMvc
@Configuration
public class WebConfig {
}
동일한 내용을 XML로 설정하려면 mvc:annotation-driven 요소를 사용해라.
<?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)를 등록한다.
이는 다음을 활성화 한다.
- 데이터 바인딩에 사용한 JavaBeans PropertyEditors에 추가적으로 ConversionService 인스턴스를 통한 스프링 3 방식의 타입 변환.
- ConversionService로 @NumberFormat 어노테이션을 사용해서 숫자 필드의 포매팅을 지원한다
- 클래스패스에 Joda Time 1.3 이상의 버전이 있다면 @DateTimeFormat 어노테이션을 사용해서 Date, Calendar, Long, Joda Time 필드의 포매팅을 지원한다.
- 클래스패스에 JSR-303 프로바이더가 있으면 @Valid로 @Controller 입력의 유효성검사를 지원한다.
- HttpMessageConverter가 @RequestMapping나 @ExceptionHandler 메서드에서 @RequestBody 메서드 파라미터와 @ResponseBody 메서드 반환값을 지원한다.
- 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를 봐라.
@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에서 코드 자동완성 기능을 사용할 수 있다. 아래 예제에서 사용할 수 있는 것들중 일부를 보여주고 있다.
<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 경로패턴에 한정해서 적용되도록 설정할 수 있다.
자바로 인터셉터를 등록하는 예제:
@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로 등록하는 예제:
<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"라는 뷰로 보내는 예제:
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
<mvc:view-controller> 요소를 사용해서 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 디렉토리에서 리소스를 제공하려면 다음과 같이 사용할 것이다.
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/public-resources/");
}
}
XML로는 다음과 같이 사용한다.
<mvc:resources mapping="/resources/**" location="/public-resources/"/>
브라우저 캐시가 사용할 최대 기간을 보장하고 브라우저의 HTTP 요청을 줄이려고 1년 만료 헤더와 함께 이러한 리소스를 제공하려면 다음과 같이 설정한다.
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/public-resources/").setCachePeriod(31556926);
}
}
XML로는 다음과 같이 설정한다.
<mvc:resources mapping="/resources/**" location="/public-resources/" cache-period="31556926"/>
mapping 속성은 SimpleUrlHandlerMapping가 사용할 수 있는 Ant 패턴이어야 하고 location 속성은 하나이상의 리소스 디렉토리 위치를 지정해야 한다. 목록을 콤마로 구분해서 여러 리소스 위치를 지정할 수도 있다. 해당 요청의 리소스가 존재하는지 여부를 지정한 순서대로 지정된 위치를 확인한다. 예를 들어 웹 어플리케이션 루트와 클래스 패스에 어떤 jar에서라도 알려진 /META-INF/public-web-resources/ 경로 모두에서 리소스를 제공하려면 다음과 같이 설정한다.
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/", "classpath:/META-INF/public-web-resources/");
}
}
XML에서는 다음과 같이 설정한다.
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/public-web-resources/"/>
새 로운 버전의 어플리케이션을 배포했을 때 변경될 수도 있는 리소스를 제공하는 경우 클라이언트가 새로운 버전으로 배포된 어플리케이션의 리소스를 강제로 요청하도록 리소스 요청에 사용하는 매핑 패턴에 버전 문자열을 포함시키는 것을 권장한다. 이러한 버전 문자열은 파라미터화될 수 있고 SpEL로 접근할 수 있으므로 새로운 버전을 배포할 때 단일 지점에서 쉽게 관리할 수 있다.
예 제처럼 프로덕션에서 Dojo 자바스크립트 라이브러리의 성능 최적화가 된 커스텀 빌드버전 (권장사항대로)을 사용하는 어플리케이션을 생각해 보자. 그리고 이 빌드는 일반적으로 웹 어플리케이션내에서 /public-resources/dojo/dojo.js 경로에 배포한다. Dojo의 다른 부분들이 어플리케이션의 새로운 각 버전에 대한 커스컴 빌드에 포함될 것이므로 클라이언트 웹 브라우저가 새로운 버전의 어플리케이션이 배포될 때마다 커스텀 빌드 dojo.js 리소스를 강제대로 새로 다운로드 하도록 해야 한다. 다음과 같이 어플리케이션의 버전을 프로퍼티 파일에서 관리하는 것이 이를 위한 가장 간단한 방법이다.
application.version=1.0.0
그 다음 프로퍼티 파일의 값을 util:properties 태그를 사용하는 빈처럼 SpEL에서 접근가능하도록 한다.
<util:properties id="applicationProps" location="/WEB-INF/spring/application.properties"/>
이제 SpEL로 접근가능한 어플리케이션 버전을 resources 태그를 사용할때 포함시킬 수 있다.
<mvc:resources mapping="/resources-#{applicationProps['application.version']}/**" location="/public-resources/"/>
자바에서는 @PropertySouce 어노테이션을 사용해서 정의된 모든 프로퍼티에 접근하도록 Environment 추상화를 주입할 수 있다.
@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 태그의 장점을 취할 수 있다.
<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)보다 낮은 값으로 설정해야 한다.
기본 설정을 사용해서 이 기능을 활성화 하려면 다음과 같이 한다.
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
XML에서는 다음과 같이 설정한다.
<mvc:default-servlet-handler/>
"/" 서블릿 매핑을 오버라이딩할 때의 주의할 점은 기본 서블릿의 RequestDispatcher를 경로가 아니라 이름으로 획득해야 한다는 것이다. DefaultServletHttpRequestHandler는 시작할 때 대부분의 주요 서블릿 컨테이너(Tomcat, Jetty, Glassfish, JBoss, Resin, WebLogic, WebSphere를 포함해서)의 알려진 이름 목록을 사용해서 컨테이너의 기본 서블릿을 자동으로 탐지하려고 시도할 것이다. 다른 이름으로 기본 서블릿을 커스텀해서 설정했거나 기본 서블릿 이름을 알지 못하는 다른 서블릿 컨테이너를 사용했다면 기본 서블릿의 이름을 다음 예제에서 처럼 명시적으로 제공해야 한다.
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("myCustomDefaultServlet");
}
}
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를 직접 확장한다. 다음은 그 예제이다.
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Override
public void addInterceptors(InterceptorRegistry registry){
// ...
}
@Override
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
// 생성하거나 "super"가 아답터를 생성하게 한다
// 그 다음 그 프로퍼티중 하나를 커스터마이징한다
}
}
이 방법으로 빈을 수정하는 것은 이번 섹션의 앞에서 보여주었던 고수준의 결과물을 사용하는 것을 막지 않는다.
16.14.9 MVC 네임스페이스를 사용한 고급 커스터마이징
MVC 네임스페이스에서는 생성된 설정 이상의 세밀한 제어가 약간 더 어렵다.
세밀한 제어가 필요하다면 제공하는 설정을 교체하기 보다는 타입으로 커스터마이징하려는 빈을 탐지하는 BeanPostProcessor을 설정하고 필요에 따라 그 프로퍼티를 수정하는 것을 고려해봐라. 다음은 그 예제이다.
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
// 아답터의 프로퍼티를 수정한다
}
}
}
자동 탐지되도록 <component scan />에 MyPostProcessor를 포함시키거나 원한다면 XML 빈 선언에 명시적으로 MyPostProcessor를 선언할 수 있다.
와... 제가 본 글중에 가장 깔끔하고 명확하게 써진 글이네요. 추천이 있으면 누르고 싶습니다 ^^
내용은 레퍼런스 문서의 번역이라서 그렇습니다. ^^