아래와 같이 뷰(View)화면을 위한 동적인 HTML
을 만드는 작업이 자바 코드에 섞여서, 코드가 지저분하고 복잡해지는 문제가 발생한다.
@WebServlet(name="memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance(); // 싱글톤
@Override
protected void service(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);
//응답(동적 html)
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
"</head>\n" +
"<body>\n" +
"성공\n" +
"<ul>\n" +
" <li>id="+member.getId()+"</li>\n" +
" <li>username="+member.getUsername()+"</li>\n" +
" <li>age="+member.getAge()+"</li>\n" +
"</ul>\n" +
"<a href=\"/index.html\">메인</a>\n" +
"</body>\n" +
"</html>");
}
}
이렇게 자바 코드로 HTML
을 만드는 것은 매우 비효율적이다. 이를 해결하기 위해 나온 것이 템플릿 엔진
인데, 템플릿 엔진은 HTML
문서에 동적으로 변경해야 하는 부분만 자바 코드로 변경이 가능하도록 한 것이다.
뷰를 생성하는 HTML
작업에만 집중하고, 중간에 동적으로 변경이 필요한 부분에만 자바 코드를 적용하는 방식이다. 참고로 템플릿 엔진 중에는 지금은 거의 사용되지 않아 권장하지 않고, 대신 타임리프를 사용하는 추세다.
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
//request,response 사용 가능
MemberRepository memberRepository = MemberRepository.getInstance();
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
//비즈니스 로직
Member member = new Member(username, age);
memberRepository.save(member);
%>
<html>
<head>
<title>Title</title>
</head>
<body>
성공
<ul>
<li>id=<%=member.getId()%></li>
<li>username=<%=member.getUsername()%></li>
<li>age=<%=member.getAge()%></li>
</ul>
</body>
</html>
<% ~~ %>
: 자바 코드 입력 가능한 부분서블릿과 JSP
만으로 비즈니스 로직 + 뷰 렌더링을 한꺼번에 처리하면 유지보수가 힘들어지는 문제가 발생한다. 화면과 어플리케이션 로직은 변경의 Life Cycle 다르기 때문이다. 즉, UI 수정과 비즈니스 로직을 수정하는 일은 서로에게 영향이 없어야 한다.
따라서 MVC 패턴은 서블릿이나, JSP로 처리하던 것을 Model
, View
, Controller
세 가지로 역할을 나누어 분리하고 있다.
HTTP
요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행하는 역할을 한다. 또한 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다. 주의할 점은, 비즈니스 로직은 컨트롤러가 아닌, Service 라는 계층에서 별도로 처리해야 한다는 것이다.
몸델에 뷰에 출력하거나 필요한 데이터를 모두 담아둔다. 이로 인해 뷰는 오직 화면 렌더링 역할에만 집중이 가능해지는 것이다.
모델에 담겨있는 데이터를 사용해서 화면을 그리는 역할에만 집중한다. (HTML 생성 역할)
서블릿을 Controller
로 사용하고, JSP
를 View
로, HttpServletRequest
객체를 Model
로 사용하여 MVC 패턴을 적용해보자.
request.setAttribute()
: 데이터 보관request.getAttribute()
: 데이터 조회@WebServlet(name="mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response); //다른 서블릿, JSP 로 이동하는 함수
}
}
dispatcher.forward
함수는 다른 서블릿이나 JSP 뷰로 이동하는 함수이며, 서버 내부에서 다시 호출이 일어나게 된다. 실제 클라이언트에게 응답이 나갔다가 다시 서버로 돌아와 URL 이 달라지는 redirect
와 다른 것에 주의하자.
🔖 WEB-INF
외부에서 직접적으로JSP
파일을 경로로 접근 불가능하도록 하는WAS
의 원칙이다. 따라서/WEB_INF
폴더 아래 뷰 파일들은controller(servlet)
통해서만 호출이 가능하다.
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(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);
//Model에 데이터를 보관
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
setAttribute()
로 request
객체에 데이터를 보관해서 뷰에 전달하고, request.getAttribute()
로 뷰에서 데이터 꺼내면 된다. 꺼낼 때는 JSP에서 제공하는 기능을 활용한다면 더 간소화된 ${member.id}
형태로도 변경이 가능하다.
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] -->
<form action="save" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
View
로 이동하는 코드가 중복 호출되며, ViewPath
의 중복 등 코드의 공통 처리가 힘들다.HttpServletRequest
, HttpServletResponse
를 인자로 받지만 사용하지 않아 불필요한 코드가 발생한다.따라서, 컨트롤러 즉 서블릿 호출전에 먼저 공통 기능들을 처리하는 것이 필요해지는데 바로 프론트 컨트롤러 패턴을 도입하여 해결할 수 있다. 프론트 컬트롤러 패턴은 스프링 MVC의 핵심이 된다. (필터 체인이랑은 다른 개념이다.)
프론트 컨트롤러(Servlet) 하나로 클라이언트의 요청을 받고, 프론트는 요청에 맞는 컨트롤러를 찾아서 호출하는 패턴이다. 참고로 Spring MVC
의 DispatcherServlet
도 프론트 컨트롤러 패턴으로 구현되어 있다.
공통의 관심사를 앞에서 한곳으로 모으기 때문에 앞서 말한 코드 중복의 문제들을 해결할 수 있어진다. 또한 프론트를 제외한 나머지 컨트롤러는 서블릿 사용하지 않아도 되므로 불필요한 코드가 생기는 문제도 해결이 가능해진다.
기존 구조를 최대한 유지하면서, 프론트 컨트롤러 도입해본다. 우선 프론트 컨트롤러가 URL
매핑 정보에서 컨트롤러를 조회해서 적절한 컨트롤러를 호출하는 기능을 추가해보자.
🔖 컨트롤러 인터페이스
public interface ControllerV1 { void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; }
프론트 컨트롤러는 해당 인터페이스를 호출해서 구현과 관계 없이 로직의 일관성을 유지할 수 있다.
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
@WebServlet(name="frontControllerServletV1", urlPatterns = "/front-controller/v1/*" )
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>(); // URL 매핑 정보 저장
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-from", 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); // 다형성
}
}
/front-controller/v1
를 포함한 하위의 모든 요청은 해당 서블릿에서 받아들일 수 있도록 하였다. 또한, requestURI
를 조회해서 실제 호출할 컨트롤러를 MAP
에서 찾은 후, 다형성을 통한 process()
를 통해 실제 컨트롤러를 호출시킨다.
모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 존재하기 때문에, 별도로 뷰 처리하는 객체 생성하여 단순 반복 되는 뷰 렌더링 로직을 분리시킨다.
🔖 Controller 인터페이스
public interface ControllerV2 { MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; }
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);
}
}
public class MemberSaveControllerV2 implements ControllerV2 {
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
memberRepository.save(member);
request.setAttribute("member", member);
return new MyView("/WEB-INF/views/save-result.jsp"); // 뷰 경로 저장
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
MyView view = controller.process(request, response);
view.render(request,response); // JSP로 렌더링 하는 forward 로직 수행
}
다음으로 1. Servlet
종속성을 제거하고, 2. 뷰 이름의 중복을 제거하도록 구조를 변경해보자.
HttpServletRequest
대신 맵으로 요청 파라미터 정보를 넘기고 request
객체가 아닌 별도의 모델을 만들어서 반환하면 완벽하게 서블릿 종속성을 제거할 수 있다. 또한, 컨트롤러는 뷰의 논리 이름만 반환하고 프론트 컨트롤러에 물리 경로를 추가하는 복잡한 로직을 위임하면 뷰 이름의 중복 제거가 가능해진다.
이번에 새롭게 도입할 ModelView
는 바로 위의 두 기능을 대체할 객체이며, 뷰 이름과 뷰에 전달할 Model
데이터를 포함한다.
🔖 Controller 인터페이스
public interface ControllerV3 { ModelView process(Map<String, String> paramMap); //서블릿에 종속적 X }
HttpServletRequest
가 제공하는 파라미터를, 프론트 컨트롤러에서paramMap
에 담아서 호출한다.
@Getter
@Setter
public class ModelView {
private String viewName; // view 이름
private Map<String,Object> model = new HashMap<>(); // 뷰 렌더링 시 필요한 model 객체
public ModelView(String viewName) {
this.viewName = viewName;
}
}
model
: 뷰를 렌더링 할때 필요한 데이터를 (key,value)형태로 저장한다.public class MemberSaveControllerV3 implements ControllerV3 {
private 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;
}
}
paramMap
: 요청 파라미터의 정보가 담겨 있음save-result
: view의 논리적 이름으로 지정, 물리적 이름은 Front Controller에서 처리@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
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");
}
모든 파라미터 정보 가져와서 Map
에 담는다.
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;
}
맨 앞의 Front 컨트롤러에서만 servlet
에 종속적이기 때문에, 여기서 모든 파라미터 정보를 Map
에 담아 넘겨줘야 한다. 나머지 컨트롤러에서는 뷰이름 + 렌더링 시 필요한 데이터
가 담긴 ModelView
를 반환하는 역할만 수행한다.
public class MyView {
...
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key,value));
}
}
항상 ModelView
를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공해보자.
뷰 렌더링을 위한 모델 데이터가 들어있는 맵을 이전에는 컨트롤러에서 생성했다면, 프론트 컨트롤러에서 미리 생성해서 전달하고 컨트롤러는ViewName
만 반환하면 ModelView
란 객체 자체를 생성할 일이 없어진다.
🔖 Controller Interface
public interface ControllerV4 { // 모델 객체가 외부에서 전달 String process(Map<String,String> paramMap, Map<String, Object> model); }
public class MemberSaveControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
...
//변경 부분 : 모델 객체를 생성하지 않는다.
model.put("member", member);
return "save-result"; //뷰 이름만 반환
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model); //파라미터와 함께 model(빈 객체)도 함께 전달
MyView view = viewResolver(viewName); //뷰 논리 이름을 직접 반환하기 때문에 바로 사용
view.render(model, request, response);
}
어댑터를 도입하여, 프레임워크를 유연하고 확장성 있게 설계해보자. 즉 프론트 컨트롤러가 한가지 방식이 아닌, 다양한 방식의 컨트롤러를 처리 가능하도록 변경하는 것이다.
각 핸들러에 맞는 어댑터를 만들어놓고,
🔖 Handler Adapter 인터페이스
public interface MyHandlerAdapter { boolean supports(Object handler); //handler == controller ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; }
supports()
:Adapter
가 해당 컨트롤러를 처리 할 수 있는지 판단하는 메서드이다.handle()
: 프론트 컨트롤러가 아닌 Adapter가 실제 컨트롤러를 호출하고, 결과로ModelView
반환한다.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) { //ControllerV3 인터페이스를 구현했는지 체크
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);
ModelView mv = controller.process(paramMap); //Adapter에서 실제 컨트롤러 호출
return mv; //ModelView 반환
}
private Map<String, String> createParamMap(HttpServletRequest request) {
...
}
}
URL
에 매핑해서 사용이 가능하다. (더 넓은 범위)@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
//모든 컨트롤러 지원(ControllerV4 -> Object)
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-from", 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-from", 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); //1.핸들러 매핑
if(handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler); //2.핸들러 처리 가능한 어댑터 조회
ModelView mv = adapter.handle(request, response, handler); //3.실제 어댑터 호출(controller.process()과정)
String viewName = mv.getViewName();
MyView view = viewResolver(viewName); //4.논리 -> 물리 주소 변환
view.render(mv.getModel(),request,response); //5.뷰 렌더링(모델뷰 같이 전달)
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
MyHandlerAdapter a;
for (MyHandlerAdapter adapter : handlerAdapters) {
if(adapter.supports(handler)){
return adapter;
}
}
throw new IllegalArgumentException("handler adapter 를 찾을 수 없습니다. handlers" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
getHandler()
: URL에 매핑된 핸들러 객체 찾아서 반환getHandlerAdapter()
: 핸들러 처리 가능한 어댑터 찾기스프링 MVC 역시 프론트 컨트롤러 패턴으로 구현되어 있으며, 따라서 가장 핵심은 앞서서의 프론트 컨트롤러라 칭했던 DispatcherServlet
이다. DisplatcherServlet
역시 HttpServlet
을 상속받고 있다.
단, 스프링 부트는 내장 톰캣을 띄우면서 자바 코드로 DispatcherServlet
을 서블릿으로 등록하는 과정을 처리해버리기 때문에 @WebServlet
어노테이션을 통해 서블릿을 직접 등록하지 않는다.
또한 등록하면서 모든 경로에 대해서 매핑해놓지만 자세한 것이 우선순위가 높으므로 우리가 기존에 구현했던 매핑 정보에 영향이 없다.
handlerMappingMap
->HandlerMapping
(Interface)MyHandlerAdapter
->HandlerAdapter
(Interface)ModelView
->ModelAndView
viewResolver
->ViewResolver
(Interface)MyView
->View
(Interface)
Handler
역시 구현체가 많고, View
와 같은 경우도 JSP
/ Thymleaf
등 구현체가 여러개가 될 수 있으니 쉽게 확장할 수 있도록 모두 인터페이스로 제공하고 있는 것을 볼 수 있다.
스프링 MVC의 모든 동작이 DispacherServlet.doDispatch()
메소드에서 실행된다.
핸들러 조회
: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회 mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
핸들러 어댑터 조회
: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
핸들러 어댑터 실행
: 핸들러 어댑터를 실행핸들러 실행
: 핸들러 어댑터가 실제 핸들러를 실행ModelAndView 반환
: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
viewResolver 호출
: 뷰 리졸버를 찾고 실행 View view;
String viewName = mv.getViewName();
View 반환
: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환 view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
뷰 렌더링
: 뷰를 통해서 뷰를 렌더링 view.render(mv.getModelInternal(), request, response);
컨트롤러의 호출에는 두 가지가 필요하다.
HandlerMapping
: 핸들러 매핑에서, 해당 컨트롤러를 찾을 수 있어야한다.HandlerAdapter
: 핸들러 매핑을 통해 찾은 핸들러를 실행 가능한 어댑터가 필요하다.그렇다면, 핸들러 매핑과 어댑터 구현체들에는 어떤 것들이 있고 어떻게 동작하는지 스프링이 제공했던 과거 버전의 컨트롤러(핸들러)를 통해 더 자세하게 알아보자.
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
@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;
}
}
스프링 빈의 이름으로 URL
을 등록하고 있는 것을 볼 수 있다.
우선, 핸들러를 찾아야 한다. 핸들러를 찾는 여러 가지 구현체 중에 중요한 두 가지이며, 우선순위 순서대로 나열했다.
RequestMappingHandlerMapping
: 애노테이션 기반의 컨트롤러인 @RequestMapping
에서 사용한다.BeanNameUrlHandlerMapping
: 스프링 빈의 이름으로 핸들러를 찾는다.앞선 예제에서는 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하기 때문에 BeanNameUrlHandlerMapping
이 사용될 것이며, 만약 주로 사용하는 @RequestMapping
을 통해 핸들러를 등록해주었다면 RequestMappingHandlerMapping
이 사용될 것이다.
다음은 각 핸들러를 처리할 수 있도록 하는 핸들러 어댑터 구현체들이다.
RequestMappingHandlerAdapter
: 애노테이션 기반의 컨트롤러인 @RequestMapping
에서 사용한다.HttpRequestHandlerAdapter
: HttpRequestHandler
를 처리한다.SimpleControllerHandlerAdapter
: Controller
인터페이스(애노테이션X, 과거에 사용)를 처리한다. 앞선 예제에서는 Controller
인터페이스를 구현했으니 HandlerAdpater
의 supports()
실행 중 SimpleControllerHandlerAdapter
가 대상 어댑터로 지정되고, OldController
를 내부에서 실행할 것이다.
ViewResolver
는 뷰의 논리 이름을 물리 이름으로 반환하는 역할을 한다. 그리고 스프링 부트는 InternalResourceViewResolver
라는 뷰 리졸버를 자동으로 등록해준다.
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return new ModelAndView("new-form");
}
application.properties
에 등록한 spring.mvc.view.prefix
, spring.mvc.view.suffix
설정 정보를 사용해서 빈으로 등록된다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
BeanNameViewResolver
: 빈 이름으로 뷰를 찾아서 반환한다. InternalResourceViewResolver
: JSP
를 처리할 수 있는 뷰를 반환한다. 해당 예제에서는 new-form
으로 된 빈이 없으니, InternalResourceViewResolver
가 실행된다. 반환된 InternalResourceView
는 JSP
처럼 forward()
를 호출해서 처리할 수 있는 경우에 사용한다.
🔖 참고 사항
Thymeleaf
뷰 템플릿을 사용하면ThymeleafViewResolver
를 등록해야 하지만, 스프링 부트에서는 라이브러리만 추가하면 이런 작업을 모두 자동화해준다.
@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;
}
}
@Controller
에는 다음의 두 가지 기능이 존재한다. 따라서 @Component
+ @RequestMapping
를 사용하던지, @Controller
하나만 사용하던지 같은 기능이다.
Controller
로 인식하기 때문에 RequestMappingHandlerMapping
이 인식할 수 있어진다.@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
//@RequestMapping(value = "/new-form", method = RequestMethod.GET)
@GetMapping("/new-form")
public String newForm(){
return "new-form";
}
@PostMapping("/save")
public String save(
//request.getParameter("username")
@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";
}
}
Model 파라미터
를 사용해서 모델을 생성하지 않고 인자로 전달받고, ModelAndView
객체가 아닌 뷰의 논리 이름을 반환하고 있다.
HTTP 요청 파라미터를 받을 수 있는 @RequestParam
을 사용함으로써 메서드 인자에 HttpServletRequest
을 없앨 수 있었고, HTTP 메서드 구분을 위해 내부에 @RequestParam
를 가지고 있는 @GetMapping, @PostMapping
로 바꾸어서 더욱 간결하게 코드를 변경하였다.