📌 목차
프론트 컨트롤러 도입 - v1
View 분리 - v2
Model 추가 - v3
단순하고 실용적인 컨트롤러 - v4
유연한 컨트롤러 1 - v5
유연한 컨트롤러 2 - v5
📌 개요
이전에 jsp 와 servlet 만으로 MVC 패턴을 적용하였지만 여전히 중복코드가 발생했고 여러 문제점이 있었다.
이를 해결하기 위해 frontController 를 도입함으로써 중복코드를 제거한다. 버전은 총 5개가 있다.
먼저 v1 에서는 구조적으로 형태를 맞출것이므로 이전에 만들어둔 코드에 비해서 코드가 추가되거나 크게 변경되는 부분은 없다.
역할과 책임을 분류하면 중복된 코드를 다른코드에 영향이 안가도록 지울 수 있다.
그다음으로 v2 버전에서는 중복코드를 처리해주는 뷰를 도입함으로써 중복코드를 해결할 것이다.
v3 버전에서는 지금까지 사용해온 서블릿에 대해 종속성을 제거하고 뷰 이름에 대한 중복을 제거하면서 개선한다. 이로 인해 구현코드와 테스트코드 작성이 쉬워지고 경로의 변화나 확장자의 변화에 대해 유연하게 대처할 수 있다.
v4 버전에서는 필요없는 객체를 생성하고 반환하는 과정을 파라미터를 이용해 간략화 한다.
마지막으로 v5 버전에서는 어뎁터 패턴을 이용하여 원하는 컨트롤러 버전을 유연하게 사용할 수 있도록 구현한다.
📌 프론트 컨트롤러 도입 - v1
이번에는 프론트 컨트롤러를 도입함으로써 이를 해결해보자.
먼저 기존의 코드를 최대한 유지하면서 구조를 만들어 본다.
구조는 위 사진과 같다.
클라이언트가 http 요청을 하면 프론트 컨트롤러 라는 게이트를 거쳐가고 url 매핑정보를 가져온다음에 컨트롤러와 jsp 를 호출한다.
package hello.servlet.web.frontcontroller.v1;
public interface ControllerV1 {
void process(HttpServletRequest request , HttpServletResponse response) throws ServletException, IOException;
}
컨트롤러는 인터페이스로 구현한다. 이제 이 인터페이스를 구현한 회원등록 컨트롤러를 살펴보자.
package hello.servlet.web.frontcontroller.v1.controller;
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); // 리다이렉트가 아님
}
}
viewPath 를 선언하고 dispatcher 를 사용해서 Controller 를 통해 jsp 파일을 불러올 수 있도록 한다.
package hello.servlet.web.frontcontroller.v1.controller;
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")); // getParameter 의 반환값은 항상 String이다.
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);
}
}
회원저장 컨트롤러는 MemberRepository 를 싱글톤으로 가져오고 request 의 파라미터를 가져온뒤 , Member 객체를 만들고 저장한다.
이제 viewPath 를 선언하고 dispatcher 객체를 만들어서 jsp 파일을 호출한다.
package hello.servlet.web.frontcontroller.v1.controller;
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);
}
}
회원 리스트 목록 조회는 findAll 메서드를 통해 List 에 담고 viewPath 를 만들어준뒤 request 의 setAttribute 에 데이터를 저장하고 dispatcher 를 호출한다.
이제 frontController 를 살펴보자.
package hello.servlet.web.frontcontroller.v1;
@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(); // URI 정보를 받는다.
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request , response);
}
}
기존의 servlet,jsp 를 통한 MVC 패턴과의 차이점을 본다면 frontController 에서 url 경로를 생성자를 통해 미리 지정해준다. 따라서 회원저장 , 회원폼 , 회원목록조회 구현체에는 url 링크를 가지지 않는다.
또한 controllerMap 을 통해서 하나의 공통된 컨트롤러를 거쳐서 로직을 실행하도록 만들었다.
기본적인 구조는 맞추었으나 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 발생하고 깔끔하지 않다.
이제 구조를 맞췄으니 간략화를 해보자.
📌 View 분리 - v2
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
이 부분을 중복이 발생하지 않도록 처리해주는 뷰를 만들어보자.
구조는 위와같다.
MyView 를 통해 랜더링을 함으로써 컨트롤러에서 랜더링을 하지 않도록 한다.
package hello.servlet.web.frontcontroller;
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);
}
}
viewPath 를 생성자로 받고 render 함수를 추가하였다.
package hello.servlet.web.frontcontroller.v2;
public interface ControllerV2 {
MyView process(HttpServletRequest request , HttpServletResponse response) throws ServletException, IOException;
}
Controller 는 이제 MyView 객체를 반환한다.
이제 인터페이스를 구현해보자.
package hello.servlet.web.frontcontroller.v2.controller;
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");
}
}
회원 등록 컨트롤러에 대한 코드는 위와 같다.
코드가 훨씬 깔끔해졌다. MyView 객체를 생성함과 동시에 경로를 지정해주고 반환해주는 역할을 한다.
package hello.servlet.web.frontcontroller.v2.controller;
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")); // getParameter 의 반환값은 항상 String이다.
Member member = new Member(username , age);
memberRepository.save(member);
// Model 에 데이터를 보관한다.
request.setAttribute("member", member);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
회원 저장코드는 위와 같다. request 에서 parameter 로 이름과 나이를 받아오고 Member 객체를 생성하고 저장한뒤에 MyView 객체를 반환한다.
package hello.servlet.web.frontcontroller.v2.controller;
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");
}
}
회원 목록조회코드도 깔끔하졌다. findAll 함수로 모두 찾은뒤 request 에 저장한뒤 MyView 객체를 경로와함께 반환해주면 끝이다.
package hello.servlet.web.frontcontroller.v2;
@WebServlet(name = "frontControllerServletV2" , urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
private 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 {
String requestURI = request.getRequestURI(); // URI 정보를 받는다.
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView view = controller.process(request, response);
view.render(request, response); // 추가된 코드
}
}
frontController 를 보자. v1 버전과 비교했을때 생성자 부분은 변함이 없다.
다만 view.render(request , response) 코드가 딱 한줄 추가되었다.
역할을 분리함으로써 중복된 코드를 분리하니 frontController 코드 한줄이 추가되고 회원등록 , 회원저장 , 목록조회 코드에서 중복되는 코드가 전부 지워지면서 코드의 수가 줄었다.
이제 조금 더 역할을 분리해보자.
📌 Model 추가 - v3
개선해야할 점을 찾아보자.
컨트롤러 입장에서 HttpServletRequest, HttpServletResponse 이 꼭 필요할까?
요청 파라미터 정보는 자바의 Map 으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.
그리고 request 객체를 Model 로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다. 우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경해보자.
이렇게 하면 구현 코드도 매우 단순해지고, 테스트 코드 작성이 쉽다.
컨트롤러에서 지정하는 뷰 이름이 중복되는것을 알 수 있다. 만약에 경로가 바뀌거나 확장자가 바뀌면 하나하나 바꿔야할 수 있다. 따라서 뷰의 논리 이름을 반환하고 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 하자.
/WEB-INF/views/new-form.jsp -> new-form/WEB-INF/views/save-result.jsp -> save-result/WEB-INF/views/members.jsp -> members구조는 위와같다. viewResolver 가 추가되었다.
또한 Controller 에서 이제 ModelView 를 반환한다.
지금까지 request.setAttribute() 를 통해 데이터를 저장했는데 서블릿의 종속성을 제거하기 위해 데이터를 저장할 별도의 객체가 필요하다. 이를 ModelView 에서 해줄 예정이다.
package hello.servlet.web.frontcontroller;
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;
}
}
ModelView 코드는 위와같다. lombok 을 통해서 getter,setter 를 구현하지 않아도 된다.
package hello.servlet.web.frontcontroller.v3;
public interface ControllerV3 {
ModelView process(Map<String,String> paramMap);
}
Controller 는 위와같다. ModelView 를 반환하고 파라미터는 Map<String,String> 이다. 이전의 컨트롤러에서는 HttpServletRequest , HttpServletResponse 를 모두 담았지만 이젠 그럴 필요가없다.
이제 회원등록 , 회원 저장 , 목록조회 코드를 살펴보자.
package hello.servlet.web.frontcontroller.v3.controller;
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
이제 객체를 반환할때 실제 물리 주소가 아니라 논리적 주소를 반환한다.
또한 메서드의 파라미터에 HttpServletRequest 가 더이상 존재하지 않고 paramMap 만 존재한다.
package hello.servlet.web.frontcontroller.v3.controller;
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;
}
}
이전의 v2 버전에서는 request.setAttribute("member", member) 와 같이 데이터를 저장했지만 이젠 ModelView 를 통해 데이터를 저장을 한다.
또한 여기서도 논리적 주소인 save-result 를 사용하고 있고 파라미터 정보 또한 httpservletRequest 를 사용하지 않는다.
package hello.servlet.web.frontcontroller.v3.controller;
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;
}
}
v2 버전에서는 request.setAttribute("members", members); 로 회원목록을 저장했지만 이제 ModelView를 통해 데이터를 저장한다.
package hello.servlet.web.frontcontroller.v3;
@WebServlet(name = "frontControllerServletV3" , urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private 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 {
String requestURI = request.getRequestURI(); // URI 정보를 받는다.
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// paramMap 을 넘겨주어야 함
Map<String, String> paraMap = createParamMap(request);
ModelView mv = controller.process(paraMap);
// 논리적 주소를 물리적 주소로 반환해야한다.
String viewName = mv.getViewName();
// 아래 코드를 통해 파일의 경로나 확장자를 유연하게 대처할 수 있다.
MyView view = viewResolver(viewName);
view.render(mv.getModel() , request, response); // 추가된 코드
}
private static MyView viewResolver(String viewName) {
// 객체를 생성하는것은 service 클래스에 준위가 맞지 않기 때문에 따로 클래스를 뽑아 주는게 좋다.
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private static Map<String, String> createParamMap(HttpServletRequest request) { // ctrl+alt+M - 람다식 메서드 반환
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
// paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
return paraMap;
}
}
frontController 코드는 위와 같다. 먼저 createParamMap 메서드를 보면 람다식을 통해 paraMap 에 request 에 대한 모든 정보를 넘겨주고 반환한다.
즉 이름 , 나이 정보에 대해서 저장을 한다.
이후 paramMap 을 파라미터로 넣어주면서 ModelView 객체를 만든다. 이때 ModelView 를 통해 논리적 주소를 얻을 수 있다.
viewName 에 논리적 주소를 먼저 담고 viewResolver 메서드를 통해서 논리적 주소를 물리적 주소로 반환한다.
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 static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key,value) -> request.setAttribute(key,value));
}
이전에 사용하던 MyView 클래스에 두개의 메서드가 추가되었다.
먼저 modelToRequestAttribute 를 보면 model 을 파라미터로 넘겨받은 후에
모델에 있는 key,value 를 전부 request.setAttribute() 를 통해 저장해준다.
그리고 dispatcher 를 통해 렌더링을 해준다.
v2 버전에서는 setAttribute 를 모든 컨트롤러에서 매번 해주었지만 이젠 MyView 에서 한꺼번에 처리해준다.
이제 컨트롤러는 서블릿에 대한 종속성을 제거하고 뷰 경로의 중복을 제거할 수 있게 되었다.
📌 단순하고 실용적인 컨트롤러 - v4
v3 버전에서 Controller 코드를 보면 중복이 발생한다. 항상 ModelView 객체를 생성하고 반환해야 한다는 점이다.
구조적으로는 잘 짜여있지만 매번 컨트롤러를 만들때마다 ModelView 객체를 생성하고 반환해야한다면 어떨까?
이번에는 실용성을 늘려보자.
이제 Controller 가 ModelView 를 반환하지 않고 ViewName 을 반환하게 된다. 코드를 살펴보자.
package hello.servlet.web.frontcontroller.v4;
import java.util.Map;
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
따라서 Controller 인터페이스는 ModelView 객체를 반환하지 않고 String 문자열을 반환한다. 그리고 Model 은 파라미터로 넘긴다.
이제 회원 등록 , 회원 저장 , 목록 조회 코드를 구현해보자.
package hello.servlet.web.frontcontroller.v4.controller;
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
회원 등록 코드가 훨씬 깔끔해졌다. 이전에는 ModelView 객체를 생성하고 반환을 해야했는데 이제 논리적 주소만 반환하면 된다.
package hello.servlet.web.frontcontroller.v4.controller;
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 으로 값을 저장만 해주면 끝이다.
반환값은 논리주소이므로 ModelView 객체를 생성하지 않아도 된다.
package hello.servlet.web.frontcontroller.v4.controller;
public class MemberListControllerV4 implements ControllerV4 {
private final 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";
}
}
목록 조회도 코드가 더욱 깔끔해졌다.
파라미터에 정보를 넣음으로써 매번 객체를 생성해야하는 복잡한 코드를 더이상 쓰지 않아도 된다.
package hello.servlet.web.frontcontroller.v4;
@WebServlet(name = "frontControllerServletV4" , urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
private 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 {
String requestURI = request.getRequestURI(); // URI 정보를 받는다.
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// paramMap 을 넘겨주어야 함
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 static MyView viewResolver(String viewName) {
// 객체를 생성하는것은 service 클래스에 준위가 맞지 않기 때문에 따로 클래스를 뽑아 주는게 좋다.
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private static Map<String, String> createParamMap(HttpServletRequest request) { // ctrl+alt+M - 람다식 메서드 반환
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
// paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
return paraMap;
}
}
frontController 코드를 살펴보자. v3 버전에 비해 바뀐점은 크게 없다.
service 메서드 안에서 직접 model 을 만들어 주는 코드 한줄만 추가되었다.
컨트롤러가 직접 뷰의 논리 주소를 반환하므로 model 객체는 한번만 생성해도 충분하다.
지금까지 단지 model 을 파라미터로 넘겼을 뿐인데 객체를 여러번 생성하는 중복코드를 지울 수 있게 되었다. 이를 통해 구조적인 변화는 크게 변하지 않았지만 실용성이 크게 증가하였다.
📌 유연한 컨트롤러1 - v5
만약 어떤 개발자는 V3 버전의 컨트롤러를 사용하고싶고 다른 개발자는 V4 버전의 컨트롤러를 사용하고 싶다면 어떡할까?
지금까지 개발한 frontController 는 하나의 컨트롤러 인터페이스만 사용할 수 있다.
이를 해결 하기 위해서는 어뎁터 패턴 이 필요하다.
110v 전기 콘센트에 220v 제품을 사용할 수 없으므로 변환 어뎁터를 사용하듯이 적용해보자.
이제 handler 라는 개념이 추가된다.
handler adapter 는 여러 컨트롤러를 저장하고 사용할 수 있게 하는 역할을 가진다.
handler 는 이전에 사용한 Controller 와 개념이 유사하다.
package hello.servlet.web.frontcontroller.v5;
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request , HttpServletResponse response , Object handler) throws ServletException , IOException;
}
MyHandlerAdapter 인터페이스 코드는 위와 같다. supports 는 핸들러를 사용할 수 있는지 여부를 판단해준다.
handler 메서드는 Controller 를 연결해주는 역할을 한다.
이제 Adapter 를 구현해보자.
package hello.servlet.web.frontcontroller.v5.adapter;
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);
ModelView mv = controller.process(paramMap);
return mv;
}
private static Map<String, String> createParamMap(HttpServletRequest request) { // ctrl+alt+M - 람다식 메서드 반환
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
// paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
return paraMap;
}
}
먼저 supports 메서드를 구현한 코드를 보면 instanceof 를 통해 지원하는지 여부를 판단한다.
ControllerV3HandlerAdapter 이므로 V3 버전인지 판단해주는 기능을 한다.
handle 메서드는 handler 를 ControllerV3 로 캐스팅한다. 그리고 paramMap 을 통해 reqeust 의 파라미터를 저장하고 ModelView 를 반환한다.
이제 frontController 를 보자.
package hello.servlet.web.frontcontroller.v5;
@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());
// v4 추가
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);
// 논리적 주소를 물리적 주소로 반환해야한다.
String viewName = mv.getViewName();
// 아래 코드를 통해 파일의 경로나 확장자를 유연하게 대처할 수 있다.
MyView view = viewResolver(viewName);
view.render(mv.getModel() , request, response); // 추가된 코드
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter 를 찾을 수 없습니다. handler=" + handler);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI(); // URI 정보를 받는다.
return handlerMappingMap.get(requestURI);
}
private static MyView viewResolver(String viewName) {
// 객체를 생성하는것은 service 클래스에 준위가 맞지 않기 때문에 따로 클래스를 뽑아 주는게 좋다.
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
생성자에 두개의 init 메서드가 있다. handlerMappingMap 에게 경로와 컨트롤러를 넣어준다. handlerAdapters 에게는 사용할 컨트롤러 어뎁터 객체를 저장한다.
이로 인해서 getHandlerAdapter 메서드에서는 handlerAdapters 에 저장된 컨트롤러 어뎁터 객체를 반복문으로 하나하나씩 supports 하는지 확인해준다. 그리고 맞는 어뎁터를 반환한다.
그리고 getHandler 메서드를 통해 Object handler 객체에게 사용할 컨트롤러를 반환해준다.
마지막으로 v4 버전도 어뎁터를 만들어 보자.
📌 유연한 컨트롤러2 - v5
package hello.servlet.web.frontcontroller.v5.adapter;
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 static Map<String, String> createParamMap(HttpServletRequest request) { // ctrl+alt+M - 람다식 메서드 반환
Map<String, String> paraMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paraMap.put(paramName, request.getParameter(paramName)));
// paramMap 에 request 에 있는 모든 파라미터를 넣어준다.
return paraMap;
}
}
V4 버전에서는 뷰의 이름을 반환한다. 따라서 model 객체를 생성한 뒤에 논리적 주소를 가져오고 ModelView 에게 viewName 을 저장한 뒤에 반환해준다.
앞서 살펴본 FrontControllerServletV4 코드와 유사하다.
📌 정리
지금까지 5개의 버전을 통해서 MVC 모델을 개선해왔다.
프론트 컨트롤러를 도입하고 반복되거나 중복되는 코드를 제거한뒤에 , 어뎁터를 통해 실용성을 추가했다.
지금까지 만든 MVC 패턴은 실제로 Spring MVC 패턴과 유사하다. 다음에는 Spring MVC 패턴에 대해 알아보자.