



💡 기존 코드를 최대한 유지하면서 프론트 컨트롤러를 도입해보자.

먼저 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 그러고 나서 각 컨트롤러들은 이 인터페이스를 구현하면 되는 것이다.
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);
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);
}
}
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/frontcontroller/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);
}
}
urlPatterns
urlPatterns = "/front-controller/v1/*" : /front-controller/v1 를 포함한 하위 모든 요청을 이 서블릿에서 받아들인다.controllerMap
key : 매핑 URLvalue : 호출될 컨트롤러service()
requestURI 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾는다. controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다. JSP
💡 컨트롤러에서 뷰로 이동하는 부분의 중복을 해결해보자.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

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 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");
}
}
각 컨트롤러는 dispatcher.forward() 를 직접 생성해서 호출하지 않아도 된다. 단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환한다.
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/frontcontroller/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);
}
}
프론트 컨트롤러의 도입으로 MyView 객체의 render() 를 호출하는 부분을 일관되게 처리해 최종적으로 JSP 가 실행된다. 즉, 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환만 해주면 된다 !
💡 서블릿 종속성을 제거하기 위해 Model 을 직접 만들고, View 이름까지 전달해보자.
서블릿 종속성 제거
HttpServletRequest, HttpServletResponse 가 꼭 필요할까 ?Map 으로 대신 넘기도록 하고, request 객체를 Model 로 사용하는 대신 Model 객체를 별도로 만들어서 반환해보자. -> 구현 코드가 단순해지고, 테스트 코드 작성도 쉬울 것이다 !
뷰 이름 중복 제거
컨트롤러에서 뷰의 논리 이름을 반환하고, 실제 물리적 위치 이름은 프론트 컨트롤러에서 처리하도록 단순화하자.
/WEB-INF/views/new-form.jsp -> new-form
/WEB-INF/views/save-result.jsp -> save-result
/WEB-INF/views/members.jsp -> members
-> 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다 !

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;
}
}
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
ModelView 를 생성할 때, new-form 이라는 view의 논리적인 이름을 지정한다. 실제 물리적인 이름은 프런트 컨트롤러에서 처리한다. 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"); // 파라미터 정보가 map 에 담겨있음.
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); // 모델에 뷰에서 필요한 member 객체를 담고 반환함.
return mv;
}
}
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/frontcontroller/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;
}
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
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;
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
MyView view = viewResolver(viewName);
컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경, 그리고 실제 물리 경로가 있는 MyView 객체를 반환한다.
논리 뷰 이름 : members
물리 뷰 경로 : /WEB-INF/views/members.jsp
view.render(mv.getModel(), request, response);
뷰 객체 -> HTML 화면 렌더링
뷰 객체의 render() 는 모델 정보도 함께 받음.
JSP 는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둠.
JSP로 포워드 해서 JSP 렌더링
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);
}
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 void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
💡 개발자가 단순하고 편리하게 사용할 수 있는 컨트롤러를 구현해보자.
기본적인 구조는 V3 과 같지만, 컨트롤러가 ModelView 를 반환하지 않고, ViewName 만 반환한다.

public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
ModelView 가 없기 때문에 model 객체는 파라미터로 전달되어 그냥 사용하면 되고, 결과로 뷰의 이름만 반환해주면 된다.
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";
}
}
model 추가만 해주면 된다.@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);
}
💡 어댑터 패턴 을 적용해 다른 방식의 컨트롤러 인터페이스를 호환해보자.

핸들러 어댑터 : 어댑터 역할을 해줘 다양한 종류의 컨트롤러를 호출할 수 있다.
핸들러 : 컨트롤러의 개념 (더 넓은 개념)
MyHandlerAdapter
public interface MyHandlerAdapter {
boolean supports(Object handler); // 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; // 어댑터는 실제 컨트롤러를 호출 -> ModelView 반환, 이전에는 프론터 컨트롤러가 실제 컨트롤러 호출했다면 이제는 이 어댑터 통해서 호출
}
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
// ControllerV3 을 처리할 수 있는 어댑터
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
// handler를 컨트롤러 V3로 변환한 후, V3 형식에 맞도록 호출 -> ModelView 반환
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
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;
}
}
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/frontcontroller/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); // 실제 어댑터 호출
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
// URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
// 핸들러를 처리할 수 있는 어댑터 조회
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
// handler가 ControllerV3 인터페이스를 구현했다면, ControllerV3HandlerAdapter 객체가
반환
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
ControllerV4 기능도 추가해보자.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 추가
}
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) { // handler 가 ControllerV4 인 경우에만 처리하는 어댑터
return (handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
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 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;
}
}
1. v1 : 프론트 컨트롤러 도입
2. v2 : View 분리
3. v3 : Model 추가
4. v4 : 단순하고 실용적인 컨트롤러
5. v5 : 유연한 컨트롤러