framework 와 library 는 명확한 차이가 있다.
library 는 사용자가 능동적으로 어떤 도구를 사용할지 고르고 골라 필요한걸 선택해서 사용한다.
framework 는 이미 구조가 짜여져 있고, 고정된 틀과 흐름이 있고 사용자는 그 흐름에 맞춰 원하는 결과가 나올 수 있도록 재료들을 제공해줘야 한다.
좀 더 쉽게 정리하면 library 는 사용자가 골라서 사용하는 거고, framework 는 사용자가 맞춰줘야 한다.
갑자기 library 와 framework 의 차이를 정리한 이유는 Spring 은 library 가 아니고 framework 이기 때문이다.
즉, framework 의 생명주기(lifecycle)에 대해 알고 언제 무슨 일이 일어나며 그 일이 일어날 때 사용자가 무엇을 제공해 주어야 원하는 결과를 받아볼 수 있기 때문이다.
그런 의미에서 지금부터 Spring Web MVC 설정하는 방법에 대해 정리한다.
앞선 내용에서 자주 언급 했지만, Servlet WebApplicationContext 를 만드는 DispatcherServlet 을 사용해서 Spring Web MVC 를 사용하면 DispatcherServlet.properties 파일을 참고하여 기본 전략이 설정 된다.
만약 이 기본 설정을 바꾸고 싶으면 그 타입의 bean 을 등록하여 덮어 쓰거나 추가할 수 있다.
그 예로 ViewResolver 타입의 InternalResourceViewResolver 클래스를 bean 으로 등록하여 prefix 와 suffix 설정을 추가한 ViewResolver 를 사용하도록 했었다.
@Bean
public ViewResolver myCustomViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
하지만 일반적으로 이렇게 사용 할 bean을 설정하는 일은 거의 없다.
왜냐하면 보편적으로 많이 사용하는 Web 설정에 대해 Spring 에서 @EnableWebMvc annotation 을 지원하기 때문이다.
package me.nimkoes;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@ComponentScan
@EnableWebMvc
public class MyWebConfig {
@Bean
public ViewResolver myCustomViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
@EnableWebMvc annotation 을 열어보면 DelegatingWebMvcConfiguration.class 를 import 하고 있다.
주석을 삭제한 코드는 다음과 같다.
package org.springframework.web.servlet.config.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
DelegatingWebMvcConfiguration.class 파일을 열어보면 WebMvcConfigurationSupport.class 를 상속 받고 있는것을 볼 수 있다.
package org.springframework.web.servlet.config.annotation;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
/**
* A subclass of {@code WebMvcConfigurationSupport} that detects and delegates
* to all beans of type {@link WebMvcConfigurer} allowing them to customize the
* configuration provided by {@code WebMvcConfigurationSupport}. This is the
* class actually imported by {@link EnableWebMvc @EnableWebMvc}.
*
* @author Rossen Stoyanchev
* @since 3.1
*/
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
// 생략...
}
WebMvcConfigurationSupport.class 파일을 열어보면 실제로 등록하는 bean 들을 볼 수 있다.
내용을 첨부 하기에는 너무 길기 때문에 관심이 있으면 직접 열어보고 대신 WebMvcConfigurationSupport.class api 문서를 링크로 첨부 한다.
문서를 통해서도 어떤 bean 을 등록하는지에 대한 정보도 확인할 수 있다.
This is the main class providing the configuration behind the MVC Java config. It is typically imported by adding @EnableWebMvc to an application @Configuration class. An alternative more advanced option is to extend directly from this class and override methods as necessary, remembering to add @Configuration to the subclass and @Bean to overridden @Bean methods. For more details see the javadoc of @EnableWebMvc. This class registers the following HandlerMappings: - RequestMappingHandlerMapping ordered at 0 for mapping requests to annotated controller methods. - HandlerMapping ordered at 1 to map URL paths directly to view names. - BeanNameUrlHandlerMapping ordered at 2 to map URL paths to controller bean names. - RouterFunctionMapping ordered at 3 to map router functions. - HandlerMapping ordered at Integer.MAX_VALUE-1 to serve static resource requests. - HandlerMapping ordered at Integer.MAX_VALUE to forward requests to the default servlet. Registers these HandlerAdapters: - RequestMappingHandlerAdapter for processing requests with annotated controller methods. - HttpRequestHandlerAdapter for processing requests with HttpRequestHandlers. - SimpleControllerHandlerAdapter for processing requests with interface-based Controllers. - HandlerFunctionAdapter for processing requests with router functions. Registers a HandlerExceptionResolverComposite with this chain of exception resolvers: - ExceptionHandlerExceptionResolver for handling exceptions through ExceptionHandler methods. - ResponseStatusExceptionResolver for exceptions annotated with ResponseStatus. - DefaultHandlerExceptionResolver for resolving known Spring exception types Registers an AntPathMatcher and a UrlPathHelper to be used by: - the RequestMappingHandlerMapping, - the HandlerMapping for ViewControllers - and the HandlerMapping for serving resources Note that those beans can be configured with a PathMatchConfigurer. Both the RequestMappingHandlerAdapter and the ExceptionHandlerExceptionResolver are configured with default instances of the following by default: - a ContentNegotiationManager - a DefaultFormattingConversionService - an OptionalValidatorFactoryBean if a JSR-303 implementation is available on the classpath - a range of HttpMessageConverters depending on the third-party libraries available on the classpath. |
WebMvcConfigurationSupport.class 에서 눈여겨 볼 만한 부분이 있다.
/**
* Provide access to the shared handler interceptors used to configure
* {@link HandlerMapping} instances with.
* <p>This method cannot be overridden; use {@link #addInterceptors} instead.
*/
protected final Object[] getInterceptors() {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService()));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
/**
* Override this method to add Spring MVC interceptors for
* pre- and post-processing of controller invocation.
* @see InterceptorRegistry
*/
protected void addInterceptors(InterceptorRegistry registry) {
}
전통적인 servlet 생명주기에서 보았던 interceptor 를 java 코드로 추가할 수 있도록 장치가 마련되어 있다.
InterceptorRegistry 클래스를 열어보니 ORDER 정보로 정렬 한 ArrayList 타입의 값을 사용하도록 되어 있었다.
@EnableWebMvc annotation 을 사용하기 전과 후에 어떤 bean 이 등록 되는지 비교해 보았다.
실제로 어떤 bean 이 등록 되는지 보기 위해 DispatcherServlet 클래스의 initStrategis 메소드에 debgger 를 설정했다.
@EnableWebMvc annotation 을 사용할 때 주의할 점이 있는데, ServletContext 를 반드시 설정해 주어야 한다.
그렇지 않으면 bean 설정이 제대로 되지 않고 오류가 발생 한다.
package me.nimkoes;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class WebApplication implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
/*
* @EnableWebMvc annotation 을 사용하면
* DispatcherServlet 이 ServletContext 를 참조하기 때문에 반드시 설정해 주어야 정상적으로 동작 한다.
*/
context.setServletContext(servletContext);
context.register(MyWebConfig.class);
context.refresh();
// DispatcherServlet 을 만들 때 설정 정보를 가지고 있는 context 를 넘겨 준다.
DispatcherServlet dispatcherServlet = new DispatcherServlet(context);
// ServletContext 에 DispatcherServlet 을 등록 한다.
ServletRegistration.Dynamic app = servletContext.addServlet("app", dispatcherServlet);
app.addMapping("/app/*");
}
}
왼쪽은 @EnableWebMvc annotation 을 사용하지 않았을 때, 오른쪽은 사용했을 경우이다.
handlerMappings, handlerAdapters, viewResolvers 세 항목 위주로 다른 점을 확인 해보았다.
handlerMappings 와 handlerAdapters 를 보면 등록 된 baen 은 같은데 순서가 달라졌다.
작은 차이이지만 이 순서도 마냥 무시할 수 있는것은 아닌데, 처리 가능한 handler 를 찾기 위해 반복문을 수행 하는데, 더 자주 사용하는 bean 을 배열 인덱스의 앞쪽에 위치 함으로 반복 횟수를 조금이라도 줄일 수 있다.
viewResolvers 는 @EnableWebMvc annotation 을 사용했을 때 ViewResolverComposite bean 이 하나 더 등록 되는것을 볼 수 있었다.
마지막으로 확인해볼 내용은 DelegatingWebMvcConfiguration.class 이다.
이 클래스는 @EnableWebMvc annotation 이 import 하고 있는 클래스이다.
본문 앞쪽에서는 이 클래스에 대해 WebMvcConfigurationSupport.class 클래스를 상속 받는다고 단순하게 정리하고 넘어갔다.
하지만 이 클래스는 Spring Web MVC 를 사용할 때 기능을 손쉽게 확장할 수 있도록 해주는 중요한 클래스이다.
이 delegation 구조를 활용해서 기능을 확장하는 방법은 WebMvcConfigurer 인터페이스를 구현하는 것이다.
인터페이스를 구현 한다고 해서 확장하지도 않을 기능에 대한 추상 메소드를 구현해야만 하는 일은 없다.
왜냐하면 이 인터페이스는 전부 default 메소드를 사용하기 때문이다.
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.config.annotation;
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
/**
* Defines callback methods to customize the Java-based configuration for
* Spring MVC enabled via {@code @EnableWebMvc}.
*
* <p>{@code @EnableWebMvc}-annotated configuration classes may implement
* this interface to be called back and given a chance to customize the
* default configuration.
*
* @author Rossen Stoyanchev
* @author Keith Donald
* @author David Syer
* @since 3.1
*/
public interface WebMvcConfigurer {
/**
* Helps with configuring HandlerMappings path matching options such as trailing slash match,
* suffix registration, path matcher and path helper.
* Configured path matcher and path helper instances are shared for:
* <ul>
* <li>RequestMappings</li>
* <li>ViewControllerMappings</li>
* <li>ResourcesMappings</li>
* </ul>
* @since 4.0.3
*/
default void configurePathMatch(PathMatchConfigurer configurer) {
}
/**
* Configure content negotiation options.
*/
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
/**
* Configure asynchronous request handling options.
*/
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
/**
* Configure a handler to delegate unhandled requests by forwarding to the
* Servlet container's "default" servlet. A common use case for this is when
* the {@link DispatcherServlet} is mapped to "/" thus overriding the
* Servlet container's default handling of static resources.
*/
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
/**
* Add {@link Converter Converters} and {@link Formatter Formatters} in addition to the ones
* registered by default.
*/
default void addFormatters(FormatterRegistry registry) {
}
/**
* Add Spring MVC lifecycle interceptors for pre- and post-processing of
* controller method invocations. Interceptors can be registered to apply
* to all requests or be limited to a subset of URL patterns.
* <p><strong>Note</strong> that interceptors registered here only apply to
* controllers and not to resource handler requests. To intercept requests for
* static resources either declare a
* {@link org.springframework.web.servlet.handler.MappedInterceptor MappedInterceptor}
* bean or switch to advanced configuration mode by extending
* {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
* WebMvcConfigurationSupport} and then override {@code resourceHandlerMapping}.
*/
default void addInterceptors(InterceptorRegistry registry) {
}
/**
* Add handlers to serve static resources such as images, js, and, css
* files from specific locations under web application root, the classpath,
* and others.
*/
default void addResourceHandlers(ResourceHandlerRegistry registry) {
}
/**
* Configure cross origin requests processing.
* @since 4.2
*/
default void addCorsMappings(CorsRegistry registry) {
}
/**
* Configure simple automated controllers pre-configured with the response
* status code and/or a view to render the response body. This is useful in
* cases where there is no need for custom controller logic -- e.g. render a
* home page, perform simple site URL redirects, return a 404 status with
* HTML content, a 204 with no content, and more.
*/
default void addViewControllers(ViewControllerRegistry registry) {
}
/**
* Configure view resolvers to translate String-based view names returned from
* controllers into concrete {@link org.springframework.web.servlet.View}
* implementations to perform rendering with.
* @since 4.1
*/
default void configureViewResolvers(ViewResolverRegistry registry) {
}
/**
* Add resolvers to support custom controller method argument types.
* <p>This does not override the built-in support for resolving handler
* method arguments. To customize the built-in support for argument
* resolution, configure {@link RequestMappingHandlerAdapter} directly.
* @param resolvers initially an empty list
*/
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
/**
* Add handlers to support custom controller method return value types.
* <p>Using this option does not override the built-in support for handling
* return values. To customize the built-in support for handling return
* values, configure RequestMappingHandlerAdapter directly.
* @param handlers initially an empty list
*/
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
/**
* Configure the {@link HttpMessageConverter HttpMessageConverters} to use for reading or writing
* to the body of the request or response. If no converters are added, a
* default list of converters is registered.
* <p><strong>Note</strong> that adding converters to the list, turns off
* default converter registration. To simply add a converter without impacting
* default registration, consider using the method
* {@link #extendMessageConverters(java.util.List)} instead.
* @param converters initially an empty list of converters
*/
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
/**
* A hook for extending or modifying the list of converters after it has been
* configured. This may be useful for example to allow default converters to
* be registered and then insert a custom converter through this method.
* @param converters the list of configured converters to extend.
* @since 4.1.3
*/
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
/**
* Configure exception resolvers.
* <p>The given list starts out empty. If it is left empty, the framework
* configures a default set of resolvers, see
* {@link WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)}.
* Or if any exception resolvers are added to the list, then the application
* effectively takes over and must provide, fully initialized, exception
* resolvers.
* <p>Alternatively you can use
* {@link #extendHandlerExceptionResolvers(List)} which allows you to extend
* or modify the list of exception resolvers configured by default.
* @param resolvers initially an empty list
* @see #extendHandlerExceptionResolvers(List)
* @see WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)
*/
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
/**
* Extending or modify the list of exception resolvers configured by default.
* This can be useful for inserting a custom exception resolver without
* interfering with default ones.
* @param resolvers the list of configured resolvers to extend
* @since 4.3
* @see WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers(List)
*/
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
/**
* Provide a custom {@link Validator} instead of the one created by default.
* The default implementation, assuming JSR-303 is on the classpath, is:
* {@link org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean}.
* Leave the return value as {@code null} to keep the default.
*/
@Nullable
default Validator getValidator() {
return null;
}
/**
* Provide a custom {@link MessageCodesResolver} for building message codes
* from data binding and validation error codes. Leave the return value as
* {@code null} to keep the default.
*/
@Nullable
default MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
View 를 반환할 때 사용했던 ViewResolver 타입의 InternalResourceViewResolver 객체를 직접 만들어 bean 으로 등록하는 대신 이 인터페이스를 사용해서 다음과 같이 동일하게 동작하도록 할 수 있다.
package me.nimkoes;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ComponentScan
@EnableWebMvc
public class MyWebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("WEB-INF", ".jsp");
}
}
등록된 bean 정보를 확인해보면 직접 bean 을 등록 했을 때와 달리 ViewResolvers 에 하나의 bean 만 등록 되어있고 설정한 값이 정상적으로 적용된 것도 같이 볼 수 있다.
이렇게 사용하는 방법은 Spring Boot 에서 지원하는 내용은 아니니 구분해서 알아두는게 좋을것 같다.
'Archive > Spring Web MVC' 카테고리의 다른 글
2.3 Spring Web MVC 의 Formatter (Spring Boot 자동 설정) (0) | 2021.07.06 |
---|---|
2.2 Spring Boot 의 Spring MVC 설정 (war 배포) (0) | 2021.07.05 |
1.5 Spring MVC 동작 원리 마무리 (0) | 2021.06.28 |
1.4 DispatcherServlet 기본 동작 원리 (0) | 2021.06.27 |
1.3 Spring MVC 연동 (Root & Servlet ApplicationContext) (0) | 2021.06.24 |