스프링 MVC-프론트 컨트롤러
강의에 따라 총 5번의 리팩토링을 진행할 것이다. 첫 번째는 프론트 컨트롤러를 적용하는 것이다.
V1
프론트 컨트롤러는 위와 같은 방식으로 작동한다. 기존에 각각의 서블릿을 구현한 컨트롤러 셋이 작동했다면, 이제는 프론트 컨트롤러를 적용하여 서블릿은 프론트 컨트롤러 하나만 구현하고, 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출해주는 것이다.
이로써 나머지 컨트롤러는 서블릿을 구현하지 않아도 되고, 공통 관심 사항을 적용하기 쉬워지는 장점이 있다.
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 jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 {
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 {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if(controller==null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request,response);
}
}
프론트 컨트롤러를 구현한 코드이다. 요청의 URI로부터 어떤 컨트롤러를 호출하는지 알아낸 후 미리 만들어놓은 컨트롤러 맵에서 알맞은 컨트롤러를 찾아 연결한 후 내부 로직을 실행하는 것이다.
이로써 위에서 서술한 서블렛의 미구현, 공통 관심사항 적용 등의 장점이 구현된 것을 볼 수 있다.
이제 중복된 코드들을 지우며 리팩토링을 계속해보자.
V2
기존 코드에서는 컨트롤러 마다 jsp로, 즉 뷰로 이동하는 부분이 중복되어 있었다. 따라서 앞으로는 view로의 이동을 처리하는 객체를 따로 만들어 처리함으로써 중복을 제거하도록 했다.
package hello.servlet.web.frontcontroller;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class MyView {
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);
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));
}
}
view로의 이동을 전담하는 MyView 객체이다. view의 경로를 받아 생성된 후 외부에서 render() 메서드를 통해 view를 띄워주는 역할을 한다.
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");
}
}
이제 컨트롤러에서는 view로의 이동까지 전담하지 않고, MyView 객체를 생성하며 view 경로를 넘겨주고 반환할 뿐이다.
이후의 처리는 반환된 MyView 객체를 이용하여 프론트 컨트롤러에서 전담한다.
V3
기존 코드에서는 HTTP의 request 내부에 있는 저장소를 model로써 사용하였다. 이젠 model 역할을 할 객체를 새로 만듬으로써 코드가 서블릿에 종속되는 것을 방지할 것이다. 이렇게 하면 코드도 단순해지고, 테스트를 작성하기도 쉬워진다.
package hello.servlet.web.frontcontroller;
import java.util.HashMap;
import java.util.Map;
public class ModelView {
String viewName;
Map<String,Object> model = new HashMap<>();
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;
}
public ModelView(String viewName) {
this.viewName=viewName;
}
}
view의 이름과 view를 렌더링 할 때에 필요한 정보들을 담고 있는 ModelView 객체이다. getter와 setter를 통해 외부에서 접근이 자유롭도록 구현되었다.
public ModelView process(Map<String, String> paraMap) {
List<Member> members = memberRepository.findAll();
ModelView mv= new ModelView("members");
mv.getModel().put("members",members);
return mv;
}
이젠 컨트롤러에서 MyView 객체 대신 ModelView 객체를 반환한다. 더이상 서블릿을 받을 필요가 없으므로 필요한 파라미터만 프론트 컨트롤러에서 Map의 형태로 매개변수로 넣어준다.
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
프론트 컨트롤러에서는 ModelView에서 view의 논리 경로를 받아 view의 절대 경로를 가진 MyView 객체를 반환하는 viewResolver 메서드가 추가되었다. 이로써 컨트롤러에서는 view의 논리 경로만 입력하면 된다.
V4
매번 컨트롤러가 ModelView 객체를 만들어 반환하는 것보단, 논리경로만을 반환하는 것으로 충분하다고 판단, 리팩토링을 진행했다.
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";
}
}
이젠 컨트롤러에서 ModelView 객체 대신 view의 논리 경로만을 반환한다. model 역할은 프론트 컨트롤러에서 파라미터로 넣어준 Map으로 대체한다.
V5
이제 컨트롤러의 버전이 많아진 만큼, 여러 버전의 컨트롤러를 동시에 사용할 수 있는 유연한 형태가 필요하다. 이럴 때 사용하는 것이 ‘어댑터 패턴’이다.
어댑터 패턴은 위 사진과 같이 작동한다. 핸들러는 컨트롤러와 동의어로 사용된다고 보면 된다. 이제 우리는 컨트롤러에 직접 접근하지 않는다.
필요한 컨트롤러가 있는지 조회한 후, 해당 핸들러를 처리할 수 있는 어댑터가 있는지도 조회한다. 있다면 핸들러 어댑터를 통해서 컨트롤러에 접근한다.
핸들러 어댑터는 버전이 달라도 컨트롤러를 프론트 컨트롤러에서 사용할 수 있도록 처리해주는 역할을 한다.
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 jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
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 Map<String, String> createParamMap(HttpServletRequest request) {
Map<String,String> paramMap = new HashMap<>();
request.getParameterNames().asIterator().
forEachRemaining(paramName->paramMap.put(paramName, request.getParameter (paramName)));
return paramMap;
}
}
V3버전 컨트롤러를 지원하는 핸들러 어댑터이다. supports() 메서드로 해당 컨트롤러의 버전을 확인하고, request에서 파라미터들을 받아 온 후 컨트롤러에게 넘겨준다.
기존 프론트 컨트롤러에서가 아닌 어댑터를 통해 컨트롤러에 접근하는 만큼, 프론트 컨트롤러가 하던 일 중 일부가 넘어온 것을 볼 수 있다.
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 hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 {
final Map<String, Object> handlerMappingMap=new HashMap<>();
final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
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());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
@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) {
MyHandlerAdapter a;
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();
return handlerMappingMap.get(requestURI);
}
최종 버전의 프론트 컨트롤러 코드이다. 이제 컨트롤러 맵을 만드는 대신 비슷한 역할을 하는 핸들러 맵과 어댑터 맵이 추가되었다.
request의 URI를 읽고 그에 맞는 핸들러를 가져오는 과정까지는 동일하지만, 해당 핸들러에 맞는 핸들러 어댑터를 찾는 getHandlerAdapter() 메서드가 추가되었다.
기존엔 컨트롤러에서 ModelView를 반환받았지만 이젠 어댑터를 통해 컨트롤러에 접근한 후, ModelView를 반환받는다. 이러한 어댑터 패턴을 통해 이전보다 더 유연한 구조의 MVC 패턴이 된 것을 확인할 수 있다.
다음 포스팅에서는 지금의 이 최종버전 MVC 패턴과 아주 유사한, 스프링 MVC 패턴에 대해 알아보도록 하겠다.