출처 : https://inf.run/ApBk
해당 강의를 유료 구매 후 수강 후 기록 및 복습 용으로 포스팅합니다.
프론트 컨트롤러 도입 이전 상황을 봐보자.

그냥 각자 호출하면 각자 나오는 형식이었다.
하지만 프론트 컨트롤러를 도입하면?

사실 공통된 기능들의 대다수는 보통 사용자의 요청을 받는 기능이다.
즉, 서블릿이 기존에 했던 역할들이다.
이제 프론트 컨트롤러를 서블릿으로 구현하고
요청에 맞는 컨트롤러를 찾아서 호출해준다면
프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
스프링의 경우 DispatcherServlet이 FrontController 패턴으로 구현되어있다.
이제 단계적으로 프론트 컨트롤러를 도입해보자.
단계가 점점 올라감에 따라 어떻게 개선되어가는지 지켜보자.

public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, 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);
}
}
저장 컨트롤러)
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);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
목록 컨트롤러)
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);
}
}
이제 가장 중요한 프론트 컨트롤러를 만들어보자.
FrontControllerV1
@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");
// /front-controller/v1/members/new-form
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if(controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
urlPatterns에 의 경우 모든걸 받아들인단 이야기다.
/front-controller/v1/ 라고 한다면
/front-controller/v1을 포함한 하위 모든 요청을 받아들인단 소리다.
지금 service함수의 기능을 보면 먼저 requestURI를 조회해서 실제 호출할 컨트롤러를 Map에서 찾는다. 만약 없으면 404 코드를 반환한다.
컨트롤러를 찾아서 controller.process(request,response)를 호출해서 해당 컨트롤러를 실행하고 있다.
여기서 수정해야할 부분들이 뭐가 있을까?
우선 모든 컨트롤러들에서 중복되는 부분들이 보인다.
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
이 부분을 분리하기 위해 따로 뷰를 처리하는 객체를 만들어보자.

우선 뷰를 처리할 객체를 만들어줬다.
이름을 MyView라고 하겠다.
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);
}
}
아까 ControllerV1은 반환형이 void였다. 하지만 이번 V2는 이제 뷰 객체를 반환한다.
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, 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");
}
}
저장 컨트롤러)
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);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
이제 프론트컨트롤러를 봐보자.
@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 {
// /front-controller/v1/members/new-form
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);
}
}
ControllerV2의 반환 타입이 MyView이므로 view.render()을 하면 forward 로직을 수행해서 JSP가 실행된다.
프론트 컨트롤러의 도입으로 MyView 객체의 render()를 호출하는 부분을 모두 일관되게 처리할 수 있게 되었다.
여기서 조금 더 수정해야할 부분들을 봐보자.
컨트롤러에 입장에서, HttpServletRequest, HttpServletResponse가 꼭 필요할까?
우리가 프론트 컨트롤러를 만든 목적이 프론트 컨트롤러가 servlet역할을 하고 나머지 컨트롤러들은 servlet을 몰라도 기능하게 만드는 것이 목표였다.
요청 파라미터 정보를 자바의 Map으로 넘기게 하면 가능하다.
그리고 request를 우리가 지금까지 request.setAttribute()처럼 모델로 사용했었는데 이제 그러지 말고 별도의 Model 객체를 만들어서 사용해보자.
또 뷰 이름도 자세히 보며 절대 경로들이 겹친다.
그 실제 물리 위치 이름들은 그냥 프론트 컨트롤러에서 처리하도록 만들어보자.

지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다.
그리고 Model도 request을 활용해서 데이터를 저장했다.
이제 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View이름까지 전달하는 객체를 만들어보자.
ModelView)
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)
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
이제 컨트롤러들은 서블릿 기술을 전혀 활용하지 않는다. 그냥 맵으로 인자를 던져주면 된다.
HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해주면 된다.
회원 등록)
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
이제 모델을 보내면서 그 안에 논리 이름만 담아서 보내주면 된다.
회원 저장)
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;
}
}
모델은 단순한 map이기 때문에 모델에 뷰에 필요한 member 객체를 넣고 반환했다.
FrontControllerV3)
@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 {
// /front-controller/v1/members/new-form
String requestURI = request.getRequestURI();
ControllerV3 controller = controllerMap.get(requestURI);
if(controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName(); // 논리이름 new-form
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
// paramMap
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator().
forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
v4는 v3에서 약간 바뀌었다.
개발자 입장에서 생각해보면 ModelView를 계속 반환하는 것이 불편하다.
그냥 ViewName만 반환했으면 좋겠다.
그래서 인자에 이제 model객체도 같이 넣어줄 것이다.
ControllerV4
public interface ControllerV4 {
/**
*
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String,String> paramMap, Map<String,Object> model);
}
등록)
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
너무나 간단해졌다.
그냥 뷰의 논리 이름만 반환하면 된다.
저장)
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";
}
}
이젠 모델이 파라미터로 전송되기 때문에, 모델을 직접 생성하지 않고 그냥 put 해주면 된다.
FrontControllerV4
@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;
}
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); // 모델 추가.
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
// paramMap
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator().
forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
이제 모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다.
컨트롤러에서 모델 객체에 값을 담으면 그대로 있게 된다.
이제 단순하고 실용적으로 변화했다.
v1부터 v4까지의 변천 과정을 보면 너무나 코드가 간결해졌다.
프론트 컨트롤러가 조금 더 많은 역할을 하지만 나머지 기존의 컨트롤러들은 이제 너무나 가벼워졌기에
개발자가 컨트롤러를 그냥 만들기 편리해졌다.