5가지 버전
프론트 컨트롤러 도입(v1), View분리(v2), Model 추가(v3), 단순하고 실용적인 컨트롤러(v4), 유연한 컨트롤러(v5)로 단계별로 만들어 본다.
4. MVC 프레임워크 만들기
여기서는 기존 회원관리 애플리케이션의 단점을 한 단계씩 버전업하면서 단계별로 만들어본다. 버전별로 먼저 어떻게 변경되는지 간단하게 정리하면 다음과 같다.
- v1 : 프론트 컨트롤러라는 것을 도입한다. 말 그대로 기존 컨트롤러에 앞서 동작하는 부분으로, 요청에 맞는 컨트롤러를 호출해주는 역할을 한다.
- v2 : v1에서 반복되는 뷰 로직이 있는데 이를 분리해준다.
- v3 : Model을 추가해서 컨트롤러에 반복되는 서블릿 종속성을 제거하고 뷰 이름 중복도 제거한다.
- v4 : v3과 비슷하지만 컨트롤러 구현부분에서 ModelView를 직접 생성 후 반환하지 않고 viewName만 반환하도록 변경한다.
- v5 : 프론트 컨트롤러가 유연하게 v3,v4 등을 사용할 수 있도록 어댑터패턴을 적용해본다.
1) 프론트 컨트롤러 패턴 소개
Front Controller 패턴 특징
- 프론트 컨트롤러 서블릿 하나로 클라이언트 요청 받음
- 프론트 컨트롤러가 요청에 맞는 컨트롤러 찾아서 호출
- 입구는 하나
- 공통 처리 가능
- 프론트 컨트롤러 제외한 나머지 컨트롤러는 서블릿 사용 안 해도됨
- 스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있음
2) 프론트 컨트롤러 도입-V1
- 인터페이스 ControllerV1을 만든다.
- 인터페이스를 구현한 컨트롤러(MemberFormController, MemberSaveController, MemberListController)를 만든다. 이전 서블릿의 로직은 그대로 갖다 쓴다.
- 인터페이스를 구현한 FrontController를 만든다. HttpServlet을 상속하고 @WebServlet을 통해 URL 매핑을 한다. 이 때 모든 URL에서 접근가능하도록 마지막에 /*로 처리한다.
- FrontController 내부적으로는 Map을 만들어 URL을 key, 컨트롤러 구현체를 value로 처리한 매핑정보 MAP을 하나 만든다.
ControllerV1
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입한다. 각 컨트롤러들은 이 인터페이스를 구현하면 된다.
회원 등록 컨트롤러
[MemberFormControllerV1.java]
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 dispather = request.getRequestDispatcher(viewPath);//viewPath로 경로 이동
dispather.forward(request, response); //다른 서블릿이나 JSP로 이동할 수 있는 기능(서버내부에서 다시 호출)
}
}
회원 저장 컨트롤러
[MemberSaveControllerV1.java]
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);
//request 객체에 데이터를 보관해서 뷰에 전달할 수 있음
//뷰는 request.getAttribute()를 사용해서 데이터를 꺼내면 됨.
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
회원 목록 컨트롤러
[MemberListControllerV1]
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); //model에 담아야함 (키,값)
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispather = request.getRequestDispatcher(viewPath);
dispather.forward(request, response);
}
}
그리고 프론트 컨트롤러 만든다.
[FrontControllerServletV1.java]
@WebServlet(name = "FrontControllerServletV1" , urlPatterns = "/front-controller/v1/*") //v1하위에 어떤 게 들어와도 이 서블릿이 무조건 호출
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>(); //key:매핑URL, value:호출될 컨트롤러
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());
//서블릿이 처음 생성이 될 때 controllerMap에 값을 넣어둠
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
// /front-controller/v1/members
String requestURI = request.getRequestURI(); //MemberListControllerV1()객체가 반환됨 (부모가 ControllerV1)
//다형성에 의해 인터페이스로 받을 수 있음
//ControllerV1 controller = MemberListControllerV1(); 부모는 자식을 담을 수 있음
ControllerV1 controller = controllerMap.get(requestURI);
//예외처리
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
/*
먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾는다. 만약 없다면
404(SC_NOT_FOUND) 상태 코드를 반환.
컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행
* */
}
}
프론트 컨트롤러 분석
- localhost:8080/front-controller/v1/new-form의 요청이 오면 위의 FrontContoller가 실행된다.
- 생성자에서 매핑정보를 우선 생성한다. URL주소를 key, ControllerV1 인터페이스 구현체를 value로 Map에 넣어둔다.
- service()가 실행된다.
- request.getRequestURI()를 통해 요청한 URI를 꺼내온다.
- 꺼내온 URI로 controllerMap에서 구현체를 가져온다.(다형성 적용)
- 매핑된 구현체가 없으면 404 처리를 하고, 구현체가 있으면 오버라이딩 메소드(process())를 실행하여 이전에 만들었던 JSP forward로직을 실행한다.
3) View 분리 -V2
v1에선 컨트롤러마다 JSP forward() 로직이 공통적으로 들어가있음 ->이부분 제거하기 위한 수정 진행(별도로 뷰 처리하는 객체)
뷰 객체인 MyView를 만든다.(frontcontroller패키지 밑에 만들어 v1,v2,v3,v4 모두 사용하게끔함)
[MyView.java]
public class MyView {
private String viewPath; // "/WEB-INF/views/new-form.jsp"
public MyView(String viewPath) {
this.viewPath = viewPath;
}
//각 컨트롤러마다 호출했던 forward 로직을 여기서 처리
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
컨트롤러 인터페이스(컨트롤러가 뷰를 반환하는 특징 있음)
[ControllerV2.java]
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
v1과 공통적으로 회원폼, 회원 저장, 회원 목록 컨트롤러 만들음
[MemberFormControllerV2.java] - 회원 등록 폼
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//---------기존 코드
// String viewPath = "/WEB-INF/views/new-form.jsp";
// RequestDispatcher dispather = request.getRequestDispatcher(viewPath);
// dispather.forward(request, response);
//그냥 생성자에 viewpath 넣어주면 됨
return new MyView("/WEB-INF/views/new-form.jsp"); //inline 단축키 ctrl+alt+n
}
}
각 컨트롤러는 복잡한 dispatcher.forward()를 직접 생성해서 호출 안해도 된다. 단순히 MyView 객체 생성하고 거기에 뷰이름만 넣고 반환하면 된다.
[MemberSaveControllerV2.java]-회원 저장
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);
//request 객체에 데이터를 보관해서 뷰에 전달할 수 있음
//뷰는 request.getAttribute()를 사용해서 데이터를 꺼내면 됨.
// String viewPath = "/WEB-INF/views/save-result.jsp";
// RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
// dispatcher.forward(request, response);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
[MemberListControllerV2.java] - 회원 목록
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); //model에 담아야함 (키,값)
// String viewPath = "/WEB-INF/views/members.jsp";
// RequestDispatcher dispather = request.getRequestDispatcher(viewPath);
// dispather.forward(request, response);
return new MyView("/WEB-INF/views/members.jsp");
}
}
프론트 컨트롤러는 다음과 같다. v1에서는 process()를 실행하면 각각의 컨트롤러에서 forward()가 실행된다. v2는 MyView의 render()를 실행하는데, 셋팅한 viewPath를 통하여 forward() 실행된다.
[FrontControllerV2.java]
// /front-controller/v2/members/new-form
//urlPattern을 * 주어 /fornt-controller/v2/ 하위의 모든 URL요청 매핑되어 이 서블릿이 호출됨
@WebServlet(name = "FrontControllerServletV2" , urlPatterns = "/front-controller/v2/*") //v1하위에 어떤 게 들어와도 이 서블릿이 무조건 호출
public class FrontControllerServletV2 extends HttpServlet {
private Map<String, ControllerV2> controllerMap = new HashMap<>(); //key:매핑URL, value:호출될 컨트롤러
//URL별 매핑정보 생성
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());
//서블릿이 처음 생성이 될 때 controllerMap에 값을 넣어둠
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// /front-controller/v2/members
//요청 URI에 맞는 구현체가져옴
String requestURI = request.getRequestURI(); //MemberListControllerV2()객체가 반환됨 (부모가 ControllerV2)
//다형성에 의해 인터페이스로 받을 수 있음
//ControllerV2 controller = MemberListControllerV2(); 부모는 자식을 담을 수 있음
ControllerV2 controller = controllerMap.get(requestURI);
//예외처리
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//new MyView(" /WEB-INF/views/new-form.jsp");
//가져온 구현체의 오버라이딩 메소드 실행
//v2 : 기존과 다르게 viewName을 셋팅한 MyView 객체를 return받은 후, render()를 실행한다.
MyView view = controller.process(request, response);
view.render(request, response);
/*
먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾는다. 만약 없다면
404(SC_NOT_FOUND) 상태 코드를 반환.
컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행
* */
}
}
/*
* 회원 가입 ->front-controller/v2/members/new-form ->FrontControllerServletV2서블릿 호출
* controller이 찾을 때 MemberFormControllerV2()로 찾아서 process 호출
* MemberFormControllerV2에 들어가서 new MyView 생성해서 넘겨줌(return)
* 반환 결과 :new MyView(" /WEB-INF/views/new-form.jsp");
* 반환 결과의 render호출 (MyView클래스의 viewpath는 "/WEB-INF/views/new-form.jsp")
* 호출된 render에서 dispatcher의 view path는 "/WEB-INF/views/new-form.jsp"로 forward됨
*
*
실행실행
- 등록: http://localhost:8080/front-controller/v2/members/new-form
- 목록: http://localhost:8080/front-controller/v2/members
4)Model 추가 - V3
변경할 부분
- 컨트롤러는 HttpServletRequest, HttpServletResponse에 종속적이다. 모델도 컨트롤러에서 request.setAttribute()를 통해서만 사용했다. 우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경해보자(Model 별도로 필요)
- 컨트롤러에서 지정하는 뷰 이름에 중복이 있다. "/WEB-INF/views/" 나 ".jsp" 같은 부분을 제거하기 위해 컨트롤러는 논리 이름만 반환한다
- 기존 중복 포함된 반환명 -> 중복 제거한 논리명
- /WEB-INF/views/new-form.jsp -> new-form
- /WEB-INF/views/save-result.jsp -> save-result
- /WEB-INF/views/members.jsp -> members
모델로 사용할 클래스
[ModelView.java]
뷰의 이름과 뷰를 렌더링할 때 필요한 model객체 가지고 있다. model은 단순하게 map으로 되어 있기 떄문에 컨트롤러에서 뷰에 필요한 데이터를 key, value로 넣어주면 된다.
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;
}
}
[ControllerV3.java]
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap); //V2와 비교해보면 서블릿에 종속적이지 않음
/*
paramMap에 키와 값 넣을 때 -> paramMap.put("email","sample@naver.com");
paramMap.put(request.getParameter("email"));
paramMap.put(request.getParameter("password"));
람다를 쓰면
request
.getParameterNames()
.asIterator(
name =>
paramMap.put(name, request.getParameter(name))
)
*/
}
HttpServletRequest가 제공하는 파라미터-> 프론트 컨트롤러가 paramMap에 담아서 호출해주면 된다.
응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 된다.
[MemberFormControllerV3.java]-회원 등록 폼
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
ModelView를 생성할 때 new-form이라는 view의 논리적 이름 지정한다. 실제 물리적인 이름은 프론트 컨트롤러에서 처리한다.
[MemberSaveControllerV3.java]-회원저장
public class MemberSaveControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
String username = paramMap.get("username"); //단순히 꺼내서 쓰기만 하면 됨
//paramMap.get("username") -> 파라미터 정보가 map에 담겨 있어 map에서 필요한 요청 파라미터 조회하면 된다.
int age = Integer.parseInt(paramMap.get("age")); //문자로 뜨기 때문에 parseInt로 바꿔줘야함
Member member = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);//모델은 단순한 map이므로 모델 뷰에서 필요한 member 객체를 담고 반환해줌
return mv;
}
}
[MemberListControllerV3.java]-회원 목록
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;
}
}
[FrontControllerServletV3.java]
// /front-controller/v2/members/new-form
//urlPattern을 *으로 주어 /front-controller/v3/ 하위의 모든 URL요청 매핑되어 이 서블릿이 호출됨.
@WebServlet(name = "FrontControllerServletV3" , urlPatterns = "/front-controller/v3/*") //v1하위에 어떤 게 들어와도 이 서블릿이 무조건 호출
public class FrontControllerServletV3 extends HttpServlet {
//URL을 key로 하여, ControllerV3 구현체들을 value로 가져올 수 있는 매핑정보 Map
private Map<String, ControllerV3> controllerMap = new HashMap<>(); //key:매핑URL, value:호출될 컨트롤러
//URL별 매핑정보 생성
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());
//서블릿이 처음 생성이 될 때 controllerMap에 값을 넣어둠
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV3.service");
// /front-controller/v2/members
//요청 URL에 맞는 구현체 가져옴
String requestURI = request.getRequestURI(); //MemberListControllerV3()객체가 반환됨 (부모가 ControllerV2)
//다형성에 의해 인터페이스로 받을 수 있음
//ControllerV2 controller = MemberListControllerV2(); 부모는 자식을 담을 수 있음
ControllerV3 controller = controllerMap.get(requestURI);
//예외처리
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//v3 : request, response를 넘겨주지 말고 paramMap을 넘김
Map<String, String> paramMap = createParamMap(request);
//key : paramName, value : request.getParameter(paramName)
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();//논리이름 new-form
// /WEB-INF/views/new-form.jsp
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response); //model을 render에 담아야함
}
//view의 논리이름을 물리이름으로 바꿔줌
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
//request 요청 파라미터를 Map에 셋팅해주는 역할을 함
//HttpServletReuqest에서 파라미터 정보를 꺼내서 Map으로 변환하고 해당 Map(paramMap)을 컨트롤러에 전달하면서 호출
private static Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //getrequest에서 모든 파라미터 이름 가져옴
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); //paramMap에 꺼낸 거 다 넣어줌
return paramMap;
}
}
위의 프론트 컨트롤러를 v2와 비교했을 때 달라진 점은 process(request, response)를 호출하는 것이 아니라 process(paramMap)을 호출한다는 점이다. 서블릿 기술 객체를 넘겨 컨트롤러에서 모델처리를 하는 것이 아니라, 모델처리를 프론트 컨트롤러에서 처리 후(createPraramMap) 리턴 받은 map을 변수로 전달한다는 점이다.
그리고 process()의 처리 결과는 MyView가 아니라 ModelView이다. 컨트롤러에서 ModelView에 논리명을 셋팅하고 model처리를 한 후 반환한다. 이후 반환 받은 ModelView 타입의 변수에서 논리명을 꺼내 물리명으로 viewResolver 처리를 한다.
- 논리 뷰 이름 : members
- 물리 뷰 경로 : /WEB-INF/views/members.jsp
view.render(mv.getModel(), request, response)
- 뷰 객체를 통해 HTML 화면 렌더링
- 뷰 객체의 render()는 모델 정보도 함께 받는다.
- JSP는 rquest.getAttribute()로 데이터 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute()로 담아둔다.
- JSP로 forward해서 JSP 랜더링
[My View.java]
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; // "/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 {
//컨트롤러는 ModelView를 생성하지 않고 model을 매개변수로 받고 View의 논리 이름을 반환한다.
modelToRequestAttribute(model, request);
//모델에 있는 데이터를 forEach를 통해 다 꺼냄
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
//render 로직 모두 호출
}
private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value)-> request.setAttribute(key, value));
}
}
실행
- 등록: http://localhost:8080/front-controller/v3/members/new-form
- 목록: http://localhost:8080/front-controller/v3/members
이후
ModelView->ModelAndView
viewResolver->viewResolver
5) 단순하고 실용적인 컨트롤러 -v4
앞선 v3는 서블릿 종속 제거와 뷰 경로 중복 제거 등으로 개선을 했지만 컨트롤러 구현에서 ModelView 객체를 생성하고 반환해야 하는 부분이 번거롭다. 실용성을 위해 v3를 조금 변경해서 개발자들이 매우 편리하게 개발할 수 있는 v4 버전을 개발해보고자한다.
기본적인 구조는 V3와 같지만 컨트롤러가 ModelView를 반환하지 않고, String 타입의 ViewName만 반환하도록 변경한다.
[ControllerV4.java]
public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
V4는 ModelView가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고 결과로 뷰의 이름만 반환해주면 된다.
[MemberFormControllerV4.java]
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form"; //view의 논리 이름
}
}
[MemberSaveControllerV4.java]
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")); //문자로 뜨기 때문에 parseInt로 바꿔줘야함
Member member = new Member(username, age);
memberRepository.save(member);
//넘겨받은 model에 저장 후 viewName만 반환
model.put("member", member);
return "save-result";
}
}
[MemberListControllerV4.java]
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";
}
}
[FrontControllerServletV4.java]
// /front-controller/v2/members/new-form
@WebServlet(name = "FrontControllerServletV4" , urlPatterns = "/front-controller/v4/*") //v1하위에 어떤 게 들어와도 이 서블릿이 무조건 호출
public class FrontControllerServletV4 extends HttpServlet {
private Map<String, ControllerV4> controllerMap = new HashMap<>(); //key:매핑URL, value:호출될 컨트롤러
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());
//서블릿이 처음 생성이 될 때 controllerMap에 값을 넣어둠
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//요청 URI에 맞는 구현체가져옴
// /front-controller/v4/members
String requestURI = request.getRequestURI(); //MemberListControllerV3()객체가 반환됨 (부모가 ControllerV2)
//다형성에 의해 인터페이스로 받을 수 있음
//ControllerV4 controller = MemberListControllerV4(); 부모는 자식을 담을 수 있음
ControllerV4 controller = controllerMap.get(requestURI);
//예외처리
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//가져온 구현체의 오버라이딩 메소드 실행
//v4:paramMap뿐만 아니라 Model을 같이 넘긴다.
Map<String, String> paramMap = createParamMap(request);
//key : paramName, value : request.getParameter(paramName)
Map<String, Object> model = new HashMap<>();//추가
//모델 객체를 프론트 컨트롤러에서 생성해서 넘겨줌. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게됨.
//컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰 찾기 가능
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response); //model을 render에 담아야함
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
//논리 이름을 물리 이름으로 바꿔줌
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //getreqyest에서 모든 파라미터 이름 가져옴
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); //paramMap에 꺼낸 거 다 넣어줌
return paramMap;
}
}
실행
- 등록: http://localhost:8080/front-controller/v4/members/new-form
- 목록: http://localhost:8080/front-controller/v4/members
6) 유연한 컨트롤러 -v5
어댑터 패턴을 적용하여 상황에 따라 유연하게 컨트롤러를 사용하도록 변경한다. 어떤 상황에서는 ControllerV3를 사용하고 또 다른 상황에서는 ControllerV4를 사용하고 싶을 때 어댑터 패턴을 적용하면 유연한 설계가 가능하다.
- 클라이언트가 요청을 보내면 FrontController 매핑이 되어 핸들러(컨트롤러) 매핑정보에서 해당 컨트롤러를 조회한다. 예를 들어 localhost:8080/front-controller/v5/v3/members/new-form URL 요청이 들어오면 MemberFormControllerV3을 갖고 온다.
- 가져온 핸들러(컨트롤러)에 맞는 핸들러어댑터를 갖고 온다.
- 핸들러어댑터의 handler()를 호출해서 컨트롤러 로직 수행 후 ModelView를 반환한다.
- viewName을 꺼내서 viewResolver를 호출 후 MyView를 반환한다.
- MyView의 render(model)을 호출해서 HTML 응답을 받는다.
핸들러 어댑터 : 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러 호출 가능하다.
핸들러 : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경. 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 전부 처리할 수 있기 때문이다.
[MyHandlerAdapter.java]
public interface MyHandlerAdapter {
boolean supports(Object handler);
//handler -> 컨트롤러
//어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드(처리할 수 있으면 true, 없으면 false)
//ControllerV3를 처리할 수 있는 어댑터를 꺼내야하는데 이 때 쓰는 게 supports
ModelView handler(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
//어댑터는 실제 컨트롤러 호출, 결과로 ModelView 반환
//실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야함
//이전에는 프론트 컨트롤러가 실제 컨트롤러 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출됨
}
[ControllerV3HandlerAdapter.java]
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
// 4-3. 현재 handler는 V4기 떄문에 false
//MemberFormControllerV3 -> ControllerV3의 인스턴스
return (handler instanceof ControllerV3); //controllerV3 인터페이스를 구현한 뭔가가 넘어오면 true 반환
//ControllerV3을 처리할 수 있는 어댑터를 뜻함 -> true 반환됨
}
@Override
public ModelView handler(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
//MemberFormControllerV3
ControllerV3 controller = (ControllerV3) handler; //controllerV3로 캐스팅
//controllerV3로 캐스팅해도 됨 -> supports에서 controllerV3만 지원한다고 했기 떄문
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
//handler에서는 modelview를 반환하면 됨
//adapter의 역할은 handler를 호출해주고 그 결과의 반환 타입을 modelView로 맞춰서 보내줌
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //getreqyest에서 모든 파라미터 이름 가져옴
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); //paramMap에 꺼낸 거 다 넣어줌
return paramMap;
}
}
[ControllerV4HandlerAdapter.java]
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
//4-4 handler는 V4니까 true
return (handler instanceof ControllerV4); //핸들러가 V4 버전을 지원하는지 체크
}
@Override
public ModelView handler(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler; //5-3 ControllerV4로 캐스팅
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
//viewName 반환
String viewName = controller.process(paramMap, model); //5-4 paramMap과 model 넘겨줌 -> viewName 반환
//view셋팅
ModelView mv = new ModelView(viewName); //5-5 반환받은 viewName으로 ModelView 만듬
//model 셋팅
mv.setModel(model); //5-6 model까지 값을 셋팅해주고
return mv; //5-7 modelview 반환
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //getreqyest에서 모든 파라미터 이름 가져옴
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName))); //paramMap에 꺼낸 거 다 넣어줌
return paramMap;
}
}
- supports() : 핸들러(컨트롤러)가 ControllerV3 구현체인지 확인한다. -> 타입 변환 걱정없이 실행해도 된다.
- handle() : 넘겨받은 핸들러(컨트롤러)를 ControllerV3 또는 ControllerV4로 캐스팅 후, 버전에 맞는 process처리를 한다.
[FrontControllerV5Servelt.java]
@WebServlet(name = "frontControllerServletV5" , urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
//private Map<String, ControllerV4> controllerMap = new HashMap<>();
private final Map<String, Object> handlerMappingMap = new HashMap<>();
//기존에는 ControllerV4가 들어갔는데 지금은 V3든 V4든 다 지원하기 위해 <String, Object>로 설정
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()); //2.들어가서 찾으면 이 주소가 호출되면서 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());
// 4-1.init어댑터에 V3,V4가 들어가 있음 -> 먼저 V3에서 포트 호출
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1. FrontController에서 핸들러 맵핑 정보를 찾음 (getHendler) : MemberFormControllerV3, MemberFormControllerV4
// 2. MemberFormControllerV4가 반환됨
Object handler = getHandler(request); //핸들러 매핑
if (handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//핸들러 처리할 수 있는 어댑터 조회
MyHandlerAdapter adapter = getHandlerAdapter(handler); // 3.MemberFormControllerV4가 반환 되면 어댑터를 찾음
//5-1. adapter는 ControllerV4HandlerAdapter 반환됨
ModelView mv = adapter.handler(request, response, handler); //5-2 V4의 Handler 호출
String viewName = mv.getViewName();//논리이름 new-form
MyView view = viewResolver(viewName);
//6. viewName으로 viewResolver돌림
view.render(mv.getModel(), request, response); //model을 render에 담아야함
//7. view render함
}
//핸들러 매핑 정보인 handlerMappingMap에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI); //
}
@Override
protected long getLastModified(HttpServletRequest req) {
return super.getLastModified(req);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
//3. 어댑터 찾음 -> handler는 MemberFormControllerV4임
for (MyHandlerAdapter adapter : handlerAdapters) { //iter : for문 자동처리
if (adapter.supports(handler)){ //adapter가 handler를 지원하는가
// 4-2.supports에서 포트 호출 -> ControllerV3HandlerAdapter
//4-3 false라 한바퀴 더 돌아서 ControllerV4HandlerAdapter
return adapter; //4-5 어댑터 반환(ControllerV4HandlerAdapter)
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler =" + handler);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
//논리 이름을 물리 이름으로 바꿔줌
}
}
- initHandlerMappingMap() : 이전 버전들의 controllerMap이다. <Key,Value>가 <String, ControllerV>가 아닌 아무 값이나 받을 수 있는 <String, Object>가 된다. 지금은 V3, V4로만 예를들고 있다.
- initHandlerAdapter() : 만든 핸들러 어댑터를 담는다.
- service()
- getHandler() : 요청에 맞는 컨트롤러를 가져온다.
- getHandlerAdapter() : handlerAdapters에 지금은 ControllerHandlerAdapterV3, ControllerHandlerAdapterV4만 있지만 V4, V5도 추가될 것이다. List<MyHandlerAdapter> 타입의 handlerAdapters를 반복하며 넘겨받은 handler가 어떤 버전의 컨트롤러인지 체크(support())한 후에 해당 어댑터를 반환해준다.
- 반환한 어댑터의 handle() 호출을 통하여 컨트롤러 로직 수행 후 ModelView를 반환 받는다.
- 이후 똑같은 render() 처리가 진행된다.
정리
- v1 : 프론트 컨트롤러를 도입
- 기존 구조를 최대한 유지하면서 프론트 컨트롤러 도입
- v2 : View 분류
- 단순 반복되는 뷰 로직 분리
- v3 : Model 추가
- 서블릿 종속성 제거
- 뷰 이름 중복 제거
- v4 : 단순하고 실용적인 컨트롤러
- v3와 거의 비슷
- 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공
- v5 : 유연한 컨트롤러
- 어댑터 도입
- 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계
여기에 애노테이션을 사용해서 컨트롤러를 더 편리하게 발전시킬 수도 있다. 어떻게? 바로 애노테이션을 지원하는 어댑터를 추가하면 된다! 다형성과 어댑터 덕분에 기존 구조를 유지하면서 프레임워크의 기능을 확장할 수 있다.
'Spring' 카테고리의 다른 글
[Spring MVC] 로그인 처리 (1) - 쿠키, 세션 (1) | 2023.10.10 |
---|---|
[Spring MVC] 스프링 MVC(5) (0) | 2023.06.13 |
[Spring MVC] 스프링 MVC(4) (0) | 2023.06.01 |
[Spring MVC] 스프링 MVC (3) (0) | 2023.05.15 |
[Spring MVC] 스프링 MVC (1) (0) | 2023.05.09 |