nyximos.log

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

Programming/Spring

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

nyximos 2022. 2. 15. 22:32

프론트 컨트롤러 패턴 소개

프론트 컨트롤러 도입 전

프론트 컨트롤러 도입 후

 

FrontController 패턴 특징

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

 

스프링 웹 MVC와 프론트 컨트롤러

스프링 웹 MVC의 DispatchServlet이 FrontController 패턴으로 구현되어 있음 

 

프론트 컨트롤러 도입 v1

  • 기존 코드를 최대한 유지하면서 프론트 컨트롤러를 도입한다.

 

서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입

  • 각 컨트롤러는 이 인터페이스를 구현하면 된다.
  • 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
public interface ControllerV1 {
	
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;

}

 

인터페이스를 구현한 컨트롤러 생성

  • 아까 생성한 컨트롤러 인터페이스를 구현한다.
  • 내부 로직은 기존 서블릿과 거의 같다.
  • process()를 Override하여 로직을 작성한다.
public class MemberListControllerV1 implements ControllerV1 {

	private MemberRepository memberRepository = MemberRepository.getInstance();
 
 	@Override
 	public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
    // 내부로직
   
   }

 

프론트 컨트롤러 생성

  • controllerMap을 생성하여 controller를 넣어준다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/frontcontroller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    public FrontControllerServletV1() {
    	controllerMap.put("파일 경로", new 컨트롤러());
    }
    
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
    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(SC_NOT_FOUND) 상태 코드를 반환한다.
  • 컨트롤러를 찾고 controller.process(request, response);를 호출해서 해당 컨트롤러를 실행한다

 

 

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

 

 컨트롤러 인터페이스 생성

  • 컨트롤러는 뷰를 반환한다.
public interface ControllerV2 {

	MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;

}

 

인터페이스를 구현한 컨트롤러 만들기

  • process()를 Override하여 로직을 작성한다.
  • 복잡한 dispatcher.forward()를 직접 생성해서 호출하지 않아도 된다.
  • MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환한다.
return new MyView("viewPath");

 

프론트 컨트롤러 생성

  • controllerMap에서 requestURI가 같은 controller를 가져온다.
  • 컨트롤러는 호출결과로 MyView를 반환 받는다.
  • view.render()를 호출하면 forward 로직을 수행해서 JSP가 실행된다.
MyView view = controller.process(request, response);
view.render(request, response);

 

  • 프론트 컨트롤러의 도입으로 MyView 객체의 render()를 호출하는 부분을 모두 일관되게 처리할 수 있다.
  • 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 된다.

 

 

Model 추가 v3

서블릿 종속성 제거 

  • 컨트롤러 입장에서는 HttpServletRequest, HttpServletResponse가 항상 필요하지 않다.
  • 요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.
  • request 객체를 Model로 사용하지 않고 Model 객체를 만들어서 반환하자.

 

뷰 이름 중복 제거

  • 컨트롤러에서 지정하는 뷰 이름에 중복이 있다.
  • 컨트롤러 : 뷰 논리 이름 반환
  • 프론트 컨트롤러 : 실제 물리 위치 이름 처리

 

 

ModelView

  • 서블릿의 종속성을 제거하기위해 Model을 직접 만들고 View 이름까지 전달하자.
  • 뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있다.
  • 컨트롤러에서 뷰에 필요한 데이터를 key, value로 map에 넣어주자.
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();
}

 

컨트롤러 인터페이스 생성

  • 서블릿 기술을 사용하지 않는다. → 구현이 단순해지고 테스트 코드 작성시 테스트 하기 쉽다.
  • HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해준다.
  • 응답 결과로 뷰 이름과 뷰에 전달한 Model 데이터를 포함하는 ModelView 객체를 반환하면 된다.
public interface ControllerV3 {
	ModelView process(Map<String, String> paramMap);
}

 

인터페이스를 구현한 컨트롤러 생성

  • ModelView를 생성할 때 view의 논리적인 이름을 지정한다.
  • 물리적인 이름은 프론트 컨트롤러에서 처리
ModelView mv = new ModelView("save-result");
  • 파라미터 정보는 map에 담겨있다. map에서 필요한 요청 파라미터를 조회하면 된다.
paramMap.get("username");
  • 모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 담고 반환한다
mv.getModel().put("member", member);

 

프론트 컨트롤러 생성

  • HttpServletRequest에서 파라미터 정보를 꺼내서 Map으로 변환한다.
  • 그리고 해당 Map(paramMap)을 컨트롤러에 전달하면서 호출한다.
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);

 

뷰 리졸버

String viewName = mv.getViewName();
MyView view = viewResolver(viewName)
  • 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경
  • 실제 물리 경로가 있는 MyView 객체 반환
view.render(mv.getModel(), request, response)
  • 뷰 객체를 통해서 HTML 화면 렌더링
  • 뷰 객체의 render()는 모델 정보도 함께 받는다.
  • JSP는 request.setAttribute()로 데이터를 조회
    → 모델의 데이터를 꺼내서 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, HttprServletResponse 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

  • 개발자 입장에서 항상 ModelView 객체를 생성하고 반환하는 부분이 번거롭다.
  • 개발자들이 편리하게 개발할 수 있는 v4 버전을 개발해보자.
  • 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다.
  • ModelView를 반환하지않고 ViewName만 반환한다.

 

컨트롤러 인터페이스 생성

  • model 객체를 파라미터로 전달되기 때문에 뷰의 이름만 반환하면 된다.
public interface ControllerV4 {
    /**
    * @param paramMap
    * @param model
    * @return viewName
    */
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 

인터페이스를 구현한 컨트롤러 생성

  • 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.
model.put("member", member)

 

프론트 컨트롤러 생성

  • 모델객체 전달
    모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다.

    컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.
Map<String, Object> model = new HashMap<>();
  • 뷰의 논리 이름을 직접 반환
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);

 

 

유연한 컨트롤러1 v5

어댑터 패턴

  • 이때까지의 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.
  • 어댑터 패턴을 사용하면 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경할 수 있다.

핸들러 어댑터

  • 프론트 컨트롤러와 컨트롤러 중간에 어댑터를 두어 다양한 종류의 컨트롤러를 호출한다.

핸들러

  • 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.

 

 

 

어댑터용 인터페이스

  • 어댑터는 실제 컨트롤러를 호출하고 ModelView를 반환한다.
public interface MyHandlerAdapter {

    boolean supports(Object handler);
	
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}

 

  • supports는 컨트롤러를 처리할 수 있는지 판단하는 메서드이다.
boolean supports(Object handler)

 

어댑터 구현

  • handler가 ControllerV3을 처리할 수 있는 어댑터인지
public boolean supports(Object handler) {
    return (handler instanceof ControllerV3);
}
  • handler를 컨트롤러 V3으로 변환 → 형식에 맞게 호출
  • ControllerV3는 ModelView를 반환하므로 그대로 Model View를 반환한다.
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;

 

컨트롤러 → 핸들러

  • 생성자는 핸들러 매핑과 어댑터를 초기화(등록)한다.
public FrontControllerServletV5() {
    initHandlerMappingMap(); //핸들러 매핑 초기화
    initHandlerAdapters(); //어댑터 초기화
}
  • 여러 인터페이스에서 아무 값이나 받을 수 있는 Object로 변경됐다.
private final Map<String, Object> handlerMappingMap = new HashMap<>();
  • 핸들러 매핑 정보인 handlerMappingMap에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환한다.
private Object getHandler(HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    return handlerMappingMap.get(requestURI);
}
  • 핸들러를 처리할 수 있는 어댑터를 찾는다. adapter.supports(handler)
for (MyHandlerAdapter adapter : handlerAdapters) {
    if (adapter.supports(handler)) {
    return adapter;
    }
}
  • 실제 어댑터를 호출한다.
ModelView mv = adapter.handle(request, response, handler);

 

 

유연한 컨트롤러2 v5

ControllerV4를 사용할 수 있도록 기능을 추가한다.

 

프론트 컨트롤러

  • 핸들러 매핑( handlerMappingMap )에 ControllerV4 를 사용하는 컨트롤러를 추가하고,
  • 해당 컨트롤러를 처리할 수 있는 어댑터인 ControllerV4HandlerAdapter 도 추가하자.

 

어댑터 구현

  • handler가 ControllerV4을 처리할 수 있는 어댑터인지
public boolean supports(Object handler) {
    return (handler instanceof ControllerV4);
}
  • handler를 ControllerV4로 캐스팅
  • paramMap, model을 만들어서 해당 컨트롤러를 호출
  • viewName을 반환
ControllerV4 controller = (ControllerV4) handler;

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

String viewName = controller.process(paramMap, model);

어댑터 변환

  • 어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환한다.
  • 어댑터는 ModelView를 만들어서 반환해야한다.
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;

 

 

정리

v1 프론트 컨트롤러 도입

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

v2 View 분류

  • 단순 반복되는 뷰 로직 분리

v3 Model 추가

  • 서블릿 종속성 제거
  • 뷰 이름 중복 제거

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

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

v5 유연한 컨트롤러

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

 

애노테이션을 사용해 컨트롤러를 더 편리하게 사용할 수도 있다.

→ 애노테이션 지원하는 어댑터 추가

 

 

 

참조

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

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com