Spring Web MVC 로 웹 애플리케이션 개발을 할 때 Servlet WebApplicationContext 를 만드는 가장 중요한 클래스인 DispatcherServlet 에 대해 알아보기 위해 IntelliJ IDEA 로 프로젝트를 새로 만들었다.
IntelliJ IDEA 를 실행하고 Create New Project 를 클릭해서 프로젝트 생성을 시작한다.
Spring Boot 가 아닌 maven 프로젝트에 Spring web 에 대한 의존성만 추가해서 Spring Web MVC 를 사용한다.
Maven 프로젝트를 선택하고 archetype 중 maven-archetype-webapp 을 선택했다.
적당한 GroupId 와 ArtifactId 를 입력한다.
내가 설치한 maven 을 사용하도록 설정 했다.
Local repository 는 maven 을 통해 maven 중앙 저장소에서 내려받는 의존성 (dependency) 을 저장하는 위치이다.
넥서스를 사용하지 않을 것이기 때문에 중앙 저장소라고 했다.
프로젝트를 생성한 다음 pom.xml 파일만 다음과 같이 수정해서 Spring Web 과 관련된 의존성 추가했다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.nimkoes</groupId>
<artifactId>SpringWebMVCSandbox</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>SpringWebMVCSandbox Maven Webapp</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
</dependencies>
<build>
// 생략 ...
</build>
</project>
혹시 의존 라이브러리가 다운받아지지 않는다면 오른쪽 위에 있는 새로고침 버튼을 눌러 pom.xml 에 작성한 dependency 를 다운받을 수 있도록 한다.
main 아래 java 폴더를 만들어 준다.
File > Project Structure 메뉴에 들어간다.
Modules 메뉴에 들어가서 방금 만들어 준 java 폴더를 IDE 가 Sources 폴더로 인식할 수 있도록 설정해 준다.
MVC 패턴에서 Controller 역할을 하는 클래스로 MyHelloController 를 만들고, 이 Controller 에서 사용 할 서비스로 MyHelloService 파일을 만들었다.
그리고 DispatcherServlet 이 사용 할 웹 설정 파일로 MyWebConfig 파일을 만들었다.
[MyHelloController.java]
package me.nimkoes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@Autowired
MyHelloService myHelloService;
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "Hello, " + myHelloService.getName();
}
}
[MyHelloService.java]
package me.nimkoes;
import org.springframework.stereotype.Service;
@Service
public class MyHelloService {
public String getName() {
return "nimkoes";
}
}
[MyWebConfig.java]
package me.nimkoes;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class MyWebConfig {
}
web.xml 파일도 Servlet WebApplicationContext 를 하나만 사용하도록 DispatcherServlet 을 등록해 준다.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!-- Spring Web MVC 사용하기 위한 DispatcherServlet servlet 등록 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- ContextLoaderListener 를 사용하기 위해 context-param 으로 parameter 를 정의한 것처럼 -->
<!-- DispatcherServlet 을 사용하기 위해 init-param 으로 parameter 를 정의할 수 있다. -->
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.nimkoes.MyWebConfig</param-value> <!-- Servlet WebApplicationContext 를 만들 때 사용할 설정 파일 -->
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
잘 적용 되었는지 확인해보기 위해 local tomcat 을 설정 한다.
Add Configuration 을 클릭 한다.
Local Tomcat 설정을 하기 위해 + 버튼을 누르고 Tomcat Server 아래 Local 을 선택 한다.
사용 할 Tomcat 을 선택한다.
그 다음 Fix 버튼을 누르고 SpringWebMVCSandbox:war exploded 를 선택 한다.
war exploded 를 선택 하면 war 패키징 된 웹 애플리케이션을 서버에 war 파일 압축을 해체하여 배포하겠다는 의미이다.
마지막으로 Application context 를 / 만 남기고 삭제해서 url 에 context root 를 입력하지 않도록 해주었다.
그리고나서 서버를 실행 한 다음 브라우저를 통해 /app/hello 요청을 보내면 이전과 동일한 결과가 나오는 것을 확인할 수 있다.
그럼 이제 어떻게 해서 DispatcherServlet 이 사용자의 요청을 처리할 수 있었는지 확인해보자.
DispatcherServlet 클래스는 다음과 같은 상속 구조를 가지고 있다.
public class DispatcherServlet extends FrameworkServlet { ... }
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware { ... }
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware { ... }
doGet 과 doPost 에 대해서만 작성했지만 다른 Http 요청에 대해서도 동일한 구조를 가지고 있다.
DispatcherServlet 클래스의 doService 메소드에 debugger 를 걸어두고 /app/hello 요청을 보내봤다.
/app/hello 요청을 포함한 모든 HTTP 요청은 doService 메소드를 실행하기 때문에 debugger 에 요청이 걸린다.
한 줄씩 실행 하면서 내려가면 doDispatch 라는 메소드를 호출하는 것을 볼 수 있고 실제로 이 곳에서 HTTP 요청을 dispatch 하는 작업을 한다.
doDispatch 에서는 우선 이 요청이 파일 처리를 요청하는 multipart 요청인지 확인한다.
지금은 파일 처리를 하는 요청이 아니기 때문에 다음으로 넘어간다.
그 다음 'Determine handler for the current request.' 라는 주석이 작성 된 getHandler 를 볼 수 있는데,
현재 요청을 처리할 수 있는 handler 를 찾는 일을 한다.
/**
* Return the HandlerExecutionChain for this request.
* <p>Tries all handler mappings in order.
* @param request current HTTP request
* @return the HandlerExecutionChain, or {@code null} if no handler could be found
*/
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
handler 는 handler method 와 같은 뜻이고 대표적으로 @GetMapping, @PostMapping 등으로 @RequestMapping 이 설정된 메소드가 있다.
그리고 DispatcherServlet 의 handlerMappings 에 등록 된 것은 현재 요청을 처리 할 handler 를 찾는 일을 한다.
getHandler 메소드를 보면 DispatcherServlet 클래스의 List<HandlerMapping> 타입의 handlerMappings 에 담긴 HandlerMapping 을 순회 하면서 현재 요청을 처리할 수 있는 handler 를 가져온다.
위 코드에서 보면 HandlerMapping 타입의 mapping 변수에 담고 getHandler 에 요청 정보를 가지고 처리 가능한 handler method 가 있는지 확인하는것을 볼 수 있다.
IDEA 의 도움을 받으면 현재 DispatcherServlet 에는 두 개의 HandlerMapping 이 등록되어 있는 것을 확인할 수 있다.
이 두 개의 HandlerMapping 은 DispatcherServlet 이 기본적으로 제공하는 HandlerMapping 이다.
/** List of HandlerMappings used by this servlet. */
@Nullable
private List<HandlerMapping> handlerMappings;
이 값을 어디서 할당해 주는지 궁금해서 코드를 따라가 보았다.
ApplicationContext 가 init 될 때 DispatcherServlet 클래스의 initStrategies 메소드를 실행하도록 되어 있었다.
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
이 내용을 보기 전에 Strategy Pattern (전략 패턴) 에 대해 알고 있으면 좋을것 같다.
짧게 설명하면 하나의 인터페이스를 구현한 여러 클래스들이 있고, 이 인터페이스 타입을 사용하는 쪽에서는 어떤 객체든 이 인터페이스를 구현 한 클래스의 객체이기만 하면 문제없이 동작할 수 있다.
이 때 특정 인터페이스의 구현 클래스의 객체를 '전략' 이라고 부르며, 상황에 따라 인터페이스 타입의 변수에는 이 전략들을 바꿔가면서 사용할 수 있는데, 이것을 전략 패턴이라고 한다.
initStrategies 메소드에 보면 initHandlerMappings 라는 메소드가 있는데 다음과 같이 사용 가능한 handler 를 설정해 주고 있다.
/**
* Initialize the HandlerMappings used by this class.
* <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace,
* we default to BeanNameUrlHandlerMapping.
*/
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
BeanNameUrlHandlerMapping 과 RequestMappingHandlerMapping 두 개의 handler 중 /app/hello 요청을 처리할 수 있는 handler 는 RequestMappingHandlerMapping 이다.
RequestMappingHandlerMapping 은 @Controller 와 @RequestMapping 또는 @RequestMapping annotation 을 meta annotation 으로 사용하고 있는 @GetMapping, @PostMapping 등을 처리할 수 있는 handler 이다.
실제로 @GetMapping 이 구현 된 것을 보면 @RequestMapping 인 것을 볼 수 있다.
/*
* Copyright 2002-2016 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.bind.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.core.annotation.AliasFor;
/**
* Annotation for mapping HTTP {@code GET} requests onto specific handler
* methods.
*
* <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}.
*
*
* @author Sam Brannen
* @since 4.3
* @see PostMapping
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
// ... 생략
}
/app/hello 요청을 처리 할 handler 인 RequestMappingHandlerMapping 을 찾았기 때문에 더이상 mappedHandler 가 null 이 아니다.
그 다음으로 하는 일은 HandlerAdapter 를 찾는 일이다.
HandlerMapping 은 요청을 처리 할 handler 를 찾는 일을 했다면, HandlerAdapter 는 실제로 요청을 처리한다.
HandlerAdapter 역시 initStrategies 메소드 안에서 사용 가능한 HandlerMapping 을 등록하면서 사용 가능한 HandlerAdapter 도 같이 등록 한다. 이런 모든 동작을 할 때는 전략 패턴이 사용 되었다고 생각하면 된다.
그래서 getHandlerAdapter 는 요청을 처리해줄 HandlerAdapter 를 찾아주는 역할을 한다.
/**
* Return the HandlerAdapter for this handler object.
* @param handler the handler object to find an adapter for
* @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error.
*/
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
HandlerAdapter 에는 따로 추가하지 않아도 사용 가능한 3개의 Adapater 가 등록되어 있는 것을 볼수 있다.
코드를 한 줄씩 실행해보면 현재 /app/hello 요청을 처리할 수 있는 HandlerAdapter 는 RequestMappingHandlerAdapter 인 것을 확인할 수 있다.
다음은 HTTP 요청이 어떤 타입인지 확인하고 "GET" 요청에 대해 바뀐 내용이 없을 경우 후속 작업 없이 작업을 마치도록 되어 있다.
브라우저의 개발자 도구에서 네트워크 요청 응답에 대한 정보를 보면 '304 Not Modified' 응답을 받을 때가 있는데, 위와 같은 경우에 해당하는 것 같다.
요청을 처리 할 handler method 를 찾았고, 실제로 그 요청을 처리 할 HandlerAdapter 를 찾았기 때문에 실제로 그 요청을 처리 하는 일을 해야 한다.
ModelAndView 타입의 객체를 반환하는 handle 메소드를 실행해서 처리한다.
/**
* This implementation expects the handler to be an {@link HandlerMethod}.
*/
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}
handle 메소드가 Override 된 메소드라고 되어 있어서 부모를 확인해 보았다.
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
/**
* MVC framework SPI, allowing parameterization of the core MVC workflow.
*
* <p>Interface that must be implemented for each handler type to handle a request.
* This interface is used to allow the {@link DispatcherServlet} to be indefinitely
* extensible. The {@code DispatcherServlet} accesses all installed handlers through
* this interface, meaning that it does not contain code specific to any handler type.
*
* <p>Note that a handler can be of type {@code Object}. This is to enable
* handlers from other frameworks to be integrated with this framework without
* custom coding, as well as to allow for annotation-driven handler objects that
* do not obey any specific Java interface.
*
* <p>This interface is not intended for application developers. It is available
* to handlers who want to develop their own web workflow.
*
* <p>Note: {@code HandlerAdapter} implementors may implement the {@link
* org.springframework.core.Ordered} interface to be able to specify a sorting
* order (and thus a priority) for getting applied by the {@code DispatcherServlet}.
* Non-Ordered instances get treated as lowest priority.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @see org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
* @see org.springframework.web.servlet.handler.SimpleServletHandlerAdapter
*/
public interface HandlerAdapter {
// ... 생략
/**
* Use the given handler to handle this request.
* The workflow that is required may vary widely.
* @param request current HTTP request
* @param response current HTTP response
* @param handler handler to use. This object must have previously been passed
* to the {@code supports} method of this interface, which must have
* returned {@code true}.
* @throws Exception in case of errors
* @return a ModelAndView object with the name of the view and the required
* model data, or {@code null} if the request has been handled directly
*/
@Nullable
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
// ... 생략
}
중간에 생략한 내용이 있지만, 이 메소드는 HandlerAdapter interface 에 정의 된 추상 메소드임을 알 수 있었다.
handle 메소드가 반환하는 handleInternal 메소드는 바로 아래 추상 메소드로 선언되어 있었다.
/**
* Use the given handler method to handle the request.
* @param request current HTTP request
* @param response current HTTP response
* @param handlerMethod handler method to use. This object must have previously been passed to the
* {@link #supportsInternal(HandlerMethod)} this interface, which must have returned {@code true}.
* @return a ModelAndView object with the name of the view and the required model data,
* or {@code null} if the request has been handled directly
* @throws Exception in case of errors
*/
@Nullable
protected abstract ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;
실제 구현된 곳을 찾아가 보니 예상한 대로 RequestMappingHandlerAdapter 클래스에서 구현하고 있었다.
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
이 안에서는 handlerMethod 를 invokeHandlerMethod 로 실행하게 되는데, java 의 reflection 기술을 사용한다.
handlerMethod 에는 실제로 실행 할 method 에 대한 정보를 가지고 있는데, /app/hello 요청을 처리 할 method 가 담겨있는 것을 볼 수 있다.
handlerMethod 를 실행하고 나면, 실행 결과를 어떻게 반환할지에 대해 처리를 한다.
이때는 이미 method 실행이 끝난 다음으로 returnValue 를 누가 어떻게 처리할지 결정하게 된다.
MyHelloController 클래스에 정의한 hello 메소드를 다음과 같이 작성 했다.
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "Hello, " + myHelloService.getName();
}
여기에 작성한 @ResponseBody annotation 에 의해 15개의 returnValueHandler 중 11번째인 RequestResponseBodyMethodProcessor 가 처리하게 된다.
반환 값에 대한 다양한 종류의 ReturnValueHandler 가 존재하는 이유는 그만큼 요청에 대한 반환 값의 형태가 다양하기 때문이다.
이 return value handler 는 converter 를 사용해서 반환 값을 http response body 응답 본문에 넣어주는 처리를 한다.
처리가 끝나면 드디어 다음과 같이 브라우저에서 실행 결과가 출력 되는 것을 볼 수 있다.
지금까지 DispatcherServlet 이 view 가 없는 응답에 대한 요청을 어떻게 처리하는지 정리해 보았다.
이번에는 응답 결과 view 를 반환하는 경우에 대해 정리한다.
Controller 로 등록한 MyHelloController 클래스에 /app/sample 요청을 처리 할 handler method 를 하나 추가 한다.
package me.nimkoes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class MyHelloController {
@Autowired
me.nimkoes.MyHelloService myHelloService;
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "Hello, " + myHelloService.getName();
}
@GetMapping("/sample")
public String sample() {
return "/WEB-INF/sample.jsp";
}
}
새로 추가한 'GET' 요청을 처리 할 /sample 과 /hello 의 다른 점은 메소드에 @ResponseBody annotation 을 작성하지 않았다는 것이다.
똑같이 String 타입의 결과를 반환 하지만, @ResponseBody annotation 을 사용하면, 그 응답 결과를 HTTP response body 영역에 문자열을 담고, 그렇지 않은 경우 (별도의 처리를 하지 않은 경우) 문자열이 가리키는 위치의 view 자원을 찾는 시도를 한다.
작성한 /sample 은 'WEB-INF' 폴더 아래 'sample.jsp' 파일을 찾도록 했기 때문에 그 위치에 jsp 파일을 만들어 주었다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>sample page</title>
</head>
<body>
<h2>Hello, My sample page !</h2>
</body>
</html>
이번에도 debugger 모드로 서버를 시작한 다음 요청이 어떻게 처리 되는지 확인해 보았다.
view 가 없는 @ResponseBody annotation 이 작성된 handler method 를 처리할 때와 눈에 띄게 다른 점은 return 을 처리하는 handler 와 ModelAndView 타입 객체에 값이 할당 된다는 점이다.
실제로 앞서 view 가 없던 경우 RequestResponseBodyMethodProcessor 대신 ViewNameMethodReturnValueHandler 가 return 값을 처리하도록 매핑된 것을 확인할 수 있다.
다음으로 view 를 반환할 경우 ModelAndView 타입 객체인 mav 에 반환할 view 와 view 에서 사용할 model 객체도 함께 할당되어 있는 것을 볼 수 있다.
지금까지 살펴본 DispatcherServlet 클래스가 요청을 처리하는 과정을 요약하면 다음과 같다.
- Front Controller Pattern 에서 Front Controller 역할을 하는 DispatcherServlet 으로 HTTP 요청이 들어온다.
- 요청에 대한 정보를 분석 한다. (파일 처리 요청인지 등)
- (등록 된 HandlerMapping 으로) 요청을 처리 할 수 있는 handler method 를 찾는다.
- handler method 를 실행할 수 있는, 처리할 수 있는 adapter 를 찾는다. (HandlerAdapter)
- 실제로 요청을 처리 한다.
- 리턴 값을 확인하여 처리 할 ReturnValueHandler 를 찾는다.
- handler method 에 @ResponseBody 가 있다면 HTTP Response Body 에 내용을 담는다.
- view 를 반환할 경우 해당 view 를 찾고 ModelAndView 객체에 결과를 담는다.
- 결과를 반환 한다.
그럼 마지막으로 DispatcherServlet 이 IoC Container 와 어떻게 연결되어 있는지 알아본다.
지금까지 web.xml 에 등록 된 DispatcherServlet 클래스를 사용했고, 이 클래스가 제공하는 handler mapping, handler adapter, view resolver 등 을 사용해서 annotation 기반으로 controller 를 등록해서 사용할 수 있었다.
그럼 이 handler 들은 어디서 온건지 확인해보자. 사실 앞쪽에서 이미 잠깐 보고 지나오긴 했었다.
DispatcherServlet 클래스를 살펴보면 initStrategies 라는 메소드가 있다.
/**
* This implementation calls {@link #initStrategies}.
*/
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
주석에도 설명이 되어 있지만, DispatcherServlet 이 사용 하는 전략들을 등록하는 과정이다.
initStrategies 메소드를 어떻게 호출하는지 따라가 보았다.
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Override
public final void init() throws ServletException {
// 생략 ...
initServletBean();
}
// 생략 ...
protected void initServletBean() throws ServletException {
}
}
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
@Override
protected final void initServletBean() throws ServletException {
// 생략 ...
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
// 생략 ...
}
protected WebApplicationContext initWebApplicationContext() {
// 생략 ...
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
// 생략 ...
return wac;
}
protected void onRefresh(ApplicationContext context) {
// For subclasses: do nothing by default.
}
}
생략한 내용이 많이 있지만, 결국 DispatcherServlet 도 Servlet 이었고, HttpServlet 클래스의 init 메소드가 실행될 때 전략을 초기화 하도록 되어 있다는 것을 확인할 수 있다.
전략을 등록하는 과정은 비슷하기 때문에 대표로 initViewResolver 에 대해서만 조금 자세히 살펴보자.
/**
* Initialize the ViewResolvers used by this class.
* <p>If no ViewResolver beans are defined in the BeanFactory for this
* namespace, we default to InternalResourceViewResolver.
*/
private void initViewResolvers(ApplicationContext context) {
this.viewResolvers = null;
if (this.detectAllViewResolvers) {
// Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
Map<String, ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.viewResolvers = new ArrayList<>(matchingBeans.values());
// We keep ViewResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.viewResolvers);
}
}
else {
try {
ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
this.viewResolvers = Collections.singletonList(vr);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default ViewResolver later.
}
}
// Ensure we have at least one ViewResolver, by registering
// a default ViewResolver if no other resolvers are found.
if (this.viewResolvers == null) {
this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No ViewResolvers declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
코드를 보면 등록 된 bean 중에 ViewResolver 타입의 클래스가 있는지 확인하여 모든 bean 을 List<ViewResolver> 타입의 변수 viewResolver 에 추가한다.
그런데 만약 31라인의 null 체크에서 걸린다면 즉, 임의로 등록한 ViewResolver 가 없다면 getDefaultStrategies 메소드로 기본 적략들을 가지고 와서 viewResolver 변수에 추가한다.
그럼 이 default 전략은 어디서 가져올 수 있었을까?
실제로 기본 전략을 가져오는 것 같은 getDefaultStratrgies 메소드를 찾아가보면 다음과 같이 property 파일을 참조하는 것을 볼 수 있다.
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Unresolvable class definition for DispatcherServlet's default strategy class [" +
className + "] for interface [" + key + "]", err);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}
3라인에서 property 파일의 내용을 참조 하고, 5라인 에서 콤마로 구분하여 문자열 배열을 만든다.
그리고 반복문을 실행 하면서 java reflection 기술로 클래스 파일을 읽어와 기본 전략을 생성하는 것을 볼 수 있다.
기본 전략을 가져올 property 파일의 경로는 다음과 같이 정의되어 있다.
/**
* Name of the class path resource (relative to the DispatcherServlet class)
* that defines DispatcherServlet's default strategy names.
*/
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
이 파일을 열어보면 다음과 같이 기본 적략이 미리 정의되어 있는 것을 볼 수 있다.
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
기본 전략을 사용하지 않고 임의로 정의한 ViewResolver 를 사용해보자.
현재 ViewResolver 의 기본전략으로 InternalResourceViewResolver 클래스를 사용하고 있다.
같은 타입의 객체를 bean 으로 등록하되 몇가지 설정을 추가해서 view 를 반환하는 코드를 조금 더 간결하게 만들어 보려 한다.
web.xml 에 등록 되어 있는 DispatcherServlet 클래스를 사용해서 Servlet WebApplicationContext 를 만들 때 사용하는 설정 파일인 MyWebConfig 파일에 ViewResolver 를 bean 으로 등록해보자.
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.view.InternalResourceViewResolver;
@Configuration
@ComponentScan
public class MyWebConfig {
@Bean
public ViewResolver myCustomViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
나만의 커스텀 ViewResolver 를 만들었는데, view 객체를 매핑할 때 prefix 와 suffix 값을 추가로 설정 하였다.
그럼 이제부터 view 를 반환할 때 다음과 같이 중복되는 내용을 제거하고 조금 더 간결하게 작성할 수 있다.
@GetMapping("/sample")
public String sample() {
return "sample";
}
앞서 return "/WEB-INF/sample.jsp"; 라고 작성 했을 때보다 typo 위험을 줄일 수 있다.
debugger 모드로 서버를 실행하여 initViewResolver 내부 코드를 한줄씩 실행해보면 bean 으로 등록한 내가 만든 ViewResolver 타입의 myCustomViewResolver 클래스를 찾았고, viewResolvers 변수에 추가하는 것을 확인할 수 있다.
그러면 760 라인에서 더이상 이 값을 null 이 아니기 때문에 ViewResolver 에 대한 기본 전략은 추가하지 않는다.
실행 결과 정상적으로 결과가 출력 되는 것도 확인할 수 있다.
'Archive > Spring Web MVC' 카테고리의 다른 글
2.1 Spring Web MVC 설정 @EnableWebMvc, WebMvcConfigurer (0) | 2021.07.04 |
---|---|
1.5 Spring MVC 동작 원리 마무리 (0) | 2021.06.28 |
1.3 Spring MVC 연동 (Root & Servlet ApplicationContext) (0) | 2021.06.24 |
1.2 Spring IoC Container 연동 (0) | 2021.06.24 |
1.1 MVC 와 Legacy Servlet Application (0) | 2021.06.22 |