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 |
'개발 > 스프링부트' 카테고리의 다른 글
@SpringbootApplication의 baseScanPackage 설정 이슈 (0) | 2024.07.13 |
---|---|
@Transactional 사용시 주의점(feat.AOP,Proxy) (4) | 2022.11.13 |
@Component, @Controller, @Service, @Repository의 차이 (8) | 2022.05.08 |
[Spring Data Jpa] Native query DTO Projection (0) | 2021.07.25 |
이전 댓글