Search
Duplicate

MVC 프레임워크 만들기

목차

프론트 컨트롤러 패턴 소개

이전 챕터(서블릿, JSP, MVC패턴) 에서 사용한 MVC 패턴 의 한계점인 공통 처리가 힘들다는 부분을 해결하기 위해서 프론트 컨트롤러 패턴을 사용한다고 했다. 이 프론트 컨트롤러 패턴은 서블릿 하나로 클라이언트의 요청을 받아 이 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출해주는 역할을 함으로써 공통 영역을 한군데 몰아넣을 수 있다. 그렇게 함으로써 여러 컨트롤러에 산재되있던 공통 코드를 프론트 컨트롤러 하나로 모을 수 있고, 더하여 프론트 컨트롤러를 제외하면 서블릿을 상속해서 구현해줄 필요도 없다. 현재 스프링 웹 MVCDispatcherServlet도 이 FrontController 패턴으로 구현되어 있다.
기존의 MVC Pattern만 적용한 상태
공통 코드가 모든 컨트롤러에 산재되있고 클라이언트는 각각의 컨트롤러를 호출해야 하는 단점이 있다.
프론트 컨트롤러를 도입한 뒤의 모습
공통 코드가 프론트코드에 모이고 클라이언트의 요청은 모두 프론트 컨트롤러로 집중된다.

프론트 컨트롤러 도입 - v1

이제 실제로 코드를 작성하며 프론트 컨트롤러 패턴을 적용해 보자.
핵심은 기존 코드의 변경은 최소화 하며 프론트 컨트롤러를 도입하는것으로 한다.

V1 구조

코드

1. ControllverV1

서블릿과 유사한 인터페이스를 구현해 각 컨트롤러는 해당 인터페이스를 구현함으로써 프론트 컨트롤러에서는 해당 인터페이스를 호출해서 다형성으로써 각각의 구현 컨트롤러와의 의존관계를 끊을 수 있다.
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; }
Java
복사

2. 회원 등록, 저장, 전체 조회 컨트롤러

회원 등록, 회원 가입(저장), 회원 목록 을 담당하는 세 개의 컨트롤러를 만들 것인데, 이전 챕터에서 구현한 로직을 최대한 그대로 유지할 것이다.
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); } }
Java
복사
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 { 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); Member savedMember = 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); } }
Java
복사
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 { 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); } }
Java
복사
ControllerV1을 구현한 세 컨트롤러 모두 내부 로직은 기존에 만든 MvcMember 서블릿들과 유사하다.
이제 이를 프론트 컨트롤러를 만들어서 이 컨트롤러들을 호출할 수 있도록 만들어주자.

3. 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> 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
복사
urlPatterns = "/front-controller/v1/*"
⇒ 기존에 작성하던 url과는 조금 다르다. 끝을 보면 명확한 경로가 적힌게 아닌 * 이 있는데, 이는 상위 경로(/front-controller/v1/ ) 를 포함한 하위 경로 모두 이 서블릿에서 받아들인다는 의미다.
⇒ /front-controller/v1
⇒ /front-controller/v1/depth1
⇒ /front-controller/v1/depth1/depth2
모두 해당 서블릿에서 받아들인다.
private Map<String, ControllerV1> controllerMap = new HashMap<>();
⇒ key: 매핑 URL
⇒ value: 호출될 컨트롤러
service()
⇒ getRequestURI() 메서드로 요청 URI를 얻은 뒤 controllerMap에서 적절한 컨트롤러를 찾는다.
⇒ 만약 없다면(controller == null) 404(SC_NOT_FOUND) 상태 코드를 반환한다.
⇒ 컨트롤러가 있다면 process(reqeust, response)메서드를 호출해서 해당 컨트롤러 로직을 실행한다.
JSP는 기존에 만들었던 (MVC) 것을 그대로 사용한다.

4. 동작 확인

등록 URL: http://localhost:8080/front-controller/v1/members/new-form
목록 URL: http://localhost:8080/front-controller/v1/members
혹은 Welcome Page에서 FrontController - v1 탭의 회원 가입, 회원 목록 탭을 선택해도 된다.

View 분리 - v2

MVC 패턴의 한계를 해결하기위해 프론트 컨트롤러 패턴을 도입해서 모든 Request 요청에 대해서 프론트 컨트롤러를 거치게 함으로써 공통 처리를 모을 수 있었다. 그래서 해당 프론트 컨트롤러 서블릿에서 필요한 컨트롤러를 매핑시켜 호출해줬지만, 아직 중복되는 코드는 여전히 존재한다.
String viewPath = "경로"; RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response);
Java
복사
아직 컨트롤러에서 뷰로 이동하는 코드가 모든 컨트롤러에 포함되있어서 중복이 되고 있는 상황이고, 이를 해결하기위해 뷰를 처리하는 객체를 만들어 프론트 컨트롤러에서 사용할 수 있도록 만들어보자.

V2 구조

→ 기존에 컨트롤러에서 바로 JSPforward해주는 과정이 사라지고 컨트롤러는 MyView 라는 뷰 인터페이스를 반환하면 컨트롤러에서는 MyView 인터페이스의 render() 메서드를 호출함으로써 해당 인터페이스에서 JSPforward하도록 구조가 변경되었다.

코드

1. MyView

컨트롤러에서 JSP forward하는 코드를 모듈화 하기 위해 만든 인터페이스로 클래스 생성시 전달한 jsp 경로를 viewPath로 받아 생성되며 render() 메서드 호출시 인자로 받은 request, response를 인자로 jsp forward를 한다.
뷰 객체는 V2뿐아니라 다른 버전에서도 사용될 것이기에 생성경로는 frontcontroller 하위 경로다.
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 { 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
복사

2. 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; }
Java
복사
V1과 유사하지만 반환 타입이 MyView 라는 점이 다르다.

3. 회원 등록, 가입, 목록 조회 컨트롤러

MemberFormControllverV2 - 회원 등록 폼
package hello.servlet.web.frontcontroller.v2.controller; 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 MemberFormControllerV2 implements ControllerV2 { @Override public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { return new MyView("/WEB-INF/views/new-form.jsp"); } }
Java
복사
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 { 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); Member savedMember = memberRepository.save(member); //Model에 데이터 보관 request.setAttribute("member", member); return new MyView("/WEB-INF/views/save-result.jsp"); } }
Java
복사
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 { 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"); } }
Java
복사
이제 모든 컨트롤러에서 공통적으로 구현되있던 dispatcher.forward()가 사라지고 반환타입인 MyView 객체를 생성하여 반환해주고 있다.

4. FrontControllerServletV2

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; @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(); ControllerV2 controller = controllerMap.get(requestURI); if (controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } MyView myView = controller.process(request, response); myView.render(request, response); } }
Java
복사
MyView myView = controller.process(request, response);
⇒ V1과는 다르게 MyView객체를 반환타입으로 반환받는다.
⇒ 반환받은 MyView 객체의 render(request, response) 메서드를 호출하면 forward로직이 수행되어 JSP가 실행된다.
JSP 페이지 렌더링 로직이 MyView 객체를 사용함으로써 일관되게 모임으로써 각각의 컨트롤러가 forward 로직을 제대로 수행하고 있는지 하나하나 신경쓸 필요가 없고 MyView 객체만 제대로 반환한다면 무리없이 동작한다.

Model 추가 - v3

뷰 이동 로직을 분리했다면 이번에는 서블릿의 종속성및 뷰 이름이 중복되는것도 제거해보자.

서블릿 종속성 제거

지금까지 회원 등록 폼, 회원 가입, 회원 목록 조회 를 구현하면서 모든 컨트롤러에는 Request, Response 객체를 인자값으로 전달해줬다. 그런데 모든 컨트롤러에서 해당 인자값들이 다 100% 사용되었을까 생각해보면 그렇지 않다. 회원 등록 폼에서는 아예 어떤 인자도 사용되지 않았고 회원 목록 조회에서도 Response 객체는 사용되지 않았다. 그런데도 매번 이렇게 인자값을 전달하는것은 비효율적이다.
이는 Model을 Request 객체의 Attribute로 사용하기에 생기는 일인데, 그럼 별도의 Model 객체를 만들어서 사용하면 어떨까? 이번 챕터에서는 컨트롤러에서 서블릿을 사용하지 않도록 구현해 볼 것이다.

뷰 이름 중복 제거

계속해서 viewPath를 지정해줄 때 상위경로와 접미사가 중복되는 느낌을 받지 못했는가?
뷰의 논리이름과 물리이름이 있다면 지금까지는 물리 이름이 사용되었기에 항상 전체 경로를 다 적어줬는데, 이러한 중복 또한 제거해주도록 하자.
처럼 중복을 제거한다면 차후에 수정할 일이 생기더라도 공통으로 처리해주는 곳에서만 변경해주면 된다.

V3 구조

이전 챕터에서 프론트 컨트롤러에서 바로 MyView객체의 render()메서드를 호출해 JSP로 forward 해줬다면 V3 버전에선 그전에 viewResolver를 호출해 MyView를 반환하도록 했다.
컨트롤러는 MyView가 아니라 ModelView 객체를 반환해주도록 바뀌었다.
ModelView
⇒ 지금까진 모델로 HttpServletRequest의 Attribute를 사용했는데, 서블릿의 종속성을 제거하기위해 별도의 Model을 만들어 View 이름까지 전달하는 ModelView 객체를 만들었다.

코드

1. ModelAndView

별도의 Model과 View 이름까지 저장하는 객체
package hello.servlet.web.frontcontroller; import lombok.Getter; import lombok.Setter; import java.util.HashMap; import java.util.Map; @Getter @Setter public class ModelView { private String viewName; private Map<String, Object> model = new HashMap<>(); public ModelView(String viewName) { this.viewName = viewName; } }
Java
복사
view의 논리 이름을 저장하는 viewName과 뷰에 필요한 데이터를 담는 Model을 Map으로 구현했다.

2. 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); }
Java
복사
Request, Response 객체를 인자값으로 받지 않기에 서블릿 기술로부터 종속성이 사라졌다. 이는 즉 테스트 코드를 구현하기 쉬워졌다는 의미도 된다.
HttpServletRequest에서 필요한 정보는 프론트 컨트롤러에서 paramMap으로 담아 호출하면 된다.
그리고 ControllerV3에서는 반환타입이 ModelView로 forward될 view의 논리명과 데이터가 담긴 Model이 반환된다.

3. 회원 등록 폼, 저장, 목록 조회 컨트롤러

MemberFormControllerV3 - 회원 등록 폼
package hello.servlet.web.frontcontroller.v3.controller; import hello.servlet.web.frontcontroller.ModelView; import hello.servlet.web.frontcontroller.v3.ControllerV3; import java.util.Map; public class MemberFormControllerV3 implements ControllerV3 { @Override public ModelView process(Map<String, String> paramMap) { return new ModelView("new-form"); } }
Java
복사
ModelView생성자로 들어가는 이름은 view의 논리명으로 프론트컨트롤러에서 viewResolver에 의해 물리명으로 조합된다.
MemberSaveContrllerV3 - 회원 가입
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 { 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); Member save = memberRepository.save(member); ModelView mv = new ModelView("save-result"); mv.getModel().put("member", member); return mv; } }
Java
복사
전달받은 paramMap에서 필요한 요청 파라미터를 꺼내어 사용한다.
Model의 담고싶은 데이터는 ModelView 객체의 Model 필드에 담도록 한다.
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 { 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; } }
Java
복사

4. FrontControllerV3

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; @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(); ControllerV3 controller = controllerMap.get(requestURI); if (controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } //paramMap HashMap<String, String> paramMap = createParamMap(request); ModelView mv = controller.process(paramMap); String viewName = mv.getViewName(); MyView myView = viewResolver(viewName); myView.render(mv.getModel(), request, response); } private MyView viewResolver(String viewName) { return new MyView("/WEB-INF/views/" + viewName + ".jsp"); } private HashMap<String, String> createParamMap(HttpServletRequest request) { HashMap<String, String> paramMap = new HashMap<>(); request.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); return paramMap; } }
Java
복사
HashMap<String, String> paramMap = createParamMap(request);
⇒ request에서 파라미터 정보를 담아 Map으로 담아준다.
MyView myView = viewResolver(viewName);
⇒ 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경해주는 메서드.
⇒ 실제 물리 경로가 설정된 MyView 객체를 반환해준다.
myView.render(mv.getModel(), request, response);
⇒ 이전에 사용하던 render() 메서드와는 파라미터가 다르다.
⇒ 오버로딩을 통해 해당 3개 인자값에 대응하는 render() 메서드를 만들어줘야 한다.

5. MyView

render(mv.getModel(), request, response)에 대응하는 메서드를 오버로딩하자.
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 { 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 void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { modelToRequestAttribute(model, request); render(request, response); } private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) { model.forEach(request::setAttribute); } }
Java
복사

6. 동작 확인

등록 URL : http://localhost:8080/front-controller/v3/members/new-form
조회 URL : http://localhost:8080/front-controller/v3/members

단순하고 실용적인 컨트롤러 - v4

지금까지 만든 컨트롤러에서 대부분의 한계를 극복하고 중복도 제거가 완료되었다.
하지만 사용에 있어서는 매번 ModelView를 만들어서 반환하고 하는 과정들이 번거롭다. 그렇기에 이번에는 이런 번거로운 과정을 줄여서 개발자가 더 사용하기 편하게 리팩토링을 진행해볼 것이다.

V4 구조

리팩토링의 방향은 개발자 편의에 맞춰져있기에 큰 변화가 있지는 않고 컨트롤러에서 ModelView를 반환하는 대신 ViewName만 반환하도록 한다.

코드

1. ControllerV4

package hello.servlet.web.frontcontroller.v4; import java.util.Map; public interface ControllerV4 { /** * @param paramMap * @param model * @return */ String process(Map<String, String> paramMap, Map<String, Object> model); }
Java
복사
반환 타입을 String으로 viewName만 그대로 반환하면 되도록 변경되었다. 이제 ControllerV4 구현 컨트롤러는 따로 ModelView객체를 생성할 필요 없이 viewName만 반환해주면 된다.

2. 회원 등록, 회원 가입, 회원 목록 조회 컨트롤러

MemberFormControllerV4 - 회원 등록 폼 컨트롤러
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"; } }
Java
복사
MemberSaveControllerV4 - 회원 가입 컨트롤러
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 { 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"; } }
Java
복사
MemberListControllerV4 - 회원 목록 조회 컨트롤러
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.List; import java.util.Map; public class MemberListControllerV4 implements ControllerV4 { 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"; } }
Java
복사
ControllerV4를 구현하는 3개의 컨트롤러 모두 이제 뷰의 논리 이름(ex: new-form, save-result, members)만 바노한하면 된다. 매번 객체 생성하는 비용을 줄일 수 있다.
model이 인자값으로 넘어오기 때문에 모델에 담아야 할 데이터는 인자값으로 넘어온 model Map에 담으면 된다. (call by reference)

3. FrontControllerServletV4

package hello.servlet.web.frontcontroller.v4; 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.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; @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(); ControllerV4 controller = controllerMap.get(requestURI); if (controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } //paramMap Map<String, String> paramMap = createParamMap(request); Map<String, Object> model = new HashMap<>(); String viewName = controller.process(paramMap, model); MyView myView = viewResolver(viewName); myView.render(model, request, response); } private MyView viewResolver(String viewName) { return new MyView("/WEB-INF/views/" + viewName + ".jsp"); } private HashMap<String, String> createParamMap(HttpServletRequest request) { HashMap<String, String> paramMap = new HashMap<>(); request.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); return paramMap; } }
Java
복사
Map<String, Object> model = new HashMap<>();
⇒ 컨트롤러 구현체에 전달할 Model 을 생성해준다. 각 구현체 컨트롤러에서는 여기서 전달한 Model에 데이터를 담아 전달할 수 있고, 그 덕에 ModelView 객체를 생성하지 않아도 된다.
String viewName = controller.process(paramMap, model);
⇒ 구현체는 이전과 다르게 viewName만 바로 반환하기에 이를 뷰 리졸버를 사용해서 실제 물리 뷰를 찾으면 된다.

4. 동작 확인

등록 URL : http://localhost:8080/front-controller/v4/members/new-form
조회 URL : http://localhost:8080/front-controller/v4/members

유연한 컨트롤러 1 - v5

Controller를 V1부터 V4까지 다양한 방법으로 만들어서 회원관리 웹 애플리케이션을 구현해봤다.
그럼 만약, 이 컨트롤러들이 혼용되서 V1~V4를 모두 사용해야한다면 어떻게 해야할까?
ControllerV3
public interface ControllerV3 { ModelView process(Map<String, String> paramMap); }
Java
복사
ControllerV4
public interface ControllerV4 { String process(Map<String, String> paramMap, Map<String, Object> model); }
Java
복사
당장 두 컨트롤러의 process메서드만 봐도 반환타입과 인자 값의 갯수와 타입이 다르다. 이번 챕터에서는 이렇게 다른 타입의 컨트롤러를 혼용해서 쓸 수 있도록 해주는 어댑터 패턴에 대해서 학습해보자.

어댑터 패턴

어댑터 패턴에 대한 기술 자체는 여기(링크) 를 통해 보도록 하자
결국 완전 다른 인터페이스를 모아놓고 어댑터 패턴을 사용해 프론트 컨트롤러에서 사용할 수 있도록 해준다.

V5구조

이전과 비교해 달라진 것은 핸들러 어댑터와 핸들러라 할 수 있다.
핸들러 어댑터: 프론트 컨트롤러와 핸들러(컨트롤러) 사이에 핸들러 어댑터가 추가되었는데, 이 핸들러의 어댑터 역할을 하기 때문에 핸들러 어댑터이다. 이 어댑터를 통해 상반된 핸들러도 호환이 가능해진다.
핸들러: 컨트롤러를 더 포괄적으로 부르는 것이다. 갑자기 이렇게 명칭을 바꾸는 이유는 이 어댑터를 이용해서 컨트롤러의 개념 뿐 아니라도 어댑터만 있으면 처리가 가능하기 때문이다.

코드

1. 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; }
Java
복사
boolean supports(Object handler);
⇒ 핸들러는 컨트롤러를 의미한다.
⇒ 이 어댑터가 해당 컨트롤러를 처리할 수 있는지 확인 후 반환하는 메서드이다.
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
⇒ 어댑터는 실제 컨트롤러를 호출한 뒤 ModelView를 반환해야 한다.
⇒ 실제 컨트롤러에서 ModelView를 반환하지 않는다면 어댑터에서 생성해서라도 반환해야 한다.
⇒ 컨트롤러가 호출되는 위치가 프론트컨트롤러에서 어댑터로 변경되었다.

2. ControllerV3HandlerAdapter

ControllerV3 타입의 구현체 컨트롤러를 지원하는 어댑터
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; 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; HashMap<String, String> paramMap = createParamMap(request); return controller.process(paramMap); } private HashMap<String, String> createParamMap(HttpServletRequest request) { HashMap<String, String> paramMap = new HashMap<>(); request.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); return paramMap; } }
Java
복사
return (handler instanceof ControllerV3);
⇒ 핸들러(컨트롤러)가 Controller의 자식이거나 구현체인지 판단해 지원여부를 반환한다.
⇒ 반환 값이 true일경우 이 어댑터는 해당 핸들러를 지원 지원할 수 있다는 의미다.
ControllerV3 controller = (ControllerV3) handler;
⇒ 인자값으로 받은 핸들러는 Object타입이기에 ControllerV3 타입으로 형변환을 해줘야 한다.
(support 메서드를 통해 ControllerV3의 구현체인것을 확인했으니 형변환시 문제없이 동작한다.)
V3버전은 반환타입이 ModelView이기에 바로 로직 수행 후 반환하면 된다.

3. FrontControllerServletV5

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.controller.MemberFormControllerV4; import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4; import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4; import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter; 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; @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()); } private void initHandlerAdapters() { handlerAdapters.add(new ControllerV3HandlerAdapter()); } @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 MyView viewResolver(String viewName) { return new MyView("/WEB-INF/views/" + viewName + ".jsp"); } private MyHandlerAdapter getHandlerAdapter(Object handler) { for (MyHandlerAdapter adapter : handlerAdapters) { if (adapter.supports(handler)) { return adapter; } } throw new IllegalArgumentException("HandlerAdapter 찾을 수 없습니다. handler = " + handler); } private Object getHandler(HttpServletRequest request) { String requestURI = request.getRequestURI(); return handlerMappingMap.get(requestURI); } }
Java
복사
initHandlerMappingMap();
⇒ urlPattern에 매핑되는 컨트롤러 를 초기화해주는 로직 메서드
⇒ 핸들러를 담는 맵의 value type이 Object인 이유는 담길 핸들러가 V1~V4주 어느 핸들러 인터페이스일지 모르기 때문이다.
initHandlerAdapters();
⇒ 사용할 어댑터를 리스트 콜렉션에 추가하는 작업을 하는 로직 메서드
Object handler = getHandler(request);
⇒ 초기화한 핸들러 매핑 정보에서 URL을 key로 매핑된 핸들러(컨트롤러)를 찾아 반환해준다.
MyHandlerAdapter adapter = getHandlerAdapter(handler);
⇒ 핸들러를 처리할 수 있는 어댑터를 조회한다.
⇒ 콜렉션에 등록된 어댑터를 순회하며 해당 핸들러가 지원되는지 확인후 지원하는 어댑터가 있다면 반환한다.
⇒ 만약 존재하지 않는다면 예외가 발생한다.
ModelView mv = adapter.handle(request, response, handler);
⇒ 어댑터의 handle 메서드를 통해 실제 어댑터가 구현한 로직이 수행되는데 handler는 로직에서 자신이 지원하는 핸들러로 형변환해서 사용을 마친 뒤 필요한 값을 반환한다.

4. 동작 확인

유연한 컨트롤러2 - v5

이번엔 ControllerV4도 호환되도록 리팩토링을 해주자.

코드

1. FrontControllerServletV5에 핸들러 매핑 아이템 추가

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()); //V4 추가 }
Java
복사
ControllerV4용 경로(key)와 컨트롤러를 handlerMappingMap에 추가해준다.
ControllerV4도 호환이 되도록 해주는 어댑터를 handlerAdapter 콜렉션에 추가해준다.
⇒ 아직 구현이 안되있어서 아래에서 바로 구현해주도록 하자.

2. ControllerV4HandlerAdapter

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); Map<String, Object> model = new HashMap<>(); String viewName = controller.process(paramMap, model); ModelView mv = new ModelView(viewName); mv.setModel(model); return mv; } private HashMap<String, String> createParamMap(HttpServletRequest request) { HashMap<String, String> paramMap = new HashMap<>(); request.getParameterNames().asIterator() .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); return paramMap; } }
Java
복사
String viewName = controller.process(paramMap, model); ModelView mv = new ModelView(viewName); mv.setModel(model);
⇒ ControllerV4는 반환타입이 String으로 ModelView가 아니라 viewName만 반환을 한다.
그렇기에 어댑터에서 이를 ModelView로 만들어줘서 어댑터 반환타입을 일치시켜줘야한다.

이전 챕터로

다음 챕터로