'스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 수업을 듣고 정리한 내용입니다.
서블릿, JSP, MVC 패턴에서 사용한 MVC패턴의 한계점인 공통 처리가 힘들다.
이 부분을 해결하기 위해 프론트 컨트롤러 패턴을 사용한다고 했다.
FrontController
이다.DispatcherServlet
도 FrontController
패턴으로 구현되어 있다.
- 프론트 컨트롤러를 단계적으로 도입해보자.
- 목표 : 기존 코드를 최대한 유지하면서, 프론트 컨트롤러를 도입하는 것
- 먼저 구조를 맞추어두고 점진적으로 리팩터링 해보자.
V1 구조
main.java.hello.servlet.web.frontcontroller.v1.ControllerV1
package hello.servlet.web.frontcontroller.v1;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
이제 이 인터페이스를 구현한 컨트롤러를 만들어보자. 지금 단계에서는 기존 로직을 최대한 유지하는게 핵심이다.
(1) MemberFormControllerV1 - 회원 등록 컨트롤러
main.java.hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1
package hello.servlet.web.frontcontroller.v1.controller;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.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);
}
}
(2) MemberSaveControllerV1 - 회원 저장 컨트롤러
main.java.hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1
package hello.servlet.web.frontcontroller.v1.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MemberSaveControllerV1 implements ControllerV1 {
private 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);
// Model에 데이터를 보관한다.
request.setAttribute("member",member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
(3) MemberListControllerV1 - 회원 목록 조회 컨트롤러
main.java.hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1
package hello.servlet.web.frontcontroller.v1.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class MemberListControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
ControllerV1
을 구현한 세 컨트롤러 모두 내부 로직은 기존에 만든 MvcMember 서블릿들과 유사하다.
이제 이를 프론트 컨트롤러를 만들어서 이 컨트롤러들을 호출할 수 있도록 만들어주자.
main.java.hello.servlet.web.frontcontroller.v1.FrontControllerServletV1
package hello.servlet.web.frontcontroller.v1;
import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerV1Map = new HashMap<>();
public FrontControllerServletV1() {
controllerV1Map.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); // "/front ~ "가 호출되면 new MemberFormControllerV1 실행
controllerV1Map.put("/front-controller/v1/members/save", new MemberSaveControllerV1()); // "/front ~ "가 호출되면 new MemberSaveControllerV1 실행
controllerV1Map.put("/front-controller/v1/members", new MemberListControllerV1()); // "/front ~ "가 호출되면 new MemberListControllerV1 실행
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
// /front-controller/v1/members -> new MemberListControllerV1()가 반환된다.
String requestURI = request.getRequestURI(); // frontControllerServletV1 주소
ControllerV1 controller= controllerV1Map.get(requestURI);
// controllerV1Map 3개 중 선택된 것이 반환된다.
// 없을 시 null을 반환한다.
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request,response);
}
}
프론트 컨트롤러 분석
(1) urlPatterns
urlPatterns = "/front-controller/v1/*"
: /front-controller/v1
를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다./front-controller/v1
/front-controller/v1/a
/front-controller/v1/a/b
(2) controllerMap
(3) service()
getRequestURI()
메서드로 요청 URI를 얻은 뒤 controllerMap
에서 컨트롤러를 찾는다. (만약 없다면, 404(SC_NOT_FOUND)
상태 코드를 반환한다.)controller.process(request, response);
을 호출해서 해당 컨트롤러 로직을 실행한다.
(4) JSP
JSP는 이전 MVC에서 사용했던 것을 그대로 사용한다.
실행
http://localhost:8080/front-controller/v1/members/new-form
http://localhost:8080/front-controller/v1/members
기존 서블릿, JSP로 만든 MVC와 동일하게 실행되는 것을 확인할 수 있다.
모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
➡️ 기존에 컨트롤러에서 바로 JSP
를 forward
해주는 과정이 사라지고 컨트롤러는 MyView
라는 뷰 인터페이스를 반환하면 컨트롤러에서는 MyView
인터페이스의 render()
메서드를 호출함으로써 해당 인터페이스에서 JSP
로 forward
하도록 구조가 변경되었다.
(1) MyView
java.main.web.frontcontroller.MyView
package hello.servlet.web.frontcontroller;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyView {
public String viewPath; // "/WEB-INF/views/new-form.jsp"
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);
}
}
➡️ 컨트롤러에서 JSP forward하는 코드를 모듈화 하기 위해 만든 인터페이스로 클래스 생성시 전달한 jsp 경로를 viewPath
로 받아 생성되며 render()
메서드 호출시 인자로 받은 request
, response
를 인자로 jsp forward를 한다.
(2) ControllerV2
java.main.web.frontcontroller.v2.ControllerV2
package hello.servlet.web.frontcontroller.v2;
import hello.servlet.web.frontcontroller.MyView;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
(3) MemberFormControllerV2 - 회원 등록 폼
java.main.web.frontcontroller.v2.controller.MemberFormControllerV2
package hello.servlet.web.frontcontroller.v2.controller;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
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");
}
}
dispatcher.forward()
를 직접 생성해서 호출하지 않아도 된다. 단순히, MyView
객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 된다.ControllerV1
을 구현한 클래스와 ControllerV2
를 구현한 클래스를 비교해보면, 이 부분의 중복이 확실하게 제거된 것을 확인할 수 있다.
(4) MemberSaveControllerV2 - 회원 저장
package hello.servlet.web.frontcontroller.v2.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MemberSaveControllerV2 implements ControllerV2 {
private 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);
// Model에 데이터를 보관한다.
request.setAttribute("member",member);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
(5) MemberListControllerV2 - 회원 목록
package hello.servlet.web.frontcontroller.v2.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class MemberListControllerV2 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
return new MyView("/WEB-INF/views/members.jsp");
}
}
(6) 프론트 컨트롤러 V2
package hello.servlet.web.frontcontroller.v2;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
// /front-controller/v2/members/new-form
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
private Map<String, ControllerV2> controllerV2Map = new HashMap<>();
public FrontControllerServletV2() {
controllerV2Map.put("/front-controller/v2/members/new-form", new MemberFormControllerV2()); // "/front ~ "가 호출되면 new MemberFormControllerV2 실행
controllerV2Map.put("/front-controller/v2/members/save", new MemberSaveControllerV2()); // "/front ~ "가 호출되면 new MemberSaveControllerV2 실행
controllerV2Map.put("/front-controller/v2/members", new MemberListControllerV2()); // "/front ~ "가 호출되면 new MemberListControllerV2 실행
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV2.service");
// /front-controller/v2/members -> new MemberListControllerv2()가 반환된다.
String requestURI = request.getRequestURI(); // frontControllerServletv2 주소
ControllerV2 controller= controllerV2Map.get(requestURI);
// controllerv2Map 3개 중 선택된 것이 반환된다.
// 없을 시 null을 반환한다.
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// new MyView("/WEB-INF/views/new-form.jsp");
MyView view = controller.process(request, response);
view.render(request, response);
}
}
MyView myView = controller.process(request, response);
MyView
객체를 반환타입으로 반환받는다. view.render()
를 호출하면 forward
로직을 수행해서 JSP가 실행된다.ControllerV2
의 반환 타입이 MyView
이므로 프론트 컨트롤러는 컨트롤러의 호출 결과로 MyView
를 반환 받는다.
MyView.render()
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
MyView
객체의 render()
를 호출하는 부분을 모두 일관되게 처리할 수 있다.MyView
객체를 생성만 해서 반환하면 된다.
실행
http://localhost:8080/front-controller/v2/members/new-form
http://localhost:8080/front-controller/v2/members
뷰 이동 로직을 분리했다면 이번에는 서블릿의 종속성 및 뷰 이름이 중복되는 것도 제거해보자.
Request
, Response
객체를 인자값으로 전달해주었다.Model
을 Request
객체의 Attribute
로 사용하기에 생기는 일이다.Model
객체를 만들어서 사용하면 어떨까?
viewPath
를 지정해줄 때, 상위 경로와 접미사가 중복된다.)/WEB-INF/views/new-form.jsp
➡️ new-formWEB-INF/views/save-result.jsp
➡️ save-result/WEB-INF/views/members.jsp
➡️ members
MyView
객체의 render()
메서드를 호출해 JSP로 forward
해주었다면, V3 버전에서는 그전에 viewResolver
를 호출해 MyView
를 반환하도록 했다.MyView
가 아니라 ModelView
객체를 반환해주도록 바뀌었다.ModelView
HttpServletRequest
를 사용했다.request.setAttribute()
를 통해 데이터를 저장하고 뷰에 전달했다.Model
을 직접 만들고, 추가로 View
의 이름까지 전달하는 ModelView
객체를 만들어보자❗️HttpServletRequest
를 사용할 수 없다. 따라서 직접 request.setAttribute()
를 호출할 수도 없다. 따라서 Model
이 별도로 필요하다.ModelView
객체는 다른 버전에서도 사용하므로 패키지를 frontcontroller
에 둔다.
(1) ModelView
java.main.hello.servlet.web.frontcontroller.MyView
package hello.servlet.web.frontcontroller;
import java.util.HashMap;
import java.util.Map;
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
model
객체를 가지고 있다.model
은 단순히 map
으로 되어있으므로 컨트롤러에서 뷰에 필요한 데이터를 key, value
로 넣어주면 된다.view
의 논리 이름을 저장하는 viewName
과 뷰에 필요한 데이터를 담는 Model
을 Map
으로 구현했다.
(2) ControllerV3
java.main.hello.servlet.web.frontcontroller.v3.ControllerV3
package hello.servlet.web.frontcontroller.v3;
import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
Request
, Response
객체를 인자값으로 받지 않기에 서블릿 기술로부터 종속성이 사라졌다. (서블릿 기술을 전혀 사용하지 않았다.)HttpServletRequest
가 제공하는 파라미터는 프론트 컨트롤러가 paramMap
에 담아서 호출해주면 된다.Model
데이터를 포함하는 ModelView
객체를 반환하면 된다.
(3) MemberFormControllerV3 - 회원 등록 폼
package hello.servlet.web.frontcontroller.v3.controller;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import org.springframework.ui.Model;
import java.util.Map;
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
ModelView
를 생성할 때 new-form
이라는 view
의 논리적인 이름을 지정한다. (viewResolver
에 의해 물리명으로 조합된다.)
(4) MemberSaveControllerV3 - 회원 저장(가입)
package hello.servlet.web.frontcontroller.v3.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import java.util.Map;
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.get("username");
: 파라미터 정보는 map에 담겨있다. map에서 필요한 요청 파라미터를 조회하면 된다.Model
의 담고 싶은 데이터는 ModelView
객체의 Model
필드에 담도록 한다.mv.getModel().put("member", member);
: 모델은 단순한 map
이므로 모델에 뷰에서 필요한 member
객체를 담고 반환한다.
(5) MemberListControllerV3 - 회원 목록 조회
package hello.servlet.web.frontcontroller.v3.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import java.util.List;
import java.util.Map;
public class MemberListControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}
(6) FrontControllerServletV3
package hello.servlet.web.frontcontroller.v3;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
// /front-controller/v3/members/new-form
@WebServlet(name = "frontControllerServletv3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private Map<String, ControllerV3> controllerv3Map = new HashMap<>();
public FrontControllerServletV3() {
controllerv3Map.put("/front-controller/v3/members/new-form", new MemberFormControllerV3()); // "/front ~ "가 호출되면 new MemberFormControllerv3 실행
controllerv3Map.put("/front-controller/v3/members/save", new MemberSaveControllerV3()); // "/front ~ "가 호출되면 new MemberSaveControllerv3 실행
controllerv3Map.put("/front-controller/v3/members", new MemberListControllerV3()); // "/front ~ "가 호출되면 new MemberListControllerv3 실행
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// /front-controller/v3/members -> new MemberListControllerv3()가 반환된다.
String requestURI = request.getRequestURI(); // frontControllerServletv3 주소
ControllerV3 controller= controllerv3Map.get(requestURI);
// controllerv3Map 3개 중 선택된 것이 반환된다.
// 없을 시 null을 반환한다.
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paraMap = createParamMap(request);
ModelView mv = controller.process(paraMap);
// new-form
String viewName = mv.getViewName();// 논리 이름 new-form
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> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paraName->paraMap.put(paraName, request.getParameter(paraName)));
return paraMap;
}
}
HashMap<String, String> paramMap = createParamMap(request);
: HttpServletRequest
에서 파라미터 정보를 꺼내서 Map
으로 변환한다.
ModelView mv = controller.process(paramMap);
: 해당 Map(paramMap)
을 컨트롤러에 전달하면서 호출한다.
view.render(mv.getModel(), request, response)
: 코드에서 컴파일 오류가 발생할 것이다. MyView
객체에 필요한 메서드를 추가하자❗️
💡 뷰 리졸버
(1)MyView view = viewResolver(viewName)
- 컨트롤러가 반환하는 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는
MyView
객체를 반환한다.- 논리 뷰 이름 :
members
- 물리 뷰 경로 :
/WEB-INF/views/members.jsp
(2)
view.render(mv.getModel(), request, response)
- 뷰 객체를 통해서 HTML 화면을 렌더링 한다.
- 뷰 객체의
render()
는 모델 정보도 함께 받는다.- JSP는
request.getAttribute()
로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서request.setAttribute()
로 담아둔다.- JSP로 포워드 해서 JSP를 렌더링 한다.
(7) MyView
package hello.servlet.web.frontcontroller;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class MyView {
public String viewPath; // "/WEB-INF/views/new-form.jsp"
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 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));
// 변수명 key, value를 통해 map의 루프를 돌린다.
// request에 값을 다 담는다.
}
}
render(mv.getModel(), request, response)
에 대응하는 메서드를 오버로딩
실행
등록 : http://localhost:8080/front-controller/v3/members/new-form
목록 : http://localhost:8080/front-controller/v3/members
- 앞서 만든 V3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다.
- 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 보면, 항상
ModelView
객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다.- 좋은 프레임워크는 아키텍처도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다.
ModelView
를 반환하지 않고, ViewName
만 반환한다.
java.main.hello.web.frontcontroller.v4.ControllerV4
package hello.servlet.web.frontcontroller.v4;
import java.util.Map;
public interface ControllerV4 {
/**
*
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
viewName
만 그대로 반환되도록 구현하였다.ControllerV4
구현 컨트롤러는 따로 ModelView
객체를 생성할 필요 없이, viewName
만 반환해주면 된다. (viewName : 뷰 이름)
package hello.servlet.web.frontcontroller.v4.controller;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.Map;
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
new-form
이라는 뷰의 논리 이름만 반환
package hello.servlet.web.frontcontroller.v4.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.Map;
public class MemberSaveControllerV4 implements ControllerV4 {
private 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";
}
}
model.put("member",member)
: 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.
package hello.servlet.web.frontcontroller.v4.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import java.util.List;
import java.util.Map;
public class MemberListControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
ControllerV4
를 구현하는 3개의 컨트롤러 모두 뷰의 논리 이름(form
, save-result
, members
)만 반환하면 된다.model
이 인자값으로 넘어오기 때문에 모델에 담아야 할 데이터는 인자값으로 넘어온 model Map
에 담으면 된다.
package hello.servlet.web.frontcontroller.v4;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
// /front-controller/v4/members/new-form
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
private Map<String, ControllerV4> controllerv4Map = new HashMap<>();
public FrontControllerServletV4() {
controllerv4Map.put("/front-controller/v4/members/new-form", new MemberFormControllerV4()); // "/front ~ "가 호출되면 new MemberFormControllerV4 실행
controllerv4Map.put("/front-controller/v4/members/save", new MemberSaveControllerV4()); // "/front ~ "가 호출되면 new MemberSaveControllerV4 실행
controllerv4Map.put("/front-controller/v4/members", new MemberListControllerV4()); // "/front ~ "가 호출되면 new MemberListControllerV4 실행
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// /front-controller/v4/members -> new MemberListControllerv4()가 반환된다.
String requestURI = request.getRequestURI(); // frontControllerServletv4 주소
ControllerV4 controller= controllerv4Map.get(requestURI);
// controllerv4Map 3개 중 선택된 것이 반환된다.
// 없을 시 null을 반환한다.
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paraMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); // 추가
String viewName = controller.process(paraMap, 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> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paraName->paraMap.put(paraName, request.getParameter(paraName)));
return paraMap;
}
}
모델 객체 전달
Map<String, Object> model = new HashMap<>();
: 컨트롤러 구현체에 전달할 Model
을 생성해준다. 각 구현체 컨트롤러에서는 여기서 전달한 Model
에 데이터를 담아 전달할 수도 있다. 그렇기 때문에 ModelView
객체를 생성하지 않아도 된다.뷰의 논리 이름을 직접 반환
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
실행
http://localhost:8080/front-controller/v4/members/new-form
http://localhost:8080/front-controller/v4/members
📝 정리
- 이번 버전의 컨트롤러는 매우 단순하고 실용적이다.
- 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 적은 아이디어를 적용했을 뿐인데, 컨트롤러를 구현하는 개발자 입장에서 보면 이제 군더더기 없는 코드를 작성할 수 있다.
- 또한 중요한 사실은 여기까지 한 번에 온 것이 아니라는 점이다. 프레임워크가 점진적으로 발전하는 과정 속에서 이런 방법도 찾을 수 있었다.
- 프레임워크나 공통 기능이 수고로워야 사용하는 개발자가 편리해진다.
Controller
를V1 ~ V4
까지 다양한 방법으로 만들어서 회원관리 웹 애플리케이션을 구현해보았다.
만약, 이 컨트롤러들이 혼용되서V1 ~ V4
를 모두 사용해야한다면 어떻게 해야하는걸까?
ex) 만약 어떤 개발자는 ControllerV3
방식으로 개발하고 싶고, 어떤 개발자는 ControllerV4
방식으로 개발하고 싶다면 어떻게 해야할까?
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
process
메서드만 봐도 반환타입과 인자 값의 개수, 타입이 다르다.
💡 어댑터 패턴
- 지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.
ControllerV3
,ControllerV4
는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다.- 마치
v3
는 110v이고,v4
는 220v 전기 콘셉트 같은 것이다. 이럴 때 사용하는 것이 바로 어댑터이다.- 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자.
V5 구조
✏️ 핸들러 어댑터와 핸들러
- 핸들러 어댑터 : 프론트 컨트롤러와 핸들러(컨트롤러) 사이에 핸들러 어댑터가 추가되었는데, 이 핸들러의 어댑터 역할을 하기 때문에 핸들러 어댑터이다. 이 어댑터 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
- 핸들러 : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경하였다. 이유는 이제 어댑터가 있기 때문에 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.
어댑터(용) 인터페이스
frontcontroller.v5.MyHandlerAdapter
package hello.servlet.web.frontcontroller.v5;
import hello.servlet.web.frontcontroller.ModelView;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
boolean supports(Object handler)
➡️handler
는 컨트롤러를 말한다.
➡️ 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드이다.
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler
➡️ 어댑터는 실제 컨트롤러를 호출하고, 그 결과로ModelView
를 반환해야 한다.
➡️ 실제 컨트롤러가ModelView
를 반환하지 못하면, 어댑터가ModelView
를 직접 생성해서라도 반환해야 한다.
➡️ 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다. (컨트롤러 호출되는 위치가 프론트컨트롤러에서 어댑터로 변경되었다.)
package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class ControlllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
// MemberFormControllerV3
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
// MemberFormControllerV3
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paraName->paraMap.put(paraName, request.getParameter(paraName)));
return paraMap;
}
}
하나씩 분석해보기❗️
@Override
public boolean supports(Object handler) {
// MemberFormControllerV3
return (handler instanceof ControllerV3);
}
Controller
의 자식이거나 구현체인지 판단해 반환한다. (ControllerV3
을 처리할 수 있는 어댑터)
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
// MemberFormControllerV3
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
Object
타입이기에 ControllerV3
타입으로 형변환을 해줘야 한다.supports()
를 통해 ControllerV3
의 구현체인 것을 확인했으니 타입 변환은 걱정없이 실행해도 된다.ControllerV3
는 반환타입이 ModelView
이기에 바로 로직 수행 후 ModelView
를 반환하면 된다.
package hello.servlet.web.frontcontroller.v5;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.adapter.ControlllerV3HandlerAdapter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// /front-controller/v5/* 하위로 와라
// /front-controller/v5/v3/members/new-form
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
// 최상위 object이므로 어떤 객체도 담을 수 있다.
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// ex)
// MemberFormControllerV3가 반환된다.
Object handler = getHandler(request);
// controllerv3Map 3개 중 선택된 것이 반환된다.
// 없을 시 null을 반환한다.
if (handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// ControllerV3HandlerAdapter 반환됨
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
// new-form
String viewName = mv.getViewName();// 논리 이름 new-form
MyView view = viewResolver(viewName);
view.render(mv.getModel(),request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI(); // frontControllerServletv3 주소
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler){
// MemberFormControllerV3
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private boolean initHandlerAdapters() {
return handlerAdapters.add(new ControlllerV3HandlerAdapter());
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); // "/front ~ "가 호출되면 new MemberFormControllerv3 실행
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); // "/front ~ "가 호출되면 new MemberSaveControllerv3 실행
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); // "/front ~ "가 호출되면 new MemberListControllerv3 실행
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
컨트롤러(Controller) → 핸들러(Handler)
이전에는 컨트롤러를 직접 매핑해서 사용했다. 그런데 이제는 어댑터를 사용하기 때문에, 컨트롤러 뿐만 아니라 어댑터가 지원하기만 하면, 어떤 것이라도 URL에 매핑해서 사용할 수 있다. 그래서 이름을 컨트롤러에서 더 넓은 범위 핸들러로 변경했다.
생성자
(1) initHandlerMappingMap();
➡️ urlPattern에 매핑되는 컨트롤러를 초기화해주는 로직 메서드이다.
➡️ 핸들러를 담는 맵의 value type이 Object인 이유는 담길 핸들러가 V1 ~ V4 중 어느 핸들러 인터페이스일지 모르기 때문이다.
(2) initHandlerAdapters();
➡️ 사용할 어댑터를 리스트 콜렉션에 추가하는 작업을 하는 로직 메서드
매핑 정보
private final Map<String, Object> handlerMappingMap = new HashMap<>();
➡️ 매핑 정보의 값이 ControllerV3
, ControllerV4
같은 인터페이스에서 아무 값이나 받을 수 있는 Object
로 변경되었다.
핸들러 매핑
Object handler = getHandler(request)
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI(); // frontControllerServletv3 주소
return handlerMappingMap.get(requestURI);
}
➡️ 핸들러 매핑 정보인 handlerMappingMap
에서 URL을 key로 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환한다.
핸들러를 처리할 수 있는 어댑터 조회
MyHandlerAdapter adapter = getHandlerAdapter(handler)
private MyHandlerAdapter getHandlerAdapter(Object handler){
// MemberFormControllerV3
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
➡️ handler
를 처리할 수 있는 어댑터를 adapter.supports(handler)
를 통해서 찾는다.
➡️ 콜렉션에 등록된 어댑터를 순회하며 해당 핸들러가 지원되는지 확인한 후 지원하는 어댑터가 있다면 반환한다. (handler가 ControllerV3
인터페이스를 구현했다면, ControllerV3HandlerAdapter
객체가 반환된다.)
어댑터 호출
ModelView mv = adapter.handle(request, response, handler);
➡️ 어댑터의 handle(request, response, handler)
메서드를 통해 실제 어댑터가 호출된다.
➡️ 어댑터는 handler
(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환한다.
➡️ handler
는 로직에서 자신이 지원하는 핸들러로 형변환해서 사용을 마친 뒤 필요한 값을 반환한다.
➡️ ControllerV3HandlerAdapter
의 경우 어댑터의 모양과 컨트롤러의 모양이 유사해서 변환 로직이 단순하다.
실행 결과
http://localhost:8080/front-controller/v5/v3/members/new-form
http://localhost:8080/front-controller/v5/v3/members
📝 참고
지금은 V3 컨트롤러를 사용할 수 있는 어댑터와ControllerV3
만 들어 있어서 크게 감흥이 없을 것이다.ControllerV4
를 사용할 수 있도록 기능을 추가해보자.
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter()); // 기능 추가
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3()); // "/front ~ "가 호출되면 new MemberFormControllerv3 실행
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3()); // "/front ~ "가 호출되면 new MemberSaveControllerv3 실행
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3()); // "/front ~ "가 호출되면 new MemberListControllerv3 실행
// v4 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4()); // "/front ~ "가 호출되면 new MemberFormControllerv4 실행
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4()); // "/front ~ "가 호출되면 new MemberSaveControllerv4 실행
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4()); // "/front ~ "가 호출되면 new MemberListControllerv4 실행
}
ControllerV4
경로(key)와 컨트롤러를 handlerMappingMap
에 추가해준다.ControllerV4
도 호환이 되도록 해주는 어댑터(ControllerV4HandlerAdapter
)를 handlerAdapter
콜렉션에 추가해준다.
package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
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);
HashMap<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> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paraName->paraMap.put(paraName, request.getParameter(paraName)));
return paraMap;
}
}
하나씩 분석해보자❗️
handler
가 ControllerV4
인 경우에만 처리하는 어댑터
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
실행 로직
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
➡️ handler
를 ControllerV4
로 케스팅 하고, paramMap
, model
을 만들어서 해당 컨트롤러를 호출한다.
➡️ ControllerV4
는 반환타입이 String으로 ModelView
가 아니라 viewName
만 반환한다.
createParamMap
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paraName->paraMap.put(paraName, request.getParameter(paraName)));
return paraMap;
}
createParamMap
: paramMap
객체를 생성하고, request
에 담긴 파라미터들을 iterator
를 통해서 담고 return 해준다.
어댑터 변환
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
➡️ (중요) 어댑터가 호출하는 ControllerV4
는 뷰의 이름을 반환한다.(viewName)
➡️ 그런데 어댑터는 뷰의 이름이 아니라 ModelView
를 만들어서 반환해야 한다.
➡️ 어댑터가 필요한 이유 : ControllerV4
는 뷰의 이름을(viewName) 반환했지만, 어댑터는 이것을 ModelView
을 만들어서 형식을 맞추어 반환한다.
어댑터와 ControllerV4
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
public interface MyHandlerAdapter {
ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException;
}
실행
http://localhost:8080/front-controller/v5/v4/members/new-form
http://localhost:8080/front-controller/v5/v4/members
📌 정리
(1) v1: 프론트 컨트롤러를 도입
- 기존 구조를 최대한 유지하면서 프론트 컨트롤러를 도입
(2) v2: View 분류
- 단순 반복 되는 뷰 로직 분리
(3) v3: Model 추가
- 서블릿 종속성 제거
- 뷰 이름 중복 제거
(4) v4: 단순하고 실용적인 컨트롤러
- v3와 거의 비슷
- 구현 입장에서
ModelView
를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공한다.(5) v5: 유연한 컨트롤러
- 어댑터 도입
- 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계
애노테이션을 사용해서 컨트롤러를 편리하게 사용할 수 있게 하려면?
- 애노테이션을 지원하는 어댑터를 추가하면 된다.
- 다형성과 어댑터 덕분에 기존 구조를 유지하면서, 프레임워크의 기능을 확장할 수 있다.
참고