Programming-[Backend]/Spring

[스프링 웹MVC] 8. 프론트 컨트롤러(Front Controller) - 2. 컨트롤러 개선, MVC 패턴 요약

컴퓨터 탐험가 찰리 2021. 8. 1. 10:37
728x90
반응형

 

1. ControllerV4


ControllerV4에서는 controller에서 ModelView 객체를 FrontController에 반환해야만 했던 부분을 개선한다. 대신 로직을 처리한 객체를 model로 넘기고, viewName은 단순 String으로 반환하게 된다.

ControllerV4


다른 Controller들과 똑같이 process 메서드를 작성하되, paramMap외에 두 번째 파라미터로 model 객체를 전달받는다. 이 부분은 FrontController에서 각 Controller들의 process 메서드를 호출할 때, Controller에서 FrontController로 전달할 로직을 처리한 객체를 담을 수 있도록 미리 빈 사물함(?)을 정의해놓는 것이라고 이해하면 된다.

1
2
3
public interface ControllerV4 {
  String process(Map<StringString> paramMap, Map<String, Object> model);
}
cs



각 Controller


각 Controller 부분들을 작성한다. 대표적으로 MemberSaveControllerV4의 코드만 볼건데, 기존 V3 컨트롤러들의 코드를 복사해오고, 로직은 그대로 작성한다. 다만 FrontController에서 빈 사물함 형태의 model 객체를 받아왔기 때문에, 로직을 처리한 객체를 이 model에 담으면 된다.

리턴 타입은 String으로 변경하고, viewName을 리턴값으로 전달해서 FrontController에서 이를 이용해서 물리 주소인 viewPath를 만들 수 있도록 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MemberSaveControllerV4 implements ControllerV4 {
 
  private MemberRepository memberRepository = MemberRepository.getInstance();
 
  @Override
  public String process(Map<StringString> 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.put("member", member);
 
    return "save-result";
  }
}
cs

 

FrontControllerServletV4


마지막 부분인 Frontcontroller다. 아래 코드에서 볼드체 부분만 보면, Controller에서 정보를 받아오기 위해 model 객체를 HashMap 타입으로 만드는 것을 볼 수 있다. 이후 controller.process로 처리된 model 값과 viewName값을 이용하여 viewPath를 만들고, view쪽으로 정보를 넘겨주는 것은 다른 Controller와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@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 {
    System.out.println("FrontControllerServletV4.service");
 
    String uri = request.getRequestURI();
    ControllerV4 controller = controllerMap.get(uri);
 
    if (controller == null) {
      response.setStatus(HttpServletResponse.SC_NOT_FOUND);
      return;
    }
 
    //paramMap에서 파라미터 꺼내서 myView로 전달
    Map<StringString> paramMap = createParamMap(request);
    Map<String, Object> model = new HashMap<>();
 
    String viewName = controller.process(paramMap, model);
 
    MyView view = viewResolver(viewName);
 
    view.render(model, request, response); //dispatch 및 forward
  }
 
  private Map<StringString> createParamMap(HttpServletRequest request) {
    Map<StringString> 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");
  }
}
cs




ControllerV4의 의의는 ModelView 객체를 제거함으로써 개발자가 좀 더 쉽게 코드를 작성할 수 있게 만들었다는 것이다. 프레임워크의 사용자는 개발자가 될 것이므로, 개발자의 편의를 위해 프레임워크에서 미리 고민한 결과가 반영된 코드라고 볼 수 있다.


 

2. ControllerV5 개요



아직 상상이 잘 안가지만, 이때까지 작성한 여러가지 타입의 Controller를 요청마다 다른 버전들을 적용해야 하는 경우가 생길 수 있다고 한다. ControllerV5에서는 어댑터(Adapter)와 핸들러(Handler) 개념을 도입해서 상황에 따라 ControllerV3를 쓰거나, ControllerV4를 사용할 수 있도록 해본다.

다음은 앞으로 작성할 frontControllerServletV5의 코드 도입부이다. 원래는 Map의 value의 타입을 ControllerV4와 같이 고정된 핸들러 타입으로 선언하였기 때문에, front controller에서 해당 타입의 핸들러만 처리할 수 있었다. 그러나 아래 handlerMappingMap은 value타입을 Object로 지정함으로써, 여러가지 타입을 받을 수 있다. 그래서 V3, V4 등의 여러 타입의 핸들러를 받을 수 있게 되는 것이다.

FrontControllerServletV5 중 일부

 

1
2
3
4
5
6
7
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
 
//  private final Map<String, ControllerV4> controllerV4Map = new HashMap<>();
 
  private final Map<StringObject> handlerMappingMap = new HashMap<>();
}
cs

 

 

프로세스 흐름


어댑터와 핸들러 개념을 도입함에 따라 프로세스가 추가되었다. 핸들러는 기존에 다뤄왔던 컨트롤러보다 큰 개념이라고 하는데, 일단은 컨트롤러로 생각해도 될 것 같다. 중요한 것은 어댑터 부분인데, V3, V4 등 원하는 핸들러를 사용하기 위해서 각 핸들러의 타입을 변환하여 리턴해주는 역할을 한다.




 

3. FrontControllerServletV5 - V3


이번에는 복잡한 흐름을 순서대로 이해하기 위해서 Front Controller부분의 코드를 먼저 살펴보자.

프로세스 흐름도의 1, 2번


handlerMappingMap과 handlerAdapters를 정의한다. 이 부분은 각각 핸들러를 조회하고 핸들러 어댑터를 조회하기 위한 목록이다. 아래 initHandlerMappingMap() 메서드와 initHandlerAdapters() 메서드를 보면 각 목록에 V3 핸들러를 넣어주고, V3 어댑터를 넣어주는 것을 볼 수 있다.

핸들러와 핸들러 어댑터의 목록을 만들어주고(init), 실제 service 코드에서 이를 활용한다. 처음 getHandler() 메서드에서는 요청의 uri 정보를 handlerMappingMap 매핑 정보에 넣어주어서, 알맞은 핸들러를 불러온다. 다음으로 getHandlerAdapter() 메서드에서는 handlerAdapters 어댑터 목록에서 해당하는 어댑터가 있는지 확인(supports() 메서드)하고, 어댑터를 반환한다. 그러면 이 어댑터의 handle() 메서드를 이용해서 ModelView 객체를 받아오는 구조이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@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(); // 논리 이름 new-form
    MyView view = viewResolver(viewName);
 
    view.render(mv.getModel(), request, response);
  }
 
  private Object getHandler(HttpServletRequest request) {
    String uri = request.getRequestURI();
    return handlerMappingMap.get(uri);
  }
 
  private MyHandlerAdapter getHandlerAdapter(Object handler) {
    for (MyHandlerAdapter handlerAdapter : handlerAdapters) {
      if(handlerAdapter.supports(handler)) {
        return handlerAdapter;
      }
    }
    throw new IllegalArgumentException("핸들러 어댑터를 찾을 수 없습니다. handler = " + handler);
  }
 
  private MyView viewResolver(String viewName) {
    return new MyView("/WEB-INF/views/" + viewName + ".jsp");
  }
 
}
cs

 

어댑터


FrontController에서 이용하는 어댑터와 그 내부의 메서드 코드는 다음과 같다.

MyHandlerAdapter

추후 확장이 가능하도록 인터페이스 형태로 정의한다. supports는 핸들러를 받아와서 해당 어댑터가 핸들러를 받을 수 있는지 여부를 boolean 타입으로 반환한다. 예를 들어 아래 작성할 ControllerV3HandlerAdapter는 ControllerV3 타입만 지원하는데, supports 메서드에 handler 인자가 ControllerV3 타입을 상속받는 객체이면 true를 반환하는 것이다.

handle 메서드는 supports를 통과한 핸들러를 파라미터로 받아와서, paramMap을 만든 후 핸들러의 controller.process 메서드를 실행하여 ModelView 객체를 반환할 수 있도록 해준다.

 

1
2
3
4
5
6
7
public interface MyHandlerAdapter {
 
  boolean supports(Object handler);
 
  ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
 
cs

 

실제 supports와 handle 메서드의 내용은 아래 구현체에서 확인하자

 



ControllerV3HandlerAdapter

위에서 설명한 내용대로, supports() 메서드는 instanceof을 활용하여 V3에 해당하는 handler인지를 확인한다. handle() 메서드는 supports() 메서드를 통과한 핸들러에 한해, ControllerV3 타입으로 캐스팅하고, 해당 핸들러의 process() 메서드를 실행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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<StringString> paramMap = createParamMap(request);
    ModelView mv = controller.process(paramMap);
    return mv;
  }
 
  private Map<StringString> createParamMap(HttpServletRequest request) {
    Map<StringString> paramMap = new HashMap<>();
    request.getParameterNames().asIterator()
            .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
    return paramMap;
  }
}
cs



다소 복잡할 수 있으나, Map과 List로 원하는 타입의 컨트롤러의 매핑 정보와, 어댑터 정보를 저장하고 어댑터에서 컨트롤러의 타입을 검사하고 캐스팅해주는 개념이라고 이해하면 될 것 같다. 다음 섹션에서는 V4 타입의 핸들러에 어댑터를 적용할 것인데, 초기에 init으로 선언한 Map과 List에 V4 타입에 대한 핸들러와 어댑터만 정의해주면 된다.


 

4. FrontControllerServletV5 - V4


ControllerV4를 추가로 사용하기 위해서, FrontController에 V4에 대한 정보만 추가하고 어댑터만 만들어주면 된다.

 

 

FrontControllerServletV5 - v4 추가


앞서 작성한 FrontController 코드에 V4의 핸들러와 어댑터를 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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());
  }
cs

 

 

ControllerV4HandlerAdapter



이제 V4 어댑터를 추가한다. 기본적으로는 V3 어댑터와 같은 구조를 갖는다. 그러나, V4는 V3와 달리 FrontController에서 빈 사물함과 같은 역할을 하는 model 객체를 전달받았었다. 그런데 FrontControllerV5는 V3와 공통으로 코드를 사용하므로 model 객체를 전달해주는 코드를 추가로 작성할 수 없다. 따라서 어댑터에서 12번줄과 같이 새로운 model 객체를 만들어 주었다.

그리고 controller.process() 메서드의 처리 결과가 String으로 출력되는데, MyHandlerAdapter에서 handle 메서드는 ModelView를 반환타입으로 지정해놓았고, 이것을 V3 어댑터와 같이 사용하는 형태이므로 새로운 ModelView 객체를 만들어서 반환해준다. ModelView 객체에는 .setModel() 메서드로 controller.process를 통해 로직이 실행된 후의 정보가 저장된 model 객체를 담아주도록 한다. 이렇게, 메서드의 지정된 반환 타입을 다른 타입으로 변환해주는 것이 Adapter의 역할이라고 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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<StringString> paramMap = createParamMap(request);
 
    HashMap<String, Object> model = new HashMap<>();
 
    String viewName = controller.process(paramMap, model);
    ModelView mv = new ModelView(viewName);
    mv.setModel(model);
    return mv;
  }
 
  private Map<StringString> createParamMap(HttpServletRequest request) {
    Map<StringString> paramMap = new HashMap<>();
    request.getParameterNames().asIterator()
            .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
    return paramMap;
  }
}
cs

 



MVC 패턴에 대해 전반적으로 알아보았다. 요약하자면 다음과 같다.

 

  • 클라이언트로부터 요청을 받아서 FrontController가 공통화된 부분을 처리한다.
  • 요청의 uri 정보를 바탕으로 해당하는 핸들러(컨트롤러), 핸들러 어댑터를 찾아서 비즈니스 로직을 실행한다.
  • 논리이름(viewName)과 model 객체(요청 파라미터 정보)를 View로 전달하여 알맞는 View(.jsp) 응답을 반환한다.


그리고, MVC 패턴의 구현에 필요한 요소들을 인터페이스화하고 각 메서드를 Override 해가면서 코드를 확장해나가는 법에 대해서 배울 수 있었다.

추가로 간단한 if 조건문 등은 그대로 작성해도 좋지만, 세부적인 로직이 들어간 코드는 리팩토링 기능[Ctrl + Alt + m]을 이용하고 한눈에 알아보기 쉬운 함수명으로 정리해주는 것이 좋다는 것을 배웠다.

 


참조

1. 인프런_스프링 MVC 1편 - 백엔드 웹개발 핵심 기술_김영한 님 강의

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1

728x90
반응형