WAS의 역할은 HTTP 메시지를 처리하는 것이다.

그렇다면 TCP/IP 소켓 연결, HTTP 파싱 등의 로직은 모든 WAS가 필수적, 공통적으로 구현해야할 것이다.
이러한 맥락에서 HTTP 요청/응답을 처리하는 공통 로직을 추상화하고 비즈니스 로직 구현에만 집중할 수 있도록 등장한 기술이 Servlet이다.

톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다. 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리한다. 그리고 이 서블릿 객체는 싱글톤으로 관리된다.
서블릿 컨테이너는 동시 요청을 처리하기 위해 멀티 쓰레드 처리를 지원한다. 또한 JSP도 서블릿으로 변환되어 사용한다.

서블릿을 사용해 HTTP 요청, 응답을 처리하는 흐름은 다음과 같다.

Servlet - JSP - MVC - Front Controller 순서로 발전하는 MVC pattern을 살펴보며 Spring MVC가 왜, 어떻게 Front Controller 패턴으로 구현되었는지 알아보자.
HTTP를 추상화하여 request, response 객체를 편리하게 사용할 수 있게 되었으나, 동적인 HTML을 반환해야 하는 경우에는 매우 번거로운 코드를 작성해야 한다는 단점이 남아있다.
// Servlet에서는 순수 노가다로 HTML을 작성해야 한다..
PrintWriter w = response.getWriter();
w.write("<html>");
w.write("<head>");
w.write(" <meta charset=\"UTF-8\">");
w.write(" <title>Title</title>");
w.write("</head>");
w.write("<body>");
w.write("<a href=\"/index.html\">메인</a>");
w.write("<table>");
w.write(" <thead>");
w.write(" <th>id</th>");
w.write(" <th>username</th>");
w.write(" <th>age</th>");
w.write(" </thead>");
w.write(" <tbody>");
서버에서 동적인 HTML을 보다 편리하게 만들기 위해 템플릿 엔진이 등장한다. 템플릿 엔진의 고전격인 JSP는 HTML에 Java 코드를 그대로 작성하듯 사용할 수 있다. JSP는 서버 내부에서 서블릿으로 변환된다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%
MemberRepository memberRepository = MemberRepository.getInstance();
List<Member> members = memberRepository.findAll();
%>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/index.html">메인</a>
<table>
<thead>
<th>id</th>
<th>username</th>
<th>age</th>
</thead>
<tbody>
<%
for (Member member : members) {
out.write(" <tr>");
out.write(" <td>" + member.getId() + "</td>");
out.write(" <td>" + member.getUsername() + "</td>");
out.write(" <td>" + member.getAge() + "</td>");
out.write(" </tr>");
}
%>
</tbody>
</table>
</body>
</html>
앞서 살펴본 서블릿과 JSP 코드는 뷰와 비즈니스 로직이 모두 들어가있다는 한계가 있다. 이는 유지보수에 많은 어려움을 야기했으며 뷰와 로직 간 느슨한 결합을 위해 MVC 패턴이 등장하게 된다.

Controller
HTTP 요청을 받아 파라미터를 검증하고 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해 모델에 담는다.
Model
뷰에 출력할 데이터를 담아둔다. 모델 덕분에 뷰는 비즈니스 로직을 몰라도 되고 화면을 렌더링하는 일에 집중할 수 있다.
View
모델의 데이터를 사용해 화면을 그리는 일에 집중한다.
컨트롤러의 중복 코드
모든 컨트롤러에 다음과 같은 중복 코드를 작성해야 한다.
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);String viewPath = "/WEB-INF/views/new-form.jsp";공통 로직 처리의 불편함
애플리케이션이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 로직이 증가할 것이다. 단순히 공통 기능을 메서드로 분리한다고 해결되지 않는다. 모든 컨트롤러에서 하나하나 그 메서드를 호출해야 하기 때문.

이러한 문제를 해결하기 위해서는 컨트롤러 호출 전 공통 기능을 처리하는 수문장 역할이 필요하다. 프론트 컨트롤러 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다. (입구를 하나로!)
스프링 웹 MVC의 핵심도 이 프론트 컨트롤러에 있다. 스프링 웹 MVC의 DispatcherServlet이 프론트 컨트롤러 패턴으로 구현되어 있다.

프론트 컨트롤러 패턴의 구조는 다소 복잡하게 느껴질 수 있다. 프론트 컨트롤러 패턴을 단계적으로 구현해나가며 각 요소가 어떤 문제를 해결하기 위해 존재하는지 알아보자.
우선 가장 간단한 구조를 구현해보자. 프론트 컨트롤러가 요청을 받아 URL에 해당하는 컨트롤러 구현체를 찾아 호출하는 방식이다.

프론트 컨트롤러가 구현체에 상관없이 로직을 호출할 수 있도록 인터페이스를 만든다. 이제 프론트 컨트롤러는 컨트롤러 인터페이스만 알면 컨트롤러 로직을 호출할 수 있다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
컨트롤러 구현체는 다음과 같다.
public class MemberSaveControllerV1 implements ControllerV1 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
FrontControllerServletV1
프론트 컨트롤러에서는 컨트롤러 구현체와 URL이 매핑된 Map을 하나 두고, 요청받은 URL에 해당하는 컨트롤러 구현체의 로직을 호출한다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private final Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
각 컨트롤러 구현체를 보면 뷰로 이동하는 부분에 중복이 있다.
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
이 부분을 처리하는 별도의 View 객체로 분리해 다음과 같은 구조로 개선해보자.

MyView
MyView 객체는 viewPath를 사용해 JSP를 호출하는 역할만 수행하는 단순한 객체이다.
@AllArgsConstructor
public class MyView {
private String viewPath;
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
ControllerV2
그리고 컨트롤러가 뷰를 반환하도록 인터페이스를 수정한다.
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
MemberSaveControllerV2
구현체는 다음과 같다.
public class MemberSaveControllerV2 implements ControllerV2 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
request.setAttribute("member", member);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
FrontControllerServletV2
이제 프론트 컨트롤러에서 MyView.render()를 호출해 일관된 처리가 가능하도록 개선했다.
@WebServlet(name = "frontControllerServiceV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
private final Map<String, ControllerV2> controllerMap = new HashMap<>();
public FrontControllerServletV2() {
controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV2.service");
String requestURI = request.getRequestURI();
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
MyView view = controller.process(request, response);
view.render(request, response);
}
}
HTTP 요청을 직접 받는건 프론트 컨트롤러이다. 따라서 컨트롤러 구현체가 서블릿에 의존성을 가질 필요는 없어보인다. 서블릿 종속성을 제거하기 위해 Model을 도입해보자.
그리고 컨트롤러에서 지정하는 viewPath에 중복이 있다. 컨트롤러가 직접 viewPath를 사용하는 대신 뷰의 논리 이름을 사용하게 해보자.

ModelView
Model 역할을 수행할 ModelView 클래스이다. attribute name과 데이터 객체를 Map으로 저장하며 viewPath도 저장한다.
@Getter
@Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
ControllerV3
이제 컨트롤러는 서블릿에 전혀 의존하지 않는다. 파라미터 정보를 받아 Model을 반환하는 역할만 수행한다.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
MemberSaveControllerV3
컨트롤러는 모델을 반환할 때 viewPath가 아닌 뷰의 논리 이름을 반환한다.
이를 실제 viewPath로 변환하고 JSP를 호출하는 것은 프론트 컨트롤러가 수행할 것이다.
public class MemberSaveControllerV3 implements ControllerV3 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
FrontControllerServletV3
프론트 컨트롤러에는 다음과 같은 기능이 추가된다.
createParamMapviewResolver이제 컨트롤러는 더이상 서블릿에 의존하지 않으며 실제 경로 대신 논리 이름을 사용할 수 있게 되었다.
@WebServlet(name = "frontControllerServiceV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private final Map<String, ControllerV3> controllerMap = new HashMap<>();
public FrontControllerServletV3() {
controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV3.service");
String requestURI = request.getRequestURI();
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames()
.asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
V3만 해도 충분히 잘 만들어진 프레임워크이다. 하지만 보다 개발 편의성을 높여보자.
컨트롤러에서 모델을 생성하고 반환할 필요 없이 뷰의 논리 이름만 반환하도록 해보자.

ControllerV4
컨트롤러가 뷰의 논리 이름만 반환하도록 인터페이스를 수정한다.
대신 모델 객체는 프론트 컨트롤러에서 주입하고 컨트롤러에서 모델을 채우도록 한다.
public interface ControllerV4 {
// @return viewName
String process(Map<String, String> paramMap, Map<String, Object> model);
}
MemberSaveControllerV4
모델 객체 생성 없이 뷰의 논리 이름만 반환하는 단순한 컨트롤러가 되었다.
public class MemberSaveControllerV4 implements ControllerV4 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
model.put("member", member);
return "save-result";
}
}
FrontControllerServletV4
프론트 컨트롤러에서 모델 객체를 생성해 컨트롤러 호출 시 주입해준다.
컨트롤러는 모델에 데이터를 채우고 뷰의 논리 이름을 반환한다.
뷰 리졸버를 통해 논리 이름을 실제 viewPath를 가진 뷰 객체로 변환한다.
뷰 객체의 render 메서드 호출 시 모델을 전달한다.
@WebServlet(name = "frontControllerServiceV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
private final Map<String, ControllerV4> controllerMap = new HashMap<>();
public FrontControllerServletV4() {
controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV4.service");
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames()
.asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
스프링 Web MVC를 사용해보면 @Controller, @RestController 등 다양한 타입의 컨트롤러 구현체가 있는 것을 볼 수 있다. 그런데 지금까지 우리가 만든 프론트 컨트롤러는 한가지 타입의 컨트롤러만 호출할 수 있다.
이러한 문제를 해결하기 위해 어댑터 패턴을 사용한다.
💡 지금부터 컨트롤러 ⇒ 핸들러 로 명칭을 변경한다.

HTTP 요청이 들어오면 URL에 맞는 핸들러를 조회하는 것까지는 동일하다.
그런데, 조회한 핸들러의 타입이 무엇인지는 아직 알 수 없다. 여러개의 핸들러 타입을 사용하기 때문이다.
핸들러의 타입을 결정하고 호출하기 위해 어댑터를 사용한다.
MyHandlerAdapter
어댑터는 각 핸들러에 맞게 구현된다. 즉, 어댑터의 타입도 여러개가 될 수 있다는 뜻이다. 하지만 컨트롤러 입장에서는 하나의 인터페이스에 의존하고 일관된 방법으로 로직을 호출하는 것이 좋다.
이를 위해 핸들러 어댑터용 인터페이스를 구현한다.
핸들러를 호출하는 handle 메서드는 반드시 모델을 반환하도록 했다. 만약 핸들러가 모델을 반환하지 않고 뷰의 논리 이름만 반환한다면 어댑터에서 모델을 생성해서라도 반환해야 한다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
ControllerV3HandlerAdapter
핸들러 어댑터의 구현체이다.
핸들러의 타입을 확인해주는 support 메서드, 핸들러 로직을 실행해주는 handle 메서드가 구현되어 있다.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof ControllerV3;
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
return controller.process(paramMap);
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames()
.asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
ControllerV4HandlerAdapter
ControllerV4를 처리하는 핸들러 어댑터이다.
ControllerV4는 뷰의 논리 이름만 반환하므로 핸들러 어댑터에서 뷰를 생성해 반환한다.
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof ControllerV4;
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames()
.asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
FrontControllerServletV5
HandlerAdapter.handle() 메서드로 핸들러를 호출한다.render 메서드를 호출해 JSP를 실행한다.@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
return handlerAdapters.stream()
.filter(adapter -> adapter.supports(handler))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("handler adapter not found. handler = " + handler));
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
Object controller = handlerMappingMap.get(requestURI);
return controller;
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
정리
이렇게 구현한 프론트 컨트롤러 패턴은 다음과 같은 역할을 수행한다.