MVC 프레임워크 만들기

유동우·2023년 4월 13일
0
post-thumbnail

프론트 컨트롤러 패턴 소개

FrontController 패턴 특징

  • 프론트 컨트롤러 하나 (입구가 하나)로 클라이언트의 요청을 받는다

  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출해준다

  • 프론트 컨트롤러로 공통으로 묶어서 사용이 가능하다

  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다


프론트 컨트롤러 도입 -v1

클라이언트의 HTTP 요청 -> Front Controller가 받고 Mapping 정보에서 나한테 Mapping된 controller를 찾는다 -> 찾아서 controller 호출 -> View (JSP) forward -> JSP 렌더링 및 HTML 응답

  public interface ControllerV1 {
      void process(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException;
  }

(실무)
구조를 변경할 때는 구조만 변경해야함.
구조 변경 후 이상이 없을 경우에 (commit) 내용적인 세밀한 부분을 변경

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-
  controller/v1/*")

urlPatterns

urlPatterns = 'front-controller/v1/*' -> 'front/controller/v1' 을 포함한 모든 하위 요청들을 이 서블릿에서 받아들인다

controllerMap

  • key: 매핑 URL
  • value: 호출된 컨트롤러
//FrontController
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);
      }
  }

View 분리 -v2

JSP로 이동하고 그런걸 렌더링 한다고 표현
JSP를 렌더링하거나 forward로 이동하는것을 렌더링 한다
JSP니까 dispatcher.forward 하면 자동으로 렌더링

// /front-controller/v2/members/new-form
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")

public class FrontControllerServletV2 extends HttpServlet { ... }

Model 추가 -v3

서블릿 종속성 제거

컨트롤러 입장에서는 HttpServletRequest, HttpServletResponse 가 꼭 필요하지 않다
요청 파라미터 정보를 Map으로 대신 넘기면 서블릿을 몰라도 작동할 수 있다.

뷰 이름 중복 제거

=> 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화함으로써 이후에 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.

  Map<String, String> paramMap = new HashMap<>();
  
  request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));

paramMap을 만들었다
HttpServletRequest의 파라미터 이름을 다 가져온다
request.getParameter(paramName) 으로 모든 파라미터를 다 꺼내와서
Map <String,String> 에 paramName과 paramValue를 저장

뷰 리졸버 (뷰를 해결해주는 역할)

MyView view = viewResolver(viewName)

컨트롤러가 반환한 논리 뷰 이름 -> 물리 뷰 경로 변경
실제 물리 경로가 있는 MyView 객체를 반환

frontController의 역할은 더 늘어났다

  • paramMap 만듦

  • viewResolver 호출

  • render

=> But, 실제 구현한 컨트롤러들은 된게 편리하고 간략해졌다


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

v3도 잘 설계된 컨트롤러 이지만, 개발자 입장에서 ModelView 객체를 항상 생성하고 반환하는 것은 번거로울 수 있다.

v4 => 개발자가 편리한 버전

ModelView가 없고, model 객체는 파라미터로 전달하면 되기때문
결과로 뷰의 이름만 반환해준다

public String process(Map<String, String> paramMap, Map<String, Object>
{
    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.put("member",member);
: 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.

Map<String, String> paramMap = createParamMap(request); 
Map<String, Object> model = new HashMap<>(); //추가

모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다.
컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.


유연한 컨트롤러1 -v5

개발자마다 선호하는 코딩방식이 다를 수 있다
누구는 ControllerV3 다른 사람은 ControllerV4 방식을 추구할 수 있다.

 public interface ControllerV3 {
        ModelView process(Map<String, String> paramMap);
}
  public interface ControllerV4 {
        String process(Map<String, String> paramMap, Map<String, Object> model);
}

V3와 V4는 서로 완전히 다른 인터페이스이기 때문에 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 할 수 있다.

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

  • 핸들러: 컨트롤러의 이름을 핸들러로 변경. 어댑터가 있기 때문에 컨트롤러의 개념 뿐만 아니라 해당하는 종류의 어댑터만 존재하면 어떤 것이든 다 처리할 수 있기 때문이다.

public interface MyHandlerAdapter {

  //handler는 controller를 의미, 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드 
  boolean supports(Object handler);
  
  //이 어댑터를 통해 실제 컨틀롤러가 호출된다.
  ModelView handle(HttpServletRequest request, HttpServletResponse 			response, Object handler) throws ServletException, IOException;
  
}

 //ControllerV3을 처리할 수 있는 어댑터인지 ?
 
 @Override
 public boolean supports(Object handler) {
     return (handler instanceof ControllerV3);
 }
 
 //supports()를 통해 ControllerV3만 지원하기 때문에 타입 변환가능
 
 @Override
 public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {

	ControllerV3 controller = (ControllerV3) handler;
    Map<String, String> paramMap = createParamMap(request);
    
    
    //ControllerV3와 마찬가지로 ModelView를 반환
    ModelView mv = controller.process(paramMap);
	return mv;
    
}
    
//FrontControllerServletV5

//생성자
public FrontControllerServletV5() { 
	initHandlerMappingMap(); //핸들러 매핑 초기화 
    initHandlerAdapters(); //어댑터 초기화
}

//매핑정보

//ControllerV3, ControllerV4 인터페이스에서 아무 값이나 받을 수 있는 Object로 변경
private final Map<String, Object> handlerMappingMap = new HashMap<>();


//핸들러매핑 (Object handler = getHandler(request)

private Object getHandler(HttpServletRequest request) {
      String requestURI = request.getRequestURI();
      return handlerMappingMap.get(requestURI);
}


//핸들러를 처리할 수 있는 어댑터 조회 
(MyHandlerAdapter adapter = getHandlerAdapter(handler)

for (MyHandlerAdapter adapter : handlerAdapters) {
      if (adapter.supports(handler)) {
          return adapter;
      }
}

//어댑터 호출
(Model mv = adapter.handle(request, response, handler)

유연한 컨트롤러2 -v5


private void initHandlerMappingMap(){

	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());

}
//V3 구현할 때와 마찬가지로


public boolean supports(Object handler) {
      return (handler instanceof ControllerV4);
}

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;

어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환한다.
그러나 어댑터는 뷰의 이름이 아닌 ModelView를 만들어서 반환해야하므로
ModelView로 형식을 맞추어 반환. (어댑터가 필요한 이유)


정리

  • v1: 프론트 컨트롤러를 도입
    기존 구조를 최대한 유지하면서 프론트 컨트롤러를 도입

  • v2: View 분류
    단순 반복 되는 뷰 로직 분리

  • v3: Model 추가 서블릿 종속성 제거
    뷰 이름 중복 제거

  • v4: 단순하고 실용적인 컨트롤러 v3와 거의 비슷
    구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공

  • v5: 유연한 컨트롤러
    어댑터 도입
    어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계

애노테이션을 지원하는 어댑터를 추가하면 애노테이션을 사용해서 컨트롤러를 더 편리하게 발전시킬 수 있다. (다형성 + 어댑터로 인해 기존 구조를 유지하면서 프레임워크의 기능 확장 가능)


Reference
김영한 님 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
효율적이고 꾸준하게

0개의 댓글