[스프링 MVC - 1편] MVC 프레임워크 만들기

지현·2021년 12월 8일
0

스프링

목록 보기
18/32

프론트 컨트롤러 패턴 소개

  • 프론트 컨트롤러 서블릿에 공통 로직을 모으고 각각 필요한 로직은 각각 처리
  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 공통 처리 가능
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨 (프론트 컨트롤러가 서블릿 요청을 다 받게 만듦) > HttpServlet을 상속받거나 @WebServlet을 사용하지 않아도 됨

프론트 컨트롤러 도입 - v1

@WebServlet(name="frontControllerServletV1",urlPatterns = "/front-controller/v1/*" )
// /front-controller/v1를 포함한 어떤 하위 url이 들어와도 이 서블릿이 무조건 호출
public class FrontControllerServletV1 extends HttpServlet {
    private Map<String, ControllerV1> controllerMap = new HashMap<>();
    // 키는 URL > 어떤 URL이 호출이 되면 컨트롤러 V1을 꺼내서 호출해라

    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();
        // url의 /front-controller/v1/~ 이부분을 그대로 받음

        ControllerV1 controller = controllerMap.get(requestURI);
        //map에서 해당 requestURI에 해당하는 컨트롤러 꺼내기(다형성에 의해서 인터페이스로 꺼낼 수 있음)
        //값에 있는 컨트롤러들이 다 ControllerV1 인터페이스를 구현한 객체이기 때문에
        
        if(controller==null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            //잘못된 URL이여서 해당하는 컨트롤러가 없는경우 404 error
            return;
        }

        controller.process(request,response);
        //해당 컨트롤러 실행
    }
}

View 분리 - v2

  • 컨트롤러가 더이상 JSP forward에 신경쓰지 않아도 됨 > 컨트롤러는 MyView 객체를 생성만 해서 반환
  • MyView 객체의 render() 를 호출하는 부분은 프론트 컨트롤러에서 일관되게 처리 (뷰를 만드는 행위를 렌더링이라고 한다)

FrontControllerServletV2 .java

@WebServlet(name="frontControllerServletV2",urlPatterns = "/front-controller/v2/*" )
public class FrontControllerServletV2 extends HttpServlet {
    
	...

        //프론트 컨트롤러에서 view.render()를 호출하는 공통 로직 처리
        MyView view = controller.process(request,response);
        view.render(request,response);
    }
}

MyView.java

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

Model 추가 - v3

  • 서블릿 종속성 제거 : 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경 > 구현 코드도 매우 단순해지고, 테스트 코드 작성이 쉬움
    • 요청 파라미터 정보는 프론트 컨트롤러에서 자바의 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

- viewResolver : 뷰의 논리 이름을 실제 물리 이름으로 바꿔줌

ModelView.java

  • 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체 생성
  • 이전까지는 서블릿에 종속적인 HttpServletRequest를 사용하고 Model도 request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달했음

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

V3을 개발자 입장에서 좀 더 쉽게 만들기
V3과 차이점

  • FrontController가 Controller를 호출할 때 모델도 같이 넘겨줌 > ModelView가 없음(FrontController에서 모델 객체 생성)
  • 컨트롤러가 ModelView 를 반환하지 않고, ViewName 만 반환

유연한 컨트롤러1 - v5

한가지 인터페이스를 고정해서 사용하는 것이 아니라 여러가지 컨트롤러 인터페이스를 사용해서 개발하고 싶을 때 > 어댑터 패턴 사용

  • 핸들러 어댑터 : 다양한 종류의 컨트롤러를 호출 가능
  • 어댑터를 통해서 핸들러(컨트롤러)를 호출 *핸들러가 더 넓은 범위

ControllerV3HandlerAdapter.java

// 어댑터의 역할
// 핸들러를 호출해주고 그 결과가 오면 그 반환타입을 맞춰서 반환해줘야함

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    @Override
     //핸들러가 넘어왔을 때 이 핸들러를 처리할 수 있는지 ? 판단
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
        //ControllerV3를 구현한 무언가가 넘어오게 되면 참을 반환
        //나머지는 false 반환
    }

    @Override
    //실제 컨트롤러를 전달받고 이 어댑터를 통해서만 컨트롤러를 호출하고, 그 결과로 ModelView를 반환
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV3 controller = (ControllerV3) handler;
        //유연하기위해 Object로 되어있던 handler를 타입변환(캐스팅)
        //supports메서드로 걸러진 애를 찾아서 그다음에 handle을 호출하기 때문에 캐스팅해도 됨

        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.java

@WebServlet(name="frontControllerServletV5",urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
    private final Map<String,Object> handlerMappingMap=new HashMap<>();
    //기존에 어댑터를 사용하지 않던 코드에는 Object 말고 구체적인 인터페이스 타입이 들어갔었음
    //버전 상관없이 아무 컨트롤러나 다 지원하기위해서 Object 사용
    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 Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            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");
    }

}


출처
[인프런] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

0개의 댓글