[Spring-MVC] MVC 프레임워크 만들기

나영·2023년 7월 15일

Spring-MVC

목록 보기
4/7
post-thumbnail

프론트 컨트롤러 패턴

프론트 컨트롤러란 ?

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 입구를 하나로
  • 공통 처리 기능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿 사용 안해도 X

프론트 컨트롤러의 도입 - v1

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

구조

  • ControllerV1

먼저 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 그러고 나서 각 컨트롤러들은 이 인터페이스를 구현하면 되는 것이다.

public interface ControllerV1 {

	void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
  • MemberFormControllerV1 - 회원 등록 컨트롤러
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);
   }
}
  • MemberSaveControllerV1 - 회원 저장 컨트롤러
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);
   }
}
  • MemberListControllerV1 - 회원 목록 컨트롤러
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);
   }
}
  • FrontControllerServletV1 - 프론트 컨트롤러
@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 : 매핑 URL
    • value : 호출될 컨트롤러
  • service()

    • requestURI 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾는다.
    • 만약 없다면, 404 상태코드를 반환하고, 있다면 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다.
  • JSP

    • 이전 MVC에서 사용했던 것을 그대로 사용한다.

View 분리 - v2

💡 컨트롤러에서 뷰로 이동하는 부분의 중복을 해결해보자.

String viewPath = "/WEB-INF/views/new-form.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);
   }
}
  • ControllerV2
public interface ControllerV2 {
 	MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
  • MemberFormControllerV2 - 회원 등록 폼
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 객체를 생성하고 거기에 뷰 이름만 넣고 반환한다.

  • FrontControllerServlet2 - 프론트 컨트롤러
@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 추가 - v3

💡 서블릿 종속성을 제거하기 위해 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

    -> 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다 !

구조

  • ModelView
    뷰의 이름과 뷰를 렌더링할 때 필요한 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;
    }
}

  • ControllerV3
public interface ControllerV3 {
 	ModelView process(Map<String, String> paramMap);
}
  • MemberFormControllerV3 - 회원 등록 폼
    ModelView 를 생성할 때, new-form 이라는 view의 논리적인 이름을 지정한다. 실제 물리적인 이름은 프런트 컨트롤러에서 처리한다.
public class MemberFormControllerV3 implements ControllerV3 {
   @Override
   public ModelView process(Map<String, String> paramMap) {
   		return new ModelView("new-form");
   }
}
  • MemberSaveControllerV3 - 회원 저장
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;
   }
}
  • FrontControllerServletV3 - 프런트 컨트롤러
@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));
   }
}

단순하고 실용적인 컨트롤러 - v4

💡 개발자가 단순하고 편리하게 사용할 수 있는 컨트롤러를 구현해보자.

구조

기본적인 구조는 V3 과 같지만, 컨트롤러가 ModelView 를 반환하지 않고, ViewName 만 반환한다.

  • ControllerV4
public interface ControllerV4 {
     /**
     * @param paramMap
     * @param model
     * @return viewName
     */
 	 String process(Map<String, String> paramMap, Map<String, Object> model);
}

ModelView 가 없기 때문에 model 객체는 파라미터로 전달되어 그냥 사용하면 되고, 결과로 뷰의 이름만 반환해주면 된다.

  • MemberSaveControllerV4
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";
   }
}
  • FrontControllerServletV4
    기존 코드에서 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);
 }

유연한 컨트롤러 - v5

💡 어댑터 패턴 을 적용해 다른 방식의 컨트롤러 인터페이스를 호환해보자.

구조

  • 핸들러 어댑터 : 어댑터 역할을 해줘 다양한 종류의 컨트롤러를 호출할 수 있다.

  • 핸들러 : 컨트롤러의 개념 (더 넓은 개념)

  • MyHandlerAdapter

public interface MyHandlerAdapter {

 	boolean supports(Object handler); // 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단
    
 	ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; // 어댑터는 실제 컨트롤러를 호출 -> ModelView 반환, 이전에는 프론터 컨트롤러가 실제 컨트롤러 호출했다면 이제는 이 어댑터 통해서 호출
}
  • ControllerV3HandlerAdapter - ControllerV3 을 지원하는 어댑터
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;
   }
}
  • FrontControllerServletV5
@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");
   }
}
  • V4 추가
    FrontControllerServletV5 에 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 추가
}
  • ControllerV4HandlerAdapter
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 : 단순하고 실용적인 컨트롤러

  • v3 와 거의 비슷
  • ModelView 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공

5. v5 : 유연한 컨트롤러

  • 어댑터 도입
  • 프레임워크 유연하고 확장성 있게 설계

0개의 댓글