본문 바로가기
개발/스프링부트

서블릿 MVC 와 스프링 MVC 비교하기

by hamcheeseburger 2022. 6. 4.

Servlet 환경의 MVC를 살펴보고, 스프링과 비교해보자.

Servlet

  • Dynamic Web Page를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술
  • Tomcat : 자바 서블릿 컨테이너 = 서블릿을 갖고 있고 서블릿을 적절히 관리하는 애로 생각하면 된다!

Servlet 주기

  • init()
    • 톰캣이 서블릿이 생성되면 init() 메소드를 실행하여 서블릿을 초기화함
    • 서블릿 생성 시 최초 1번 실행
  • service()
    • request를 처리하는 메소드
  • destroy()
    • 서블릿을 제거할 때 실행되는 메소드
    • 보통 서버가 종료될 때 실행됨

 

Servlet 주기 테스트

- 톰캣 설정파일에 Servlet 등록

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" id="WebApp_ID" version="2.4">
	<display-name>newmadamequery</display-name>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>
	
	<servlet>
		<servlet-name>CorinneServlet</servlet-name>
		<servlet-class>controller.CorinneServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>CorinneServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
</web-app>

Servlet Container에 어떤 Servlet을 사용할 것인지 등록하는 작업이다.

controller 패키지 아래 있는 CorinneServlet이라는 클래스를 사용할 것이며, '/'로 끝나는 url은 해당 서블릿이 처리할 것이라는 맵핑이 이루어지고 있다.

 

- HttpServlet 상속

package controller;

import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CorinneServlet extends HttpServlet {

    @Override
    public void init() {
        System.out.println("Init Servlet!");
        // 요청에 필요한 요소들을 초기화
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) 
    	throws ServletException, IOException {
        System.out.println("In service");
        // 요청에 맞는 Controller를 찾고 로직 실행
    }

    @Override
    public void destroy() {
    	super.destroy();
        System.out.println("Destroy Servlet!");
    }
}

이제 Servlet으로 MVC를 만들어 보자

package controller;

import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CorinneServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    
    private ControllerMapping controllerMapping;

    @Override
    public void init() {
    	// 요청 Uri, Method에 맞는 Controller 정보를 가지고 있는 RequestMapping을 초기화
        controllerMapping = new ControllerMapping();
        controllerMapping.initMapping();
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) 
    	throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        
        // 요청의 Method와 Uri를 구한다.
        String method = request.getMethod();
        String contextPath = request.getContextPath();
        String servletPath = request.getServletPath();
        
        // RequestMapping 에서 요청 Uri, Method에 해당하는 Controller를 불러온다.
        Controller controller = controllerMapping.findController(method, servletPath);
        try {
        
        	// controller를 실행하고, 이동할 View uri를 불러온다.
            String uri = controller.execute(request, response);
            
            // uri가 redirect라면 해당 uri로 redirect 시킨다.
            if (uri.startsWith("redirect:")) {	
            	String targetUri = contextPath + uri.substring("redirect:".length());
            	response.sendRedirect(targetUri);        
            }
            // uri가 view uri라면 해당 view로 이동시킨다.
            else {
            	RequestDispatcher requestDispatcher = request.getRequestDispatcher(uri);
                requestDispatcher.forward(request, response);
            }                   
        } catch (Exception e) {
            throw new ServletException(e.getMessage());
        }
    }

    @Override
    public void destroy() {
        super.destroy();
    }
}

 

package controller;

import java.util.HashMap;
import java.util.Map;

public class ControllerMapping {

    private final Map<String, Controller> mappings = new HashMap<>();

    public void initMapping() {
        mappings.put("GET:/main", new ForwardController("/user/mainPage.jsp"));
        mappings.put("GET:/user", new ForwardController("/user/login.jsp"));
        mappings.put("POST:/user", new LoginController());
    }

    public Controller findController(String method, String uri) {
        final String key = method + ":" + uri;
        return mappings.get(key);
    }
}

RequestDispatcher는 뭘까?

  • 원하는 자원으로 이동해주는 역할을 함
  • ServletContext 내에 포함되어 있기 때문에 특정 서블릿 내에 있는 자원으로만 이동할 수 있음

Spring Container

  • Spring은 Servlet을 활용한 프레임워크
  • 위에서 확인한 CorinneServlet 처럼 Spring은 DispatcherServlet을 가지고 있다.
  • web.xml
<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	//..	
</servlet>
<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>
  • SpringBoot에서는 위의 설정이 없어도 DispatcherServletAutoConfiguration으로 자동 설정됨

DispatcherServlet의 동작 과정

  • 요청이 전송 되면 HandlerMapping으로 URL과 매칭되는 HandlerAdapter 검색
  • 매칭된 HandlerAdapter로 컨트롤러를 실행하여 ModelAndView를 반환받음
  • 이 응답을 처리할 ViewResolver를 찾아 View를 찾고 저장된 Model를 request에 저장함
  • 해당 View 파일(ex. jsp)로 request, response를 전달
HttpServlet DispatcherServlet
init() onRefresh()
service() doService()
destroy() 부모인 FrameworkServlet의 destroy() 사용

onRefresh() == init() 동작 과정

  • resolver, handlerMappings, handlerAdapters 를 초기화한다.
  • dispatcherServlet은 HttpServletBean의 자손이다.
  • DispatcherServlet에는 init()을 구현하지 않고, HttpServletBean의 init() 메소드를 사용한다.
@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);
}

doService() == service() 의 동작 과정

요청이 들어왔을 때 실행된다.

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // ...
    try {
    	doDispatch(request, response);
    }
    finally {
        // ...	
    }
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {				
    // handelrMappings 에서 request에 맞는 Handler를 찾아옴
    HandlerExecutionChain mappedHandler = getHandler(processedRequest);
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    applyDefaultViewName(processedRequest, mv);
    mappedHandler.applyPostHandle(processedRequest, response, mv);

    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

@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;
}

⚠️ 간략히 추린 코드입니다.

HandlerAdapter는 HandlerMapping에서 반환한 Handler 객체에 맞는 Adapter를 찾는다. 

여기서 잠깐! HandlerMapping은 왜 Controller를 반환하지 않고 Handler를 반환할까?

  • @Controller 를 사용하지 않고 자신이 직접 만든 클래스를 이용해서 클라이언트의 요청을 처리할 수도 있기 때문
  • DispatcherServlet 입장에서는 클라이언트 요청을 처리하는 객체의 타입이 반드시 @Controller 를 적용한 타입일 필요는 없음
  • 이러한 이유로 스프링 MVC는 웹요청을 실제로 처리하는 객체를 Handler 라고 표현함
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

    // ...
    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    //...
}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // ...
    View view;
    String viewName = mv.getViewName();
    if (viewName != null) {
        view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
    }
    else {
        view = mv.getView();
    }

    view.render(mv.getModelInternal(), request, response);
}

스프링 5 입문 프로그래밍을 보면 ModelAndView에 포함된 Model을 request 객체에 setAttribute() 한다고 나와있다.

해당 코드를 확인하고자 View 객체들을 찾아봤다. 해당 코드를 InternalResourceView에서 찾을 수 있었다.

InternalResourceView

InternalResourceView는 JSP 혹은 같은 웹애플리케이션 내에 있는 다른 자원들의 Wrapper이다. RequestDispatcher의 forward()나 include()를 이용하며 다른 서블릿을 실행해서 그 결과를 현재 서블릿의 결과로 사용하거나 추가하는 방식이다.

@Override
protected void renderMergedOutputModel(
        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    exposeModelAsRequestAttributes(model, request);

    // ...
    RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);

    if (useInclude(request, response)) {
        rd.include(request, response);
    }

    else {
        rd.forward(request, response);
    }
}

protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

    model.forEach((name, value) -> {
        if (value != null) {
            request.setAttribute(name, value);
        }
        else {
            request.removeAttribute(name);
        }
    });
}

exposeModelAsRequestAttributes()의 코드를 보면 model 객체들을 request에 set하는 모습을 찾을 수 있다.

 

destroy()

Servlet에서 사용한 자원을 destroy하는 작업을 수행한다.

@Override
public void destroy() {
    getServletContext().log("Destroying Spring FrameworkServlet '" + getServletName() + "'");
    // Only call close() on WebApplicationContext if locally managed...
    if (this.webApplicationContext instanceof ConfigurableApplicationContext && !this.webApplicationContextInjected) {
        ((ConfigurableApplicationContext) this.webApplicationContext).close();
    }
}

해당 코드에서는 WebApplicationContext를 close하는 것을 확인할 수 있다.

간단 비교

CorinneServlet DispatcherServlet
ControllerMapping HandlerMapping
Controller Handler
RequestDispatcher View

 

이전 댓글