Spring MVC

raccoonback·2020년 7월 16일
1

boost course

목록 보기
9/10
post-thumbnail

이전 Web Server, Web Application Server 포스트에서 Spring MVC 대한 모든 요청과 응답은 DispatcherServlet에서 관리한다고 말했었다.

DispatcherServlet

DispatcherServlet은 스프링 MVC 구조를 구축할 수 있도록 도와주는 프론트 컨트롤러이다.
즉, DispatcherServlet은 모든 요청에 대한 실행 흐름을 책임을 가지고 있고, 사용자 요청 처리/뷰 렌더링/로직 등은 Controller, Model, View에 위임한다.

아래 그림을 통해서 MVC의 각각의 역할을 요약해 볼 수 있다.

Controller는 사용자의 요청을 처리하고,
그에 대한 결과 데이터를 Model에 담아 View에 전달하면,
View Template은 전달받은 Model과 선택한 View를 렌더링해서 사용자엑 반환(응답)해준다.

이러한 일련의 동작 흐름을 책임지는 것이 DispatcherServlet의 역할이다.
구체적으로,DispatcherServlet은 다음과 같은 동작을 한다.

  1. Controller에게 사용자 요청을 전달
    1. ControllerDispatcherServlet으로 부터 전달받은 Model 인자에 데이터 정보를 담으면, DispatcherServletView에 전달해준다.
    2. ControllerView에 대한 정보(경로/이름)을 반환하면, DispatcherServlet이 해당 정보를 ViewResolver에 전달한다.
    3. ControllerModelAndView 타입으로 ViewModel 정보를 한 번에 반환할 수도 있다.
  2. Controller에서 반환한 View 정보를 ViewResolver에게 전달해서 View를 찾아낸다.
  3. View Template에게 ViewModel 정보를 전달해서 렌더링한 뒤 사용자에게 결과를 응답한다.

DispatcherServlet 실행 흐름

그럼, 이제 DispatcherServlet이 어떻게 Controller, Model, View를 이용해서 일련의 요청을 처리하는지 자세히 살펴보자.

우선 사용자로부터 요청이 오면, DispatcherServletHandlerMapping을 이용해서 경로에 맞는 Controller를 찾는다.
탐색해서 찾은 ControllerHandler(HandlerExecutionChain)라고 부르는데, HandlerAdapter를 이용해서 Handler를 실행한다.
그리고 Controller가 반환한 View 정보(이름)를 View Resolver 전달해 반환할 View 오브젝트를 탐색한다.
마지막으로, View Resolver를 통해서 찾은 View 구현체Contoller가 담은 Model 정보를 View Tempate에 전달해서 View 렌더링을 수행한다.
렌더링이 완료된 결과물은 사용자 응답값에 담겨 반환된다.

요청 선처리 작업

위 그림과 같이 스프링은 Handler(HandlerExecutionChain) 탐색하기 이전에 아래와 같은 요청에 대한 선처리 작업을 수행한다.

그럼 각 단계에서 어떤 작업을 수행하는 지 알아보자.

국제화 처리

우선 요청이 어떤 지역에서 왔는지, 즉 HTTP 헤더 정보를 기반으로 지역 정보를 결정해 국제화 처리를 한다.
즉, DispatcherServlet은 자동으로 클라이언트의 Locale을 사용해서 메시지를 처리하는데, 이는 LocaleResolver 객체로 이뤄진다.

요청 정보 저장

다음으로,RequestContextHolder는 일반 Bean에서 HttpServletRequest, HttpServletResponse, HttpSession 등을 쉽게 사용할 수 있도록 각 정보들을 ThreadLocal 타입으로 저장하는데, 해당 정보는 동일한 Thread내에서는 어느 곳에서든 같은 HttpServletRequest, HttpServletResponse, HttpSession을 사용할 수 있도록 지원한다. (각 계층에서 해당 객체들을 주입받을 수 있다.)
요청마다 생성되는 Thread 내에서 안전(ThreadSafe)하게 HttpServletRequest, HttpServletResponse, HttpSession를 저장한다.

 HttpServletRequest req = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

FlashMap 복원

FlashMapManager은 리다이렉트된 정보(FlashMap)를 조회한다.
즉 리다이렉션하는 경우, 현재 요청 정보를 FlashMap에 저장하고, 리다리렉션 이후에는 이전 요청에 대한 저장된 속성 정보를 FlashMapManager로 조회/복원한다.
FlashMap는 플래시 속성을 보관하는데 사용하고 FlashMapManager는 FlashMap 인스턴스를 저장하고 획득하고 관리하는데 사용한다.

HTTP Multipart 요청 처리

마지막으로, MultipartResolver를 이용해서 파일 업로드같은 멀티파트 요청을 처리한다.

HandlerAdapter

HandlerAdapter가 결정된 HandlerExecutionChain을 실행하는데, 여기서 Interceptor가 동작하게 된다.
즉, HandlerExecutionChain 실행 이전에 인터셉터의 preHanle() 메서드를 호출하고, 실행 이후에는 인터셉터의 postHanle() 메서드를 호출한다.

Argument Resolver

HandlerAdapter는 핸들러를 실행하기에 앞서 어떠한 Arguments(인자)를 바인딩할 지를 결정한다.
여기서 대부분 사용자 요청에 대해 Arguments로 변환하는 과정을 거치는데, RequestMappingHandlerAdapter에 디폴트로 등록된 ArgumentResolver 목록은 아래 그림과 같다.

즉, Controller 구현시 많이 사용했던 RequestParam, PathVariable, RequestParam 등에 대한 ArgumentResolver가 구현되어 등록된 것을 확인할 수 있다.
뿐만 아니라, Errors, BindingResult, HttpSession, ServletRequest, ServletResponse, Model 같은 타입에 대한 ArgumentResolver가 구현되어 있기 때문에 인자로 전달받을 수 있는 것이다.
따라서, 특정 조건에 맞는 파라미터가 있는 경우에 핸들러를 호출시 원하는 Argument를 바인딩해준다.

또한, 스프링은 사용자가 필요한 ArgumentResolver를 커스텀하게 구현해서 등록할 수 있도록 HandlerMethodArgumentResolver 인터페이스를 제공하고 있다.

만약 HTTP 요청에서 User-Agent Header를 Argument 인자로 전달하고 싶다면 아래와 같이 추가할 수 있다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAgent {
}
public class UserAgentArgumentResolver implements HandlerMethodArgumentResolver {
    // 호출될 method 가 UserAgentArgumentResolver 를 실행 할 수 있는지 판별
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        // UserAgent 어노테이션이 붙은 파라미터가 있을 때만 바인딩해준다.
        return methodParameter.hasParameterAnnotation(UserAgent.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        return nativeWebRequest.getHeader("User-Agent");
    }
}
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.something.sample.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new UserAgentArgumentResolver());
    }
}
@Controller
public class TestController {
    @GetMapping(path="/test")
    public void test(@UserAgent String userAgent) {
        System.out.println("userAgent: " + userAgent);
    }
}

ReturnValue Handler

핸들러 실행 후에는 반환한 값을 처리할 수 있도록 ReturnValueHandler를 지원한다.
즉, 핸들러가 반환한 값에 대해서 특정 조건에 맞는 ReturnValueHandler에서 처리하는데, 디폴트 목록은 아래와 같다.

ReturnValueHandlerHandlerMethodReturnValueHandler 인터페이스를 구현해서 HandlerAdapter에 등록할 수 있다.

구체적인 예로, Controller에서 반환한 String 타입의 값이 적절한 View를 찾는 것을 본적이 있을 것이다.
이는 실제로, 반환값을 ViewResolver로 전달하기 이전에 ViewNameMethodReturnValueHandler에서 ModelAndViewContainer에 View 이름을 등록하는 것을 확인할 수 있다.

Handler 메소드에 주입해주는

DispatcherServlet 설정 방법

DispatcherServlet은 크게 두 가지 방법으로 설정이 가능하다.

  1. web.xml 에 정의해서 사용
  2. WebApplicationInitializer 인터페이스 구현해서 사용

web.xml

web.xml 사용하는 방법은 이전에서도 보았듯이 DispatcherSetvlet을 정의하고, Context 정보를 기재한다.
Context 정보어떤 종류의 컨테이너를 사용할 것인지Application Context에 대한 설정 정보를 담고 있어야 한다.

Context 종류는 크게 두 가지로 나뉘는데, contextClass에 정의한다.

  • XmlWebApplicationContext: XML을 사용해서 Application Context을 설정하는 경우 사용
  • AnnotationConfigWebApplicationContext: Java Annotation 사용해서 Application Context을 설정하는 경우 사용

Application Context에 대한 설정 정보를 담은 위치는 contextConfigLocation에 정의한다.

Spring 5.0 이전에는 WebMvcConfigurerAdapter 추상 클래스을 구현해서 설정하였지만, 이후부터는 WebMvcConfigurer 인터페이스를 구현해서 설정 정보를 정의하는 것을 권장한다.

따라서, 설정 정보를 담은 경로를 contextConfigLocation에 정의해준다.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.something.sample.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
    }
    
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

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

    @Bean
    public InternalResourceViewResolver getInternalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <display-name>Spring Sample</display-name>
    <servlet>
        <servlet-name>spring-sample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            // java annotation 기반의 application context 사용
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            // 설정 파일 경로
            <param-value>com.something.sample.config.WebMvcContextConfiguration</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring-sample</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

WebApplicationInitializer

Java 코드 방식을 이용하는 경우에는 WebApplicationInitializer 인터페이스를 구현해서 DispatcherServlet을 정의한다.

WebApplicationInitializer 인터페이스를 구현시에는 애플리케이션 구동시 반드시 필요한 설정은 onStartup() 메서드 구현에서 이루어진다.

public class SampleApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 사용할 Application Context 정의
        AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
        // 설정 파일 정보
        webApplicationContext.register(WebApplicationConfig.class);

        // DispatcherServlet 생성
        DispatcherServlet dispatcherServlet = new DispatcherServlet(webApplicationContext);

        // Servlet 설정
        ServletRegistration.Dynamic servlet = servletContext.addServlet("spring-sample", dispatcherServlet);
        servlet.setLoadOnStartup(0);
        servlet.addMapping("/");
    }
}

위 코드를 살펴보면, DispatcherServlet에서 사용할 Application Context 객체를 생성하고 설정 정보를 담은 클래스를 정보를 등록한다.

마직막에는 Servlet 정보를 설정하고 있는 것을 볼 수 있다.

설정 클래스

@EnableWebMvc

@EnableWebMvc 어노테이션은 DelegatingWebMvcConfiguration 설정 정보를 Import한다.
구체적으로,DelegatingWebMvcConfigurationWebMvcConfigurationSupport를 상속받아, DispatcherServlet에 필요한RequestMappingHandlerMapping, RequestMappingHandlerAdapter, ExceptionHandlerExceptionResolver, MessageConverter 과 같은Bean들을 자동으로 설정해준다.
뿐만 아니라, WebMvcConfigurer 타입 구현체에 정의한 모든 Configuration Bean을 감지하여 Context를 설정한다.

WebMvcConfigurationSupport

WebMvcConfigurationSupport은 Spring MVC를 구성하는 메인 클래스이다.
WebMvcConfigurationSupport은 Spring MVC를 구성하는데 필요한 HandlerMapping, HandlerAdapter HandlerExceptionResolverComposite, ViewResolver, AntPathMatcher, UrlPathHelper 등을 Application Context에 등록한다.

WebMvcConfigurer

Spring 5.0 이후부터는 WebMvcConfigurer 인터페이스를 구현해서 커스텀 설정 정보를 추가할 수 있다.

@EnableWebMvc 어노테이션이 붙은 클래스는 WebMvcConfigurer 인터페이스를 다시 호출하여 기본 구성을 사용자가 재정의 할 수 있도록 기회를 제공한다.

아래 예제를 보면, configureDefaultServletHandling(DefaultServletHandlerConfigurer) 메서드를 통해서 Default Servlet Handler 사용 여부를 결정할 수 있는 것을 확인할 수 있다.

구체적으로, 일치하지 않는 매핑 정보가 들어왔을 경우 서블릿 컨테이너의 Default Servlet(DefaultServletHttpRequestHandler)이 사용되도록 설정하는 것이다.
즉, DefaultServletHttpRequestHandler는 개발자가 설정한 리소스 처리 방식(addResourceHandlers(ResourceHandlerRegistry))을 토대로 기본 정적 리소스 처리 방식을 재정의한다.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.something.sample.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
        registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
        registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);

    }

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

    @Bean
    public InternalResourceViewResolver getInternalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

참고 자료

profile
한번도 실수하지 않은 사람은, 한번도 새로운 것을 시도하지 않은 사람이다.

0개의 댓글