서블릿
WAS를 직접 구현하려면, 텍스트로 이루어진 HTTP 메세지를 파싱하는 과정과 어플리케이션 로직 처리 결과를 HTTP 메세지로 생성하여 응답을 보내는 과정을 전부 수행해야한다.
HTTP 요청에서 데이터를 파싱하고, 어플리케이션 로직 결과를 응답 HTTP 메세지로 만드는 과정은 매 요청과 응답마다 동일하고 반복되는 작업이다. 서블릿은 이런 반복되는 과정을 자동화하여 개발자가 의미있는 어플리케이션 로직만 작성할 수 있도록 만들어준다.
이와 같이 서블릿을 사용하면 동적 리소스를 만들 때 요청과 응답의 흐름을 간단한 메서드 호출만으로 다룰 수 있게 만들어, HTTP 스펙을 매우 편리하게 사용할 수 있고 비즈니스 로직에만 집중할 수 있게 만들어 준다.
아래 코드와 같이 class에 service를 재정의하면 해당 url로 들어오는 HTTP 요청과 응답은 HttpServletRequest와 HttpServletResponse로 간편하게 처리할 수 있다.
톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라 부르고, 서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료(서블릿 객체의 생명주기)를 관리한다.
모든 요청마다 새로 객체를 생성하는 것은 비효율적이기 때문에, 모든 서블릿 객체는 최초 로딩 시점에 생성해두고 재활용하는 싱글톤 패턴으로 관리된다.
서블릿 내부 구조 및 실행 순서
내장 톰캣 서버 생성
스프링 부트 실행 → 내장 톰캣 서버 실행 → 톰캣 내부 서블릿 컨테이너에서 객체 생성 순으로 실행된다.
웹 애플리케이션 서버의 요청 응답 구조
HttpServletRequest
위에서도 언급한 것처럼, 서블릿은 HTTP 요청 메세지를 편리하게 사용할 수 있도록 요청 메세지를 파싱하고 그 결과를 HttpServletRequest 객체에 담아서 제공한다.
HTTP 요청 메세지는 3가지 형태로만 사용된다.
•
GET - 쿼리 파라미터
◦
url?username=hello&age=20
◦
위의 예시처럼 메세지의 본문(body) 없이 URL의 쿼리 파라미터에 데이터를 포함해서 전달하는 방식
◦
검색, 페이징, 필터 등에서 많이 사용된다.
•
POST - HTML Form
◦
content-type: application/x-www-form-urlencoded
◦
위의 content-type으로 지정하고 메세지 본문에 쿼리 파라미터 형식(username=hello&age=20)으로 HTTP 요청
◦
회원 가입, 상품 주문 등에서 HTML Form을 사용한다.
•
REST API - HTTP message body에 데이터를 담아서 요청
◦
content-type: json, content-type: text
◦
메세지 본문에 JSON이나 XML, TEXT 형태로 담아 HTTP 요청
◦
HTTP API라고도 불리며 최근에는 거의 JSON만 사용된다.
HttpServletRequest의 사용 방법은 다음과 같다.
// 전체 파라미터 조회
request.getParameterNames();
// 단일 파라미터 조회
request.getParameter("username");
// 이름이 같은 복수 파라미터 조회
request.getParamterValues("username");
// message body 조회 & String 변환
String message = StreamUtils.copyToString(
request.getInputStream(), StandardCharsets.UTF_8);
// String -> JSON 변환
private ObjectMapper objectMapper = new ObjectMapper();
objectMapper.readValue(message);
Java
복사
HttpServletResponse
서블릿은 HTTP 응답 메세지를 보낼 때, 편리하게 사용할 수 있도록 HttpServletResponse 객체를 통해 header나 body, 응답 코드 등 여러 편의 기능을 제공한다.
•
HTTP 응답 메세지 생성
◦
HTTP 응답코드 지정
◦
header 생성
◦
body 생성
•
편의 기능 제공
◦
Content-Type 지정
◦
쿠키 설정 및 추가
◦
Redirect
HttpServletResponse의 사용 방법은 다음과 같다.
// header 생성
response.setHeader("Content-Type", "text/plain;charset=utf-8");
response.setHeader("cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("my-header", "hello");
// 응답 코드 지정
response.setStatus(HttpServletResponse.SC_OK);
// 헤더 편의 메서드 제공
response.setContentType("text/plain");
response.setCharacterEncoding("utf-8");
// 쿠키 편의 메서드 제공
Cookie cookie = new Cookie("myCookie", "good");
cookie.setMaxAge(600);
response.addCookie(cookie);
// redirect 편의 메서드 제공
response.sendRedirect("/basic/hello-form.html");
// body 생성(text 응답 / HTML 응답)
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<body>");
writer.println(" <div>hello</div>");
writer.println("</body>");
writer.println("</html>");
// Java 객체를 통한 JSON 응답
private ObjectMapper objectMapper = new ObjectMapper();
HelloData data = new HelloData("kim", 20);
response.getWriter().write(
objectMapper.writeValueAsString(data));
Java
복사
서블릿으로 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>");
for (Member member : members) {
w.write(" <tr>");
w.write(" <td>" + member.getId() + "</td>");
w.write(" <td>" + member.getUsername() + "</td>");
w.write(" <td>" + member.getAge() + "</td>");
w.write(" </tr>");
}
w.write(" </tbody>");
w.write("</table>");
w.write("</body>");
w.write("</html>");
Java
복사
서블릿을 이용하여 HTML을 생성하면, 위처럼 중간에 비즈니스 로직을 추가하여 동적인 값을 넣어 HTML을 만들어낼 수 있다.
다만, 보면 알 수 있듯이 오타의 위험성이 굉장히 크고 코드 자체가 복잡하고 비효율적으로 되어있다. 이를 개선하기 위해 나온 것이 HTML 문서에서 필요한 곳에만 코드를 적용해 동적으로 변경하는 템플릿 엔진이다.
템플릿 엔진에는 JSP, Thymeleaf, Freemarker, Velocity 등이 있다. 최근에는 JSP는 잘 사용되지 않고 Spring과 호환이 잘 되는 Thymeleaf가 주로 사용된다.
JSP로 HTML 동적으로 생성하기
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
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>
Java
복사
JSP 문서는 항상 <%@ page contentType="text/html;charset=UTF-8" language="java" %>로 시작하고, 자바 코드를 거의 그대로 전부 사용할 수 있다.
<%@ page import="hello.servlet.domain.member.MemberRepository" %>로 자바의 import 문을 사용할 수 있고, <% ~~ %>로 자바 코드를 입력하거나 <%= ~~ %>로 자바 코드를 출력할 수 있다.
MVC 패턴의 등장
위처럼 JSP를 통해 서블릿보다 편리하게 HTML을 동적으로 생성할 수 있다. 하지만 이 방식에는 몇 가지 문제가 있다.
•
너무 많은 역할
HTML 코드와 자바 코드가 섞여있고, 비즈니스 로직이 전부 노출되어 있어 JSP가 여러 책임을 가지고 있게 된다. 해당 HTML에 비즈니스 로직이 많아진다면, 비즈니스 로직을 고치기 위해 HTML 전체를 읽어야하는 등 유지보수 측면에서 비효율적이다.
•
변경의 생명 주기
가장 중요한 문제는 HTML의 변경 할 때 비즈니스 로직을 변경 안 할 수 있고, 반대로 비즈니스 로직을 변경할 때 HTML을 변경하지 않을 수 있다. 이처럼 변경 주기가 서로 다르고 각자의 변경이 서로에게 영향을 주지 않기 때문에, 기능을 분리하여 유지보수성을 높이는 것이 좋다.
이를 개선하기 위해 비즈니스 로직과 HTML로 화면(View)를 그리는 일을 분리하는 MVC 패턴이 등장하게 되었다. MVC 패턴은 Model-View-Controller의 약자로, 애플리케이션을 세 가지 주요 구성요소로 분리하여 유지보수와 확장성을 높이는 패턴이다.
•
컨트롤러
사용자의 입력을 받아 모델의 데이터를 변경하거나 뷰의 표현을 변경한다. 모델과 뷰의 중간 다리 역할로, 사용자의 입력에 따라 어떤 동작을 수행할 지 결정한다.
•
모델
어플리케이션의 데이터와 비즈니스 로직을 담당하여, 사용자 인터페이스(UI) 없이 순수하게 데이터를 다루어 데이터를 변경하거나 처리하여 Controller와 View에 전달한다.
•
뷰
사용자에게 보여지는 부분을 담당하여, 모델에서 받은 데이터를 시각적으로 표현한다.
JSP에 MVC 패턴 적용하기
List<Member> members = memberRepository.findAll();
request.setAttrebute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispathcer.forward(request, response);
Java
복사
위처럼 dispathcer.forward()를 통해 다른 서블릿이나 JSP로 데이터를 전달할 수 있고, 이는 서버 내부적으로 호출하여 처리하기 때문에 클라이언트에서는 호출이 발생하는 것을 알 수 없다는 점에서 redirect와 차이가 있다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<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>
<c:forEach var="item" items="${members}">
<tr>
<td>${item.id}</td>
<td>${item.username}</td>
<td>${item.age}</td>
</tr>
</c:forEach>
</tbody>
</table>
</body>
</html>
Java
복사
JSP 파일에서도 위와 같이 비즈니스 로직 없이 가져온 데이터를 호출하여 표현한다.
위 코드에서는 몇 가지 문제가 있다.
•
코드 중복
위의 RequestDispatcher와 forward 메서드는 모든 컨트롤러마다 중복되어 호출된다. 또한 JSP 파일에도 파일 경로 prefix와 확장자 suffix가 지속적으로 반복된다.
•
사용하지 않는 코드
HttpServletRequest만 사용할 때도 있고 HttpServletResponse만 사용할 때도 있지만, 항상 둘 다 선언하여 매개변수로 받아야한다.
•
공통 처리가 어려움
기능이 복잡해 질수록 컨트롤러에서 처리하는 부분이 많아지고, 이를 코드 중복을 피하기 위해 공통 처리를 해야한다. 각 컨트롤러마다 처리해야하는 것이 다르기 때문에 모든 컨트롤러에서 공통으로 처리하기도 쉽지 않다.
이러한 문제들은 프론트 컨트롤러(Front Controller) 패턴을 도입하여 해결할 수 있다.
프론트 컨트롤러 패턴 적용하기
프론트 컨트롤러로 서블릿을 두고, 프론트 컨트롤러가 요청마다 맞는 컨트롤러를 찾아서 호출한다. 스프링에서는 DispatcherServlet이 위와 같은 FrontController 패턴으로 구현되어있다.
프론트 컨트롤러를 적용하여 위의 구조를 가지도록 수정해보자.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
Java
복사
이와 같이 컨트롤러를 찾을 인터페이스를 만들어두고,
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private 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 {
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
Java
복사
프론트 컨트롤러로 사용하는 서블릿에서 매핑 정보를 담은 Map을 통해 요청에 맞는 컨트롤러를 찾아 호출한다. 각 컨트롤러의 구현체에서 process()를 정의하여 각 요청에 맞는 비즈니스 로직을 처리할 수 있다.
이처럼 프론트 컨트롤러를 분리하면 반복되는 코드를 줄이고, 변경이 있을 때 코드를 수정하는 부분을 최소화 할 수 있다.
View 분리하기
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
Java
복사
프론트 컨트롤러를 적용한 코드에도, 아직 위와 같은 jsp 파일에 대한 경로나 dispatcher.forward()와 같은 코드의 중복이 있다.
이처럼 View를 분리하여 반복되는 코드를 줄여보자.
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
Java
복사
이와 같이 View를 따로 추출하고,
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
Java
복사
이렇게 비즈니스 로직 이후 컨트롤러마다 JSP 경로를 통해 View를 새로 생성 후 반환한다.
MyView view = controller.process(request, response);
view.render(request, response);
Java
복사
전달 받은 View를 통해서 요청에 맞는 JSP를 실행한다.
Model 추가하기
아직 고쳐지지 않은 문제로, HttpServletRequest와 HttpServletResponse가 사용되지 않더라도 항상 매개변수로 받아와야 한다. 또한 뷰의 경로에서 /WEB-INF/views/와 같은 폴더의 위치는 매번 반복되기도 하고, 폴더의 위치가 변경되었을 때 하나하나 전부 고쳐줘야한다는 문제가 있다.
위와 같이 ViewResolver와 Model을 추가하여 위 문제를 해결해보자.
@Getter @Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
Java
복사
이와 같은 ModelView를 추가해서 경로를 제외한 JSP의 논리 이름만 저장하고, 같이 전달할 attributes를 담을 model에 대한 Map을 추가한다.
@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;
}
Java
복사
paramMap으로 받아온 attribute를 통해 비즈니스 로직을 수행하고, 로직의 결과를ModelView의 model에 추가하여 반환한다. 이 과정에서 JSP의 논리 이름만 전달하여 ModelView를 생성한다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
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");
}
Java
복사
반환 받은 ModelView에서 ViewResolver를 통해 View를 구해 HTML을 동적으로 생성한다.
단순화 + 실용성
매번 컨트롤러에서 ModelView를 만들어 반환하는 과정이 개발자에게 불필요하게 느껴질 수 있다.
이를 개선하기 위해 위처럼 컨트롤러는 view이 논리 이름만 반환하고 프론트 컨트롤러에서 논리이름으로 ModelView를 만들도록 수정하자.
// ControllerV4
@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";
}
Java
복사
이와 같이 컨트롤러는 model을 매개변수로 받아와 비즈니스 로직에 맞춰 값을 저장하고, View의 논리이름만을 반환한다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
Java
복사
이와 같이 프론트 컨트롤러에서 model의 생성을 처리함으로써 개발자가 반복해야하는 작업을 줄여 실용성을 높일 수 있다.
컨트롤러 유연하게 사용하기
ControllerV3와 ControllerV4는 기본적으로 호환이 되지않는다. 두 컨트롤러를 모두 사용하려면 이를 별도로 처리해주어야 한다.
이처럼 핸들러와 핸들러 어댑터를 두고 다형성으로 처리하여 여러 버전의 컨트롤러를 사용할 수 있게 만들어보자.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
Java
복사
이와 같이 핸들러 어댑터를 인터페이스로 선언하여 다형성을 이용한다.
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) {
ControllerV3 controller = (ControllerV3) handler;
...
return mv;
}
}
Java
복사
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) {
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;
}
...
}
Java
복사
이와 같이 컨트롤러에 맞춰 ModelView를 반환하도록 핸들러를 작성한다.
@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_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
...
}
Java
복사
프론트 컨트롤러에서는 이와 같이 요청에 맞는 핸들러를 찾고, 해당 핸들러 어댑터에 해당 핸들러를 호출하여 요청에 맞는 비즈니스 로직을 처리한다. 이를 통해 여러 컨트롤러를 호환할 수 있도록 더 유연하게 사용할 수 있다.
스프링 MVC
지금까지 만들어왔던 구조는 위와 같다.
스프링의 실제 MVC 구조를 보면, 몇 가지 이름만 다를 뿐 만들었던 구조와 동일한 것을 알 수 있다.
DispatcherServlet
스프링에서는 프론트 컨트롤러 패턴을 DispatcherServlet이란 이름으로 구현해두었는데, org.springframework.web.servlet.DispatcherServlet를 자세히 살펴보면 HttpServlet을 상속받아 서블릿으로 동작하는 것을 알 수 있다.
스프링 부트는 DispatcherServlet을 자동으로 등록하면서 모든 경로(urlPattern=”/”)에 대해 매핑한다.
서블릿이 호출되면 DispatcherServlet의 부모에서 재정의한 service()가 호출되고, 여러 메서드들이 호출되면서 DispatcherServlet.doDispatch()가 호출된다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
ModelAndView mv = null;
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행
// -> 5. ModelAndView 반환 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
Java
복사
private void processDispatchResult(HttpServletRequest request,
HttpServletResponse response, HandlerExecutionChain mappedHandler,
ModelAndView mv, Exception exception) throws Exception {
// 뷰 렌더링 호출
render(mv, request, response);
}
Java
복사
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);
}
Java
복사
동작 순서
1.
핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회
2.
핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터(컨트롤러 인터페이스)를 조회
3.
핸들러 어댑터 실행 : 조회한 핸들러 어댑터를 실행한다.
4.
핸들러 실행 : 핸들러 어댑터(인터페이스)가 실제 핸들러(컨트롤러 구현체)를 실행한다.
5.
ModelAndView 반환 : 핸들러가 반환하는 정보를 핸들러 어댑터가 ModelAndVeiw로 변환하여 반환한다.
6.
viewResolver 호출 : ModelAndVeiw에서 뷰를 찾을 수 있게 뷰 리졸버를 찾고 실행한다.
7.
View 반환 : 뷰 리졸버에서 뷰의 논리이름을 물리 이름으로 바꾸고, 랜더링을 담당하는 뷰 객체를 반환한다.(JSP의 경우 InternalResourceView(JstlView) 반환 → forward() 호출)
8.
뷰 랜더링 : 뷰 객체를 통해 뷰를 랜더링 한다.
핸들러 매핑과 핸들러 어댑터
public interface Controller {
ModelAndView handleRequest(HttpServletRequset requset, HttpServletResponse response) throws Exception;
}
Java
복사
지금은 사용되지 않지만 현재의 @Controller 어노테이션 이전에는 이러한 Contoller 인터페이스를 통해 컨트롤러를 구현했다.
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception [
System.out.println("OldController.handleRequest");
return null;
}
}
Java
복사
이처럼 URL 이름으로 스프링 빈으로 등록하여 매핑한다. 스프링 빈으로 URL을 매핑하려면 핸들러 매핑 정보에서 스프링 빈 이름으로 핸들러를 찾을 수 있어야한다.
이는 위 순서대로 핸들러 매핑 정보와 핸들러 어댑터를 찾기 때문에 가능하다.
위의 HanlderAdapter의 우선순위를 보면 알 수 있겠지만, HttpRequestHandler 인터페이스 또한 핸들러 어댑터의 구현에 사용되었는데 이는 서블릿과 가장 유사한 형태의 핸들러이다.
@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("MyHttpRequestHandler.handleRequest");
}
}
Java
복사
심지어 이 형태는 반환타입이 void로 내부에서 view에 대한 처리를 끝내야한다.
최근에는 99.9%로 @RequestMapping 어노테이션 방식의 핸들러 매핑과 핸들러 어댑터를 사용한다. 이는 RequestMappingHandlerMapping과 RequestMappingHanderAdapter의 앞글자를 따서 만든 이름으로, @Contoller 애노테이션 기반의 컨트롤러에서 사용된다.
뷰 리졸버
application.properties나 application.yml에 설정을 통해 JSP의 경로와 확장자에 대해 설정할 수 있다.
# application.properties
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
YAML
복사
스프링 부트는 서버가 켜지면 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록하는데, 이때 위의 설정파일에서 등록한 prefix와 suffix를 읽어 등록해두고 논리 이름을 물리 이름으로 바꿀 때 사용한다.
return new ModelAndView("/WEB-INF/views/new-form.jsp");
Java
복사
권장하지 않지만 전체 경로를 지정해 ModelAndView를 반환해도 동작하기는 한다.
뷰 리졸버는 이와 같은 순서로 뷰를 찾고, 뷰 이름으로 스프링 빈을 등록한게 아니라면 InternalResourceViewResolver가 호출된다.
JSP를 사용하는 경우에는 InternalResourceViewResolver가 InternalResourceView를 반환하고 view.render()를 통해 내부적으로 forward()를 호출해 JSP를 실행한다.
스프링 애노테이션 기반의 MVC
위에서 언급했었는데, 스프링의 @Controller와 @RequestMapping 애노테이션이 생긴 이후로 거의 모든 프로젝트는 애노테이션 기반의 MVC 패턴을 사용한다.
@Controller
public class SpringMemberSaveControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
Java
복사
이처럼 애노테이션을 이용하면 아주 간단하고 깔끔하게 MVC 패턴을 사용할 수 있다.
@Controller
public class SpringMemberSaveControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members/save")
public String process(
@RequestParam("username") String username,
@RequestParam("age") int age,
Model model) {
Member member = new Member(username, age);
memberRepository.save(member);
model.addAttribute("member", member);
return "save-result";
}
}
Java
복사
이처럼 이전의 ControllerV4에서 구현해본 ViewResolver를 통한 View 랜더링 방식도 지원을 한다. model을 매개변수로 받아와 attribute를 추가하여 ModelAndView를 생성하지 않아도 되고, view의 논리 이름을 반환하면 내부적으로 viewResolver를 호출해 JSP로 넘길 수 있다.
또한 @RequestParam을 이용하여 attribute를 받아오는 과정도 request에서 꺼내거나 별도의 변환 없이 직관적이고 깔끔하게 가져올 수 있다.