스프링MVC4_프레임워크 만들기

하도야지·2021년 7월 23일
0

Spring MVC

목록 보기
4/7

(1) 프론트 컨트롤러 패턴 소개

1. 프론트 컨트롤러 패턴 소개

  • 도입 전
  • 도입 후

2. FrontController 패턴 특징

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

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

  • 스프링 웹 MVC의 핵심도 바로 FrontController
  • 스프링 웹 MVC의 DisPatcherServlet이 FrontController 패턴으로 구현되어 있음

(2) 프론트 컨트롤러 도입 -v1

기존 코드를 유지하면서, 프론트 컨트롤러를 도입해보자

1. v1 구조

2. v1 코드

  • ControllerV1.interface

    -> 서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
    -> 각 컨트롤러들은 이 인터페이스를 구현
    -> 프론트 컨트롤러는 이 인터페이스 호출하여 구현과 관계없이 로직의 일관성을 가져감

  • MemberFormControllerV1.java - 회원 등록 컨트롤러

  • MemberSaveControllerV1.java - 회원 저장 컨트롤러

  • MemberListControllerV1.java - 회원 목록 컨트롤러

  • FrontControllerServletV1.java - 프론트 컨트롤러
  • 프론트 컨트롤러 분석
  • urlPatterns
    -> urlPatterns = "/front-controller/v1/*" : /front-controller/v1 를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
  • controllerMap
    -> key : 매핑 URL
    -> value : 호출될 컨트롤러
  • service()
    -> 먼저 requestURL를 조회하여 실제 호출할 컨트롤러를 controllerMap에서 찾는다. 만약 없다면 404상태코드를 반환한다.
    --> 컨트롤러를 찾고 controller.process(request, response)를 호출해서 해당 컨트롤러를 실행한다.
  • JSP
    : JSP는 이전 MVC에서 사용했던 것을 그대로 사용한다.

(3) View 분리 - v2

1. 문제점 파악과 v2 구조

  • 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
  • v2 구조

2. v2 코드

  • MyView.java-> view 객체

  • ControllerV2.interface-> view객체를 반환

  • MemberFormControllerV2.java - 회원 등록 폼

  • MemberSaveControllerV2.java - 회원 저장

  • MemberListControllerV2.java - 회원 목록

  • 이제 각 컨트롤러는 복잡한 dispatcher.forward() 를 직접 생성해서 호출하지 않아도 된다.
  • 단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 된다.
  • 프론트 컨트롤러 V2
  • ControllerV2의 반환 타입이 MyView 이므로 프론트 컨트롤러는 컨트롤러의 호출 결과로 MyView 를 반환
    받는다.
  • 그리고 view.render() 를 호출하면 forward 로직을 수행해서 JSP가 실행된다.
  • 프론트 컨트롤러의 도입으로 MyView 객체의 render() 를 호출하는 부분을 모두 일관되게 처리할 수 있다. 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 된다.

(4) Model 추가 -v3

1. 서블렛 종속성 제거

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

-> 우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경해보자.

2. 뷰 이름 중복 제거

  • 컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인
  • 컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화

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

3. v3 구조

4. v3코드

  • ModelView.java

  • 지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다.
  • Model도 request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달했다.
    -> 서블릿 종속성 제거를 위해 직접 Model 생성 및 view전달 객체를 전달한다
  • 뷰의 이름과 렌더링시 필요한 model객체를 가지고 있다.
  • ControllerV3.interface
  • 서블렛을 전형 사용하지 않아 구현이 단순하며, 테스트 코드 작성이 쉽다.
  • HttpServletRequest -> paramMap으로 변환
  • 응답결과로 ModelView 객체 반환
  • MemberFormControllerV3.java - 회원 등록 폼
  • ModelView 생성 시 new-form 논리명 지정
    -> 실제 물리명은 프론트 컨트롤러에서 처리(뷰리졸버)
  • MemberSaveControllerV3.java - 회원 저장
  • paramMap.get("username");
    -> 필요한 파라미터 정보를 map에서 조회
  • mv.getModel().put("member", member);
    -> 모델(map)에 member객체 담아 반환
  • MemberListControllerV3.java - 회원 목록

  • FrontControllerServletV3.java - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@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 {

        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");
    }
}
  • 뷰 리졸버
    -> 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경
  • V3 구조 순서대로 정리
  1. HTTP 요청
    : http://localhost:8080/front-controller/v3/members/new-form
  2. 컨트롤러 조회 (클라이언트 -> FrontControllerServletV3.java)
    : String requestURI = request.getRequestURI();
    ControllerV3 controller = controllerMap.get(requestURI);
  3. 컨트롤러 호출 (FrontControllerServletV3.java )
    : ModelView mv = controller.process(paramMap);
  4. ModelView 반환 (MemberFormControllerV3.java)
    : return new ModelView("new-form");
  5. viewResolver 호출 (FrontControllerServletV3.java)
    : String viewName = mv.getViewName(); // 논리이름
    MyView view = viewResolver(viewName);
  6. MyView 반환 (FrontControllerServletV3.java)
    : return new MyView("/WEB-INF/views/" + viewName + ".jsp");
  7. render(model) 호출 (FrontControllerServletV3.java)
    : view.render(mv.getModel(), request,response);
  • MyView.java
  • view.render(mv.getModel(), request, response)
  • 뷰 객체를 통해 HTML 화면을 렌더링 함
    -> 뷰 객체의 render()에서 모델 정보도 함께 받음
    --> JSP는 request.getAttribute()로 데이터를 조회하므로 request.setAttribute()로 담아둠
  • JSP로 포워드 해서 JSP 렌더링함

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

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

  • 앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다.
  • 그런데 실제 컨트톨러 인터페이스를 구현하는 개발자 입장에서 보면, 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다.
  • 좋은 프레임워크는 아키텍처도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할
    수 있어야 한다. 소위 실용성이 있어야 한다.

2. V4 구조

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

3. V5 코드

  • ControllerV4.Interface
  • String 타입 : 뷰의 이름을 반환
  • model 객체는 파라미터로 전달됨
  • MemberFormControllerV4.java - 회원 등록 폼

  • MemberSaveControllerV4.java - 회원 저장

  • MemberListControllerV4.java - 회원 목록

  • 뷰의 논리 이름만 반환하면 된다.
  • 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.
    -> model.put("member", member)
  • FrontControllerServletV4.java
package hello.servlet.web.frontcontroller.v4;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@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 {

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

    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");
    }
}
  • 모델 객체 전달
    -> 모델 객체를 프론트 컨트롤에서 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 담기게 된다.
    --> Map<String, Object> model = new HashMap<>(); //추가
  • 뷰의 논리 이름을 직접 반환
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
  1. 정리
  • 이번 버전의 컨트롤러는 매우 단순하고 실용적이다.
  • 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데, 컨트롤러를 구현하는 개발자 입장에서 보면 이제 군더더기 없는 코드를 작성할 수 있다

(6) 유연한 컨트롤러1 - v5

누군가는 ControllerV3 방식으로 개발하고 싶고, 누군가는 ControllerV4 방식으로 개발할 수 있을까?

1. 어댑터 패턴

  • 지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.
  • ControllerV3, ControllerV4는 완전 다른 인터페이스 이므로 호환이 불가능 하다.
  • 어댑터 패턴을 사용하여 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있다.

2. V5 구조

  • 핸들러 어댑터
    -> 어댑터 역할을 하며, 이 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
  • 핸들러
    -> 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.
    --> 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.

3. V5 코드

  • MyHandlerAdapter.Interface
  • boolean supports(Object handler)
    -> handler는 컨트롤러를 의미한다.
    -> 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드다.
  • ModelView handle(HttpServletRequest request, HttpServletResponse response, Object
    handler)
    -> 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다.
    -> 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
    -> 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.
  • ControllerV3HandlerAdapter.java - V3어댑터
  • 소스 확인
public boolean supports(Object handler) {
 return (handler instanceof ControllerV3);
}

-> ControllerV3을 처리할 수 있는 어댑터인지 확인

ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;

-> handler를 컨트롤러V3으로 변환 후 V3형식에 맞게 호출
--> ControllerV3은 ModelVIew를 반환하므로 동일하게 ModelView 반환

  • FrontControllerServletV5.java - 프론트컨트롤러V5
package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

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

        String viewName = mv.getViewName(); // 논리이름
        MyView view = viewResolver(viewName);

        view.render(mv.getModel(), request, response);
    }


    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다.:" + handler);
    }


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

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
  • 컨트롤러(Controller) -> 핸들러(Handler)
    -> 이전에는 컨트롤러를 직접 매핑해서 사용했다.
    --> 이제는 어댑터가 지원하기만 하면, 어떤 것이라도 URL에 매핑해서 사용할 수 있다.
    ---> 이름을 더 넒은 범위의 핸들러로 변경했다.
  1. HTTP 요청
    : http://localhost:8080/front-controller/v5/v3/members/new-form
  2. 핸들러 조회 (클라이언트)
    : Object handler = getHandler(request)
private Object getHandler(HttpServletRequest request) {
 String requestURI = request.getRequestURI();
 return handlerMappingMap.get(requestURI);
  1. 핸들러를 처리할 수 있는 핸들러 어댑터 조회
    : MyHandlerAdapter adapter = getHandlerAdapter(handler)
      for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다.:" + handler);
  1. handle(handler)
    : ModelView mv = adapter.handle(request, response, handler);
  2. handler 호출
  3. ModelView 반환
  4. viewResolver 호출
    : MyView view = viewResolver(viewName);
  5. Myview 반환
  6. render(model) 호출
    : view.render(mv.getModel(), request, response);

(7) 유연한 컨트롤러2 - v5

FrontControllerServletV5 에 ControllerV4 기능도 추가해보자

1. V5 코드 추가

  • FrontControllerServletV5.java 추가
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 추가
}

-> 핸들러매핑(handlerMappingMap)에 ControllerV4 사용 컨트롤러 추가
-> 핸들러 어댑터에 ControllerV4HandlerAdapter 추가

  • ControllerV4HandlerAdapter.java
  • 실행로직
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를 만들어서 형식을 맞추어 반환한다.


(8) 정리

1. 프레임워크 개발 과정

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

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

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

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

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

  • 애노테이션까지 추가하여 컨트롤러를 편리하게 사용한다면...?

2. 스프링 MVC

  • 지금까지 작성한 코드는 스프링 MVC 프레임워크의 핵심 코드의 축약 버전이고, 구조도 거의 같다.
  • 스프링 MVC에는 지금까지 우리가 학습한 내용과 거의 같은 구조를 가지고 있다.
profile
프로그래머를 꿈꾸는 코더

0개의 댓글