Archive/Spring Web MVC

2.1 Spring Web MVC 설정 @EnableWebMvc, WebMvcConfigurer

nimkoes 2021. 7. 4. 01:44
728x90

 

 

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 에서 지원하는 내용은 아니니 구분해서 알아두는게 좋을것 같다.

 

 

 

 

728x90