이전엔 단순하고 실용적인 MVC 프레임워크를 만들었다.
이번 섹션에선 어댑터라는 개념을 추가해 스프링 MVC를 더 자세하게 이해해 본다.
1. 유연한 컨트롤러 (v5) - 1
어댑터 패턴
만약 어떤 개발자는 ControllerV3 방식으로 개발하고 싶고, 어떤 개발자는 ControllerV4 방식으로 개발하고 싶다면 어떻게 해야 할까?
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
지금까지 개발한 프론트 컨트롤러는 한 가지 방식의 Controller Interface만 사용할 수 있다. ControllerV3과 ControllerV4는 완전히 다른 Interface이기 때문에 동시에 사용하려면 어댑터를 사용해야 한다. 어댑터 패턴을 적용해 프론트 컨트롤러가 다양한 방식의 Controller를 처리할 수 있도록 변경해 보자.
- 안 맞는 2개를 맞춰야 할 때, 중간에 뭘 하나 껴서 맞추는 것을 어댑터 패턴이라고 부른다.
public class FrontControllerServletV4 extends HttpServlet {
// URI에 ControllerV4만 매핑할 수 있음
private Map<String, ControllerV4> controllerMap = new HashMap<>();
...
}
v5 구조
v5 구조는 아래와 같다. 핸들러는 Controller와 비슷한 개념이라고 보면 된다.
핸들러 어댑터
중간에 어댑터 역할을 하는 어댑터가 추가됐는데, 이름이 핸들러 어댑터다. 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 Controller를 호출할 수 있다.
핸들러
Controller의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어댑터가 있기 때문에 꼭 Controller의 개념뿐만 아니라 어떤 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.
- 핸드럴 매핑 정보를 통해 핸들러를 조회한다. (ex. HandlerV4)
- 핸들러 어댑터 목록에서 위에서 조회한 핸들러(ex. HandlerV4)를 처리할 수 있는 핸들러 어댑터를 조회한다. (ex. HandlerV4Adapter)
- 프론트 컨트롤러에서 위에서 조회한 핸들러 어댑터(ex. HandlerV4Adapter)를 통해 핸들러(ex. HandlerV4)를 호출한다.
- 이후 해당 핸들러(ex. HandlerV4)에서 반환한 ModelView를 핸들러 어댑터에서 프론트 컨트롤러로 전달한다.
- viewResolver에서 View의 논리 이름을 실제 물리 이름으로 바꾸고, 해당하는 MyView를 반환한다.
- MyView 객체의 render()를 호출하면 해당 객체의 viewPath와 RequestDispatcher를 통해 forward() 로직을 수행해 JSP가 실행된다.
어댑터 구현하기
a. MyHandlerAdapter
어댑터를 이렇게 구현해야 한다는 어댑터용 Interface다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException;
}
어댑터 핸들러엔 2가지 기능이 있다.
- boolean supports(Object handler)
- handler는 Controller를 말한다.
- 어댑터가 해당 Controller를 처리할 수 있는지 판단하는 메서드다.
- 핸들러 어댑터 목록에서 핸들러 어댑터를 조회할 때, 이 메서드의 반환값이 true인 것을 조회한다.
- ModelView handler(HttpServletRequest request, HttpServletResponse response, Object handler)
- 어댑터는 실제 Controller를 호출하고, 그 결과로 ModelView를 반환해야 한다.
- 실제 Controller가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
- 이전에는 프론트 컨트롤러가 호출했지만, 이제는 이 어댑터를 통해 실제 Controller가 호출된다.
- 유연성을 위해 handler는 Object로 넘긴다.
b. ControllerV3HandlerAdapter
실제 어댑터를 구현해 보자. 먼저 ControllerV3를 지원하는 어댑터를 구현해 본다.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
// ControllerV3를 처리할 수 있는 어댑터를 뜻함
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
// 핸들러를 ControllerV3로 변환한 다음에 V3 형식에 맞도록 호출
// supports()를 통해 ControllerV3만 지원하기 때문에 타입 변환은 걱정하지 않아도 됨
// ControllerV3는 ModelView를 반환하므로 그대로 ModelView 반환
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
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;
}
}
프론트 컨트롤러 (v5)
프론트 컨트롤러에서 하는 일이 정말 많아졌다. 위에 v5 구조에서 설명한 내용을 하나씩 구현하면 된다.
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
// URI를 Object로 매핑하기 때문에 다양한 Controller(ex. ControllerV3, ControllerV4)가 들어올 수 있음
private final Map<String,Object> handlerMappingMap = new HashMap<>();
// 어댑터가 여러 개 담겨 있고, 그 중에 하나를 써야 하기 때문에 List로 구현해 둠
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
// 생성자로 핸들러 매핑 정보와 지원하는 어댑터 목록을 채움
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
// 핸들러 매핑 정보
public 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());
}
// 핸들러 어댑터 목록
public void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
// 프론트 컨트롤러의 service
@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);
// request와 response, 핸들러를 넘겨 실제 Controller를 호출하고 ModelView를 반환 받음
ModelView mv = adapter.handle(request, response, handler);
// ModelView에 담긴 View 이름을 물리 이름으로 변환
MyView view = viewResolver(mv.getViewName());
// ModelVIew에 담긴 model 정보로 화면 렌더링
view.render(mv.getModel(), request, response);
}
// URI에 매핑된 핸들러를 반환함
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);
}
// View의 논리 이름을 물리 이름으로 변환
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
2. 유연한 컨트롤러 (v5) - 2
위에선 ControllerV3를 사용할 수 있는 어댑터와 ControllerV3만 들어 있다. ControllerV4를 사용할 수 있도록 기능을 추가해 보자. 프론트 컨트롤러를 사용하기 때문에, 구현 class를 교체하거나 메서드에 코드 몇 줄을 추가하는 방식으로 간단하게 기능을 추가할 수 있다.
ControllerV4 기능 추가
FrontControllerServletV5에 ControllerV4 기능도 추가한다.
// 핸들러 매핑 정보
public void initHandlerMappingMap() {
...
// 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());
}
// 핸들러 어댑터 목록
public void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter()); // v4 추가
}
ControllerV4HandlerAdapter
ControllerV4는 View의 이름을 반환하지만, 어댑터는 ModelView를 만들어서 반환해야 한다. 이런 경우에 어댑터가 꼭 필요한 이유를 알 수 있다.
- 어댑터는 반환받은 View 이름으로 ModelView를 만들고 형식에 맞춰 반환해 준다.
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
// 핸들러가 ControllerV4인 경우에만 처리하는 어댑터
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
// 핸들러를 ControllerV4로 캐스팅하고, paramMap, model을 만들어서 해당 Controller를 호출
// viewName을 반환 받아 ModelView 객체를 만들어 반환
@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<>();
String viewName = controller.process(paramMap, model);
// 어댑터 변환
ModelView mv = new ModelView(viewName);
mv.setModel(model);
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;
}
}
어댑터와 ControllerV4의 형식을 살펴보면 이해하기 쉽다.
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
public interface MyHandlerAdapter {
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException;
}
3. MVC 프레임워크 만들기 정리
지금까지 점진적으로 프레임워크(v1 ~ v5)를 발전시켜 봤다.
- v1: 프론트 컨트롤러 도입
- 기존 서블릿 구조를 최대한 유지하면서, 앞에서 모든 요청을 받아 해당하는 Controller를 호출해 주는 프론트 컨트롤러를 도입했다.
- 애플리케이션의 구조를 바꿔야 하는 경우, 전체적인 구조의 틀을 먼저 바꾸고 세세한 부분을 고쳐 나가는 것이 구현할 때 유리하다.
- v2: View 분리
- 생성자로 View 이름을 입력받고 RequestDispatcher 코드로 JSP forward()를 하는 MyView를 추가해, 단순 반복되는 View 로직(viewPath나 RequestDispatcher 코드)을 분리했다.
- v3: Model 추가
- View 이름과 model Map을 갖는 ModelView를 추가해 서블릿(HttpServletRequest, HttpServletResponse) 종속성을 제거했다.
- View의 논리 이름을 물리 이름으로 변환하는 viewResolver를 추가해 View 이름 중복("/WEB-INF/views/members/")을 제거했다.
- v4: 단순하고 실용적인 컨트롤러
- v3와 거의 비슷하지만, Map<String, Object>를 model로 사용해 파라미터로 넘기는 방식을 사용해 Controller에선 View 이름만 반환하도록 변경됐다.
- 구현 입장에서 ModelView를 직접 생성해 반환하지 않도록 편리한 인터페이스를 제공하게 할 수 있게 됐다.
- v5: 유연한 컨트롤러
- 어댑터를 도입해 프레임워크를 유연하고 확장성 있게 설계했다.
- 역할과 구현이 분리돼있기 때문에 기능을 추가하는 게 훨씬 쉽다. 구현체만 바꿔서 기능을 주입할 수 있다.
참고
여기에 애노테이션을 사용해 Controller를 더 편리하게 발전시킬 수도 있다. 애노테이션을 지원하는 어댑터만 추가하면 된다. 다형성과 어댑터 덕분에 기존 구조를 유지하면서 프레임워크의 기능을 확장할 수 있다.
스프링 MVC
여기서 더 발전시키면 좋겠지만, 스프링 MVC의 핵심 구조를 파악하는데 필요한 부분은 모두 만들어보았다. 사실 이번 섹션에서 작성한 코드는 스프링 MVC 프레임워크의 핵심 코드의 축약 버전이고, 구조도 거의 같다.
스프링에서 사용하는 핸들러 어댑터 예시 → RequestMappingHandlerAdapter
: @RequestMapping("/hello")이 있는 Controller를 처리해주는 어댑터
다음 섹션부턴 스프링 MVC의 구조를 이해하고, 기본 기능을 학습한 뒤 직접 웹 페이지를 만들어 본다.