김영한 강사님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요
이번강의에서는, MVC 패턴에서의 반복되는 코드와, 공통처리의 불편함을 해결하고자 MVC 프레임워크를 만들어본다. Spring MVC 프레임워크를 사용하면 되지 않냐? 라는 질문에 이렇게 하나하나 문제점을 해결해 보면서 Spring MVC와 유사한 프레임 워크를 만들어봄에 따라 코딩실력의 증가와 Spring MVC 컨테이너를 잘 이해하기 위함이다라고 답하며 시작해 보도록 하겠다.

공통 Controller(Front Controller)을 이용해서 반복을 처리해보겠다!!!
프론트 컨트롤러는 이 인터페이스를 의존! -> 구현과 관계없이 로직의 일관성 + DIP도 잘 지킬 수 있다!!!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 {
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);
}
}
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(); // 요청받는 URI를 얻을 수 있다
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request,response);
}
}
V1을 성공적으로 만들었다... 그런데 컨트롤러에서 뷰로 이동하고 랜더링 하는 부분의 코드가 중복된다
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);
}
}
dispatcher.forward()를 직접 호출하지 않고, MyView 객체를 생성하고 그 안에 ViewPath만 넣고 반환해주면 된다!!!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 {
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");
}
}
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");
}
}
@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 {
System.out.println("FrontControllerServletV2.service");
String requestURI = request.getRequestURI(); // 요청받는 URI를 얻을 수 있다
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);
}
}
모델 V3의 핵심은 컨트롤러에서 서블릿의 종속성을 없애자는 것이다. MVC패턴의 model부분도 request를 이용하지말고, Model 부분의 객체를 별도로 만들어보자! + 유지보수를 위해 view이름의 중복도 제거할 것이다.

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 static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key,value));
}
}
public class ModelView {
private String ViewName; // view의 논리적인 이름
private Map<String,Object> model = new HashMap<>();
public ModelView(String viewName) {
ViewName = viewName;
}
public String getViewName() {
return ViewName;
}
public void setViewName(String viewName) {
ViewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
다음은 Controller3(인터페이스)이고 특이하게 ModelView를 반환 받는 메서드를 가지고 있다
public interface ControllerV3 {
ModelView process(Map<String,String> 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;
}
}
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;
}
}
@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 {
System.out.println("FrontControllerServletV3.service");
String requestURI = request.getRequestURI(); // 요청받는 URI를 얻을 수 있다
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();// 논리이름만 얻을 수 있다. ex) new-form
/// WEB-INF/views/new-form.jsp
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;
}
}
request.getRequestUrl을 이용해, ControllerMap안에 있는 ControllerV3을 Controller에 받는다앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복도 제거하고 잘 설계된 컨트롤러이다.. 그런데 좋은 프레임 워크는 아키텍쳐도 중요하지만 실제 개발하는 개발자가 단순하고 편리하게 이용할 수 있어야한다
그런데 V3은 컨트롤러 인터페이스를 구현하는 개발자 입장에서는 항상 ModelView객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다

public interface ControllerV4 {
String process(Map<String,String> paramMap,Map<String,Object> model);
}
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form"; // modelView 없이 그냥 문자열로 반환!
}
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";
}
}
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";
}
다음은 Front Controller 이다
@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 {
System.out.println("FrontControllerServletV4.service");
String requestURI = request.getRequestURI(); // 요청받는 URI를 얻을 수 있다
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<>(); // 추가 -> 컨트롤러에서 modelView 생성하던거 그냥 프론트 컨트롤러에서 빈거 만들고, 추가만 해준다
String viewName = controller.process(paramMap, model);
/// WEB-INF/views/new-form.jsp
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;
}
}

핸들러 : 컨트롤러의 이름이 더 넓은 범위인 핸들러로 변경되었다. 어댑터가 잇기 때문에 컨트롤러 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 처리가 가능하기 때문이다핸들러 어댑터 : 중간에 어댑터 역할을 한다. 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 Front Controller에서 호출할 수 있다먼저 어뎁터용 인터페이스이다 인터페이스로 만들어서 V3,V4의 구현한 어뎁터를 이용해 V5에서 V3,V4방식을 사용할 수 있도록 해보겠다
public interface MyHandlerAdapter {
boolean support(Object handler); // handler가 넘어왔을 때, 이 컨트롤러(핸들러) 지원할 수 있는지
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean support(Object handler) {
return handler instanceof ControllerV3;
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV3 controllerV3 = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controllerV3.process(paramMap);
return mv;
}
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;
}
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean support(Object handler) {
return (handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
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;
}
}
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/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());
// 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());
}
@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();// 논리이름만 얻을 수 있다. ex) new-form
/// WEB-INF/views/new-form.jsp
MyView view = viewResolver(viewName);
view.render(mv.getModel(),request,response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
MyHandlerAdapter a;
for (MyHandlerAdapter adapter : handlerAdapters) {
if(adapter.support(handler)){
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
먼저 핸들러 매핑 정보를 저장할 handlerMappingMap과 핸들러 어댑터 목록을 저장할 handlerAdapters의 리스트를 만들어준다. 그리고 이것들을 생성자에 잘 넣어준다
getHandleAdapter 메서드를 이용해 안에서 for문과 support 메서드로 핸들러 어뎁터 목록에서 어뎁터를 찾는다!!!
찾은 어뎁터를 이용해 handle메서드를 이용해 ModelView를 받는다
viewResolver메서드를 이용해, 절대 경로를 반환받고 이를 렌더링한다
이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 어뎁터가 컨트롤러를 호출해(handle메서드를 통해), ModelView를 받는다
어댑터를 사용하기 때문에, 컨트롤러 뿐만 아니라 어댑터가 지원하기만 하면 어떤 것이라도 URL에 매핑해서 사용할 수 있다. 그래서 이름을 컨트롤러에서 더 넓은 범위의 핸들러로 변경한다
그런데 어댑터는 뷰의 이름이 아니라 ModelView를 반환해야 한다 -> 어뎁터가 필요한 이유
서로 다른 컨트롤러를 지원할때, 형식을 맞추어서 반환하게 된다
V4에서도 반환받은 String 즉 이름을 가지고 ModelView객체를 새로 만들고 거기 안에 setter을 이용해 파라미터 model정보를 넣어준 후에 반환한다
V3은 어뎁터와 반환 로직인 ModelView가 비슷해서 단순하지만 V4에서 왜 어뎁터가 필요한지 반환을 맞춰주는 어뎁터의 진가가 나온다!!!