Spring

Spring 스프링 MVC - 2, MVC 프레임워크

코드깎는머슴 2024. 4. 11. 10:47
728x90
반응형

MVC 프레임워크

1. MVC 패턴 - 한계

MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.

특히 뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다.

단순하게 모델에서 필요한 데이터를 꺼내고, 화면을 만들면 된다.

 

그런데 컨트롤러는 딱 봐도 중복이 많고, 필요하지 않는 코드들도 많이 보인다.

실제 MVC 패턴의 코드를 전부 가져오면 너무 길어지므로 

설명만 하고, 설명에 해당하는 일부 코드만 가져오겠다.

 

2. MVC 컨트롤러의 단점

2 - 1. 포워드 중복

View로 이동하는 코드가 항상 중복 호출되어야 한다.

물론 이 부분을 메서드로 공통화해도 되지만, 해당 메서드도 항상 직접 호출해야 한다.

RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

 

2 - 2. ViewPath에 중복

String viewPath = "/WEB-INF/views/new-form.jsp";

 

prefix: /WEB-INF/views/
suffix: .jsp

위 부분들이 계속해서 중복된다

.

그리고 만약 jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 한다.

 

2 - 3. 사용하지 않는 코드

다음 코드를 사용할 때도 있고, 사용하지 않을 때도 있다. 

HttpServletRequest request, HttpServletResponse response

 

그리고 이런 HttpServletRequest , HttpServletResponse 를 사용하는 코드는 테스트 케이스를 작성하기도 어렵다.

 

2 - 4. 공통 처리가 어렵다.

기능이 복잡해질 수 록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가할 것이다.

단순히 공통 기능을 메서드로 뽑으면 될 것 같지만,

결과적으로 해당 메서드를 항상 호출해야 하고,

실수로 호출하지 않으면 문제가 될 것이 다.

그리고 호출하는 것 자체도 중복이다.

 

정리하면 공통 처리가 어렵다는 문제가 있다.

이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다.

소위 수문장 역할을 하는 기능이 필요하다.

프론트 컨트롤러(Front Controller) 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.

스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다.

 

 

3. FrontController 패턴

1. 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음

2. 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출

3. 공통 처리 가능

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

 

스프링 웹 MVC와 프론트 컨트롤러 스프링 웹 MVC의 핵심도 바로 FrontController

스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있다.

 

3 - 1. FrontController 예시

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
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());
    }
}

FrontController 예제에서 일부를 가져온 것이다.

 

3 - 1 - 1. urlPatterns

urlPatterns = "/front-controller/v1/*"

 

/front-controller/v1 를 포함한 하위 모든 요청 은 이 서블릿에서 받아들인다.

    예) /front-controller/v1 , /front-controller/v1/a , /front-controller/v1/a/b

 

3 - 1 - 2. controllerMap

    key: 매핑 URL

    value: 호출될 컨트롤러

 

 

4. FrontController 개선

4 - 1. 뷰 이름 중복 제거

컨트롤러에서 지정하는 뷰 이름에 중복이 있는 경우가 있다.

 

컨트롤러는 뷰의 논리 이름을 반환하고,

실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화 하자.

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

 

예시

/WEB-INF/views/new-form.jsp >> new-form

/WEB-INF/views/save-result.jsp >> save-result

중복인 /WEB-INF/views/를 적지 않아도 된다.

 

4 - 2. 뷰 리졸버

MyView view = viewResolver(viewName)

컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다.

그리고 실제 물리 경로가 있는 MyView 객체를 반환 한다.

 

논리 뷰 이름: members

물리 뷰 경로: /WEB-INF/views/members.jsp

 

4 - 3. 어댑터 패턴

위 방식의 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.

 

하지만 실제로는 다양한 컨트롤러가 존재하며,

개발자마다, 상황마다 다른 컨트롤러를 기반으로 유연하게 개발하고 싶을 때가 있을 것이다.

 

이럴 때 사용하는 것이 어댑터이다.

 

4 - 3 - 1. 어댑터용 인터페이스 예시

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

어댑터는 이렇게 구현해야 한다는 의미의 어댑터용 인터페이스이다.

 

boolean supports(Object handler)

handler는 컨트롤러를 말한다.

어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드다.

 

ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)

어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다.

실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.

 

이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만,

이제는 이 어댑터를 통해서 실제 컨트롤러가 호출 된다.

 

4 - 3 - 2. 실제 어댑터 예시

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) {
        ControllerV3 controller = (ControllerV3) handler;
        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;
    }
}

 

하나씩 분석해보자

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 형식에 맞도록 호출한다.

위의 supports() 를 통해 ControllerV3 만 지원하기 때문에 타입 변환은 걱정없이 실행해도 된다.

ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 된다.

 

4 - 3 - 3. 핸들러

컨트롤러(Controller) => 핸들러(Handler)

이전에는 컨트롤러를 직접 매핑해서 사용했다.

그런데 이제는 어댑터를 사용하기 때문에,

컨트롤러 뿐만 아니라 어댑터 가 지원하기만 하면, 어떤 것이라도 URL에 매핑해서 사용할 수 있다.

그래서 이름을 컨트롤러에서 더 넒은 범위의 핸들러로 변경했다.

 

 

5. 프론트 컨트롤러 최종 형태

앞서 언급했던 내용들을 전부 고려했을 때,

ControllerV3와 다른 컨트롤러 ControllerV4까지 있다고 가정해보면,

프론트 컨트롤러는 아래와 같이 구성되어 있다.

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

         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 {
         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);
         MyView view = viewResolver(mv.getViewName());
         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");
     }
}

 

728x90
반응형