말하는 감자

[스프링부트 스터디] MVC 패턴 개선해 나가기 본문

Backend

[스프링부트 스터디] MVC 패턴 개선해 나가기

개똥벌레25 2021. 7. 27. 19:03
728x90

MVC 초반 패턴의 개선사항들

- 모든 컨트롤러 뷰로 포워드

- 포워드할 때 쓰는 viewPath의 경로가 중복 (/WEB-INF/views/ ~ .jsp)

- 사용하지 않는 servlet 객체

Front controller 도입

- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출

- 여기서부터는 뷰 jsp는 수정하지 않고 재사용한다.

개선 V1) 프론트 컨트롤러와 로직 컨트롤러 구분시키기

- 컨트롤러 인터페이스 생성으로 컨트롤러 호출에 일관성 부여.

- 로직 컨트롤러들은 기존 HttpServlet 상속받던 패턴을 없애고 인터페이스만 구현하는 걸로 변경한다.

public interface ControllerV1 {
    //Form, Save, List 컨트롤러는 이 인터페이스만 구현
    //내부는 전과 다를 것이 없다
    void process(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException;
}

프론트 컨트롤러만 HttpServlet 상속

- 생성 시 (키)url패턴과 (값)로직 컨트롤러 객체를 해쉬맵으로 가진다.

- 요청받은 uri를 통해 해쉬맵에서 컨트롤러 객체를 매핑하고, 컨트롤러 객체의 메서드를 실행한다.

- 수문장의 역할

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
//front-controller/v1 하위에 있는 모든 url은 이 서블릿 호출된다
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 {

        String requestURI = request.getRequestURI();
        
        ControllerV1 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        //오버라이드 된 메서드 실행, 일관성 부여
        controller.process(request, response);

    }
}

개선 V2) 로직 컨트롤러에서 중복 포워딩 없애기

- 로직 컨트롤러들이 각자 공통적으로 하던 뷰 포워딩을 프론트 컨트롤러가 (객체를 통해서) 하도록 만든다.

- 그러려면, 로직 컨트롤러들이 뷰 객체를 리턴하는 형식으로 바꿔주어야 한다.

- 따라서, 로직 컨트롤러들: 비즈니스 로직 실행 → 모델 세팅 → 뷰 객체를 프론트에게 전달

public interface ControllerV2 {
    //뷰 객체 반환해야 한다
    MyView process(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException;
}

- 로직 컨트롤러들의 내부는 다음으로 변경 (Form, Save, List 모두 똑같이 변경됨. Save만 예시로)

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

        //model에 데이터 보관
        request.setAttribute("member", member);

        //뷰 호출
        /*
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
        */
        return new MyView("/WEB-INF/views/save-result.jsp");        
    }

- 그러면 그 뷰 객체를 받은 프론트 컨트롤러는 뷰 객체를 받아, 렌더링을 실행한다.

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/members/*")
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;
        }
        
        //오버라이드 된 메서드 실행
        /*
        controller.process(request, response);
        */
        MyView view = controller.process(request, response);
        view.render(request, response);
    }
}

- 프론트 컨트롤러로부터 render를 하도록 명령받은 MyView 객체는 jsp 호출(뷰 포워딩)을 한다.

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

}

개선 V3) 로직 컨트롤러들에게서 HttpServlet 빼앗기, viewPath 중복 없애기

- 로직 컨트롤러들은 굳이 servlet의 논리를 가지고 있지 않아도 된다. (프런트가 호출하고, 프런트에게 반환하기에)

- 프런트 컨트롤러가 받은 request를 가지고 paramMap을 만들어 로직 컨트롤러에게 건내주면 된다.

- 따라서 servlet을 없애되, 비즈니스 로직 실행 후 뷰에게 전달할 모델 세팅은 해야 한다.

- 그래서 model 세팅에 request를 사용하지 않고 별도의 ModelView 객체를 사용한다.

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

- 로직 컨트롤러는 viewPath와 전달할 데이터가 들어있는 ModelView객체를 돌려준다. (Save 만 예시로)

- viewPath 또한 논리 이름만 반환해서 의존성을 없앤다. (jsp파일 경로 변경에 영향받지 않는다)

public interface ControllerV3 {
    //viewPath + 뷰에 전달할 데이터 -> ModelView
    //servlet 이제 필요없음
    ModelView process(Map<String, String> paramMap);
}
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {

        //비즈니스 로직
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        Member member = new Member(username, age);
        memberRepository.save(member);

        //model에 데이터 보관
        /*
        request.setAttribute("member", member);
        */
        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member", member);

        //뷰 호출
        /*
        return new MyView("/WEB-INF/views/save-result.jsp");
        */
        return mv;
    }

- 프론트 컨트롤러는 viewPath와 model을 가지고 (MyView 객체를 호출해서) 렌더링을 하는 작업을 한다.

@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;
        }
        /*
        MyView view = controller.process(request, response);                
        */
        //로직 컨트롤러가 Servlet을 이해하지 못하니
        //request paramMap으로 변환해서 컨트롤러 호출
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);
        
        //상대경로를 절대경로로 바꾸기
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);        
        
        //뷰 렌더링
        /*
        view.render(request, response);
        */       
        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");
    }

}

- 프론트 컨트롤러가 준 viewPath와 model을 가지고 다시 request로 만들고 뷰 포워딩

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의 아키텍쳐를 그대로 가져가면서 로직 컨트롤러들이 매번 뷰가 필요한 모델을 생성하는 과정을 생략하고

- 프론트 컨트롤러에게 viewPath 상대주소만 넘겨주도록 개선한다.

- 프론트 컨트롤러에서 비어있는 model 객체를 만들어 로직 컨트롤러에게 넘겨준다.

public interface ControllerV4 {
    //viewPath 상대주소만 리턴
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

- 로직 컨트롤러는 Model 객체 생성할 필요없이, 로직을 실행한 결과를 받은 model 객체에 넣어준다. (Save 만 예시로)

- 따라서 Model 객체를 담을 클래스도 생성할 필요가 없다.

- viewPath의 상대주소만 리턴한다.

    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 설정
        /*
        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member", member);
        return mv;
        */
        model.put("member", member);
        return "save-result";
    }

- 프론트 컨트롤러의 역할은 거의 비슷하다. 뷰를 렌더링하는 뒷 부분은 동일하다.

@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); //request 변환      
        /*
        ModelView mv = controller.process(paramMap);
        String viewName = mv.getViewName();
        */      
        Map<String, Object> model = new HashMap<>(); //HashMap으로 Model 생성
        String viewName = controller.process(paramMap, model); //컨트롤러 호출

        MyView view = viewResolver(viewName); //viewPath 상대주소 -> 절대주소
        /*
        view.render(mv.getModel(), request, response);
        */          
        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");
    }
}

개선 V5) 유연한 컨트롤러 만들기

- 다른 방식의 로직 컨트롤러들을 사용할 수 있는 어댑터 패턴 도입

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

- 핸들러:  = 컨트롤러. 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있다.

- supports: 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단한다.

- handle: 어댑터를 통해서 실제 컨트롤러(=핸들러)가 호출된다.

- 어댑터가 ModelView를 직접 생성해서라도 프론트 컨트롤러에게 반환한다.

public interface MyHandlerAdapter {
    boolean supports(Object handler);

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

- V3버전의 핸들러(ModelView를 반환했던 컨트롤러)를 지원하는 V3핸들러 어댑터

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, 
    Object handler) throws ServletException, IOException {
        ControllerV3 controller = (ControllerV3) handler; //캐스팅
        Map<String, String> paramMap = createParamMap(request);
        //v3 controller는 modelview를 반환한다
        ModelView mv = controller.process(paramMap);
        return mv;
    }
    private Map<String, String> createParamMap(HttpServletRequest request) {
        ...
    }
}

- V4버전의 핸들러(String 타입의 view name를 반환했던 컨트롤러)를 지원하는 V4핸들러 어댑터

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, 
    Object handler) throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();
        //v4 controller는 string을 반환한다
        String viewName= controller.process(paramMap, model);
		//어댑터가 직접 ModelView를 만들어서 세팅(어댑터의 목적)
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }
    private Map<String, String> createParamMap(HttpServletRequest request) {
        ...
    }
}

- 프론트 컨트롤러는 이제 매핑 정보 대신, 핸들러 매핑 정보와 핸들러 어댑터 목록을 가지고 어댑터를 호출한다.

- 프론트 컨트롤러는 로직 컨트롤러(핸들러)들을 직접적으로 호출하지 않는다.

- request로부터 핸들러 찾기 → 지원하는 어댑터 찾기 → 어댑터호출 (어댑터가 핸들러 호출 + ModelView로 통일성있게 반환) → ModelView로 뷰 리졸브

@WebServlet(name="frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
    /*
    private Map<String, ControllerV4> controllerMap = new HashMap<>();
     */
    // 컨트롤러의 범위 확장(Object)
    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());

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

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException {
    
        // request로부터 핸들러 가져오기
        Object handler = getHandler(request);

        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        // 가져온 핸들러로부터 지원하는 어댑터 찾기 (어댑터의 supports 이용)
        MyHandlerAdapter adapter = getHandlerAdapter(handler);
        // 어댑터가 컨트롤러를 부르고, ModelView 어댑테이션 하도록 하기
        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();
        Object handler = handlerMappingMap.get(requestURI);
        return handler;
    }
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
Comments