들어가며,
어플리케이션을 만드는데 기본적으로 사용되는 MVC 패턴을 공부하기 전에 Servlet과 JSP에서 MVC패턴으로 변화되는 과정에서 중요한 패턴인 프론트 컨트롤러에 대해 공부해보려고 한다.
MVC패턴은 request를 받아서 컨트롤러가 정상적인 HTTP 요청인지 확인 후 서비스, 리포지토리 계층에 요청을 전달한다. 그러면 서비스 계층에서는 비즈니스 로직을, 리포지토리에서는 DB로 전달을 하게 된다.
이 동작을 통해 Model이라는 도메인에 데이터를 전달하고 참조할 수 있게 되는 것이다.
마지막으로 View에서는 response를 내보내면 client가 확인할 수 있다.
프론트 컨트롤러란?
이전에 request를 Servlet으로 받아 처리할 때는 각 컨트롤러가 직접 요청을 받아 처리했는데 Front Controller를 도입하면 먼저 Front Controller에서 클라이언트의 모든 요청 값들을 받아서 각 컨트롤러에 맞게 전달해준다.
이를 통해 요청 처리의 일관성을 유지하고 중복 코드를 줄일 수 있으며, 보안, 로깅 등의 부가적인 작업을 효율적으로 수행할 수 있다.
@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 {
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);
}
}
FrontControllerServletV1 생성자를 통해 등록된 controllerMap을 통해 클라이언트 요청을 각 컨트롤러에 매핑한다.
service 메서드는 HttpServletRequest와 Response를 받아서 URI를 조회하고 controllerMap에서 객체를 찾아온다.
객체 값이 null인 경우 NOT_FOUND 예외 처리를 한다.
request와 response를 받아 process 메서드를 동작한다.
controllerMap 구성
- key: 매핑 URL
- value: 호출될 컨트롤러
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);
}
}
MyView라는 중간 단계를 두어 Controller가 Jsp에 직접 전달하지 않고 Front Controller를 거치게 설계됐다.
@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 view = controller.process(request, response);
view.render(request, response);
}
}
이제 각각의 Controller는 MyView만 반환하면, 프론트 컨트롤러에서 render메서드를 통해 일관되게 처리해준다.
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;
}
}
서블릿 종속성을 제거하는 것이 목적이다.
1. ModelView를 통해 컨트롤러는 논리 View 이름을 반환하게 되며, 이를 View Resolver를 통해 실제 View 경로로 변경해준다.
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; } }
2. 요청 파라미터에서 서블릿을 제거하고 Map을 통해 넘겨준다.
Spring MVC 구조
Spring MVC 구조에서는 Front Controller가 Dispatcher Servlet으로 구성되어 있다.
이외에도 어댑터를 통해 핸들러(컨트롤러)를 유연하게 사용할 수 있다는 장점이 있다.