[스프링MVC 25] MVC 프레임워크 만들기: 단순하고 실용적인 컨트롤러 (tistory.com)
[스프링MVC 24] MVC 프레임워크 만들기: Model 추가 (*중요*) (tistory.com)
지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.
근데, 어떤 개발자는 ControllerV3방식으로 개발하고 싶고, 어떤 개발자는 ControllerV4방식으로 개발하고 싶다면 어떻게 해야할까?
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
ControllerV3 , ControllerV4는 완전히 다른 인터페이스이므로, 호환이 불가능하다. (v3는 110v이고, v4는 220v인 전기 콘센트 같은 것) 이럴 때 사용하는 것이 바로 어댑터이다.
어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자.
어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계!
- 핸들러 어댑터: 중간에서 어댑터 역할을 해주는 덕분에, 다양한 종류의 컨트롤러를 호출할 수 있다.
- 핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.
- MyHandlerAdapter
- 어댑터는 이렇게 구현해야 한다는 어댑터용 인터페이스
public interface MyHandlerAdapter {
boolean supports(Object handler); //해당 컨트롤러(handler)를 처리할 수 있는지 판단
ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException;
//실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환
// 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 대신 ModelView를 직접 생성해서라도 반환해야 한다.
}
이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.
먼저 ControllerV3를 지원하는 어댑터를 구현해보자.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3); //ControllerV3만을 처리할 수 있는 어댑터
}
@Override
public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
ControllerV3 controller = (ControllerV3) handler; //handler를 ControllerV3로 캐스팅
Map<String, String> paramMap = createParamMap(req); //V3 형식에 맞도록 호출
ModelView mv = controller.process(paramMap);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest req) {
Map<String, String > paramMap=new HashMap<>();
req.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
return paramMap;
}
}
- 해당 컨트롤러(handler)를 처리할 수 있는지 판단
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
- ControllerV3만을 처리할수 있는 어댑터이다. handler가 ControllerV3인 경우에만 true를 반환한다.
- supports()를 통해 ControllerV3만 지원하기 때문에, 타입 변환은 걱정없이, handler를 ControllerV3로 캐스팅
ControllerV4를 지원하는 어댑터도 구현해보자.
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4); //handler가 ControllerV4인 경우에만 처리하는 어댑터
}
@Override
public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler; //1. handler를 ControllerV4로 캐스팅
//2. paramMap과 model을 만들어서 해당 컨트롤러를 호출
Map<String, String> paramMap = createParamMap(req);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model); //3. viewName을 반환 받는다
/**뷰의 이름을 ModelView로 만들어서 형식을 맞추어 반환한다.*/
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest req) {
Map<String, String> paramMap = new HashMap<>();
req.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
return paramMap;
}
}
- 어댑터 변환 (*중요*)
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
- 어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환한다. 그런데 어댑터는 뷰의 이름이 아니라 ModelView를 만들어서 반환해야 한다. → ControllerV4는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식(타입)을 맞추어 반환한다. (*어댑터가 꼭 필요한 이유*)
- 어댑터를 사용해서, 110v 전기 콘센트를 220v 전기 콘센트로 변경하듯이!
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;
}
- 컨트롤러(Controller) → 핸들러(Handler)
- 이전에는 컨트롤러를 직접 매핑해서 사용했다. 그런데 이제는 어댑터를 사용하기 때문에, 컨트롤러 뿐만 아니라 어떤 것이라도 어댑터가 지원하기만 하면(해당하는 종류의 어댑터만 있으면), URL에 매핑해서 사용할 수 있다.
- 매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변경되었다.
private final Map<String, Object> handlerMappingMap = new HashMap<>();
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>(); //매핑 정보 Object
private final List<MyHandlerAdapter> handlerAdapters=new ArrayList<>();
public FrontControllerServletV5(){
initHandlerMappingMap(); //핸들러 매핑을 초기화(등록)
initHandlerAdapters(); //어댑터를 초기화(등록)
}
private void initHandlerMappingMap() { //핸들러 매핑정보( handlerMappingMap )에 컨트롤러를 추가
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 req, HttpServletResponse resp) throws ServletException, IOException {
Object handler=getHandler(req); /**1. 핸들러 찾기*/
if (handler == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter=getHandlerAdapter(handler); /**2. 핸들러를 처리할 수 있는 핸들러어댑터 찾기*/
ModelView mv = adapter.handle(req, resp, handler); /**3. 실제 어댑터 호출*/
//4. 어댑터는 handler(컨트롤러)를 호출하고, 그 결과를 어댑터에 맞추어(ModelView) 반환
MyView view= viewResolver(mv.getViewName());
view.render(mv.getModel(), req, resp);
}
private Object getHandler(HttpServletRequest req) {
String requestURI = req.getRequestURI();
return handlerMappingMap.get(requestURI);
//핸들러 매핑정보(handlerMappingMap)에서, URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) { //핸들러를 처리할 수 있는 어댑터가 있는지
return adapter;
//핸들러가 ControllerV3인터페이스를 구현했다면 (supports()가 true이면),
// ControllerV3HandlerAdapter 객체가 반환된다
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없어요! handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
- 핸들러 매핑정보(handlerMappingMap)에서, 핸들러 조회
- 핸들러어댑터 목록(handlerAdapters)에서, 핸들러(해당 컨트롤러)를 처리할 수 있는 핸들러어댑터 조회
- (2에서 찾은) 실제 핸들러어댑터가 호출된다.
- 핸들러어댑터는 핸들러(컨트롤러)를 호출하고
- 그 결과를 어댑터에 맞추어(ModelView) 반환
- viewResolver 호출하면
- viewResolver가 MyView 객체를 반환
- render(model) 호출
여기에 애노테이션을 사용해서 컨트롤러를 더 편리하게 발전시킬 수도 있다. 만약 애노테이션을 사용해서 컨트롤러를 편리하게 사용할 수 있게 하려면 어떻게 해야할까? 바로 애노테이션을 지원하는 어댑터를 추가하면 된다!
다형성과 어댑터 덕분에 기존 구조를 유지하면서, 프레임워크의 기능을 확장할 수 있다.
스프링 MVC의 핵심 구조를 파악하는데 필요한 부분은 모두 만들어보았다! 지금까지 작성한 코드는 스프링 MVC 프레임워크의 핵심 코드의 축약 버전이고, 스프링 MVC와 거의 같은 구조이다.
[스프링MVC 27] 스프링 MVC 구조 (tistory.com)
[스프링MVC 22] MVC 프레임워크 만들기: 프론트 컨트롤러 (FrontController) (tistory.com)
'Spring Tutorial' 카테고리의 다른 글
[스프링MVC 28] 스프링 MVC 구조: 핸들러 매핑과 핸들러 어댑터 (0) | 2023.08.05 |
---|---|
[스프링MVC 27] 스프링 MVC 구조 (0) | 2023.08.03 |
[스프링MVC 25] MVC 프레임워크 만들기: 단순하고 실용적인 컨트롤러 (0) | 2023.07.31 |
[스프링MVC 24] MVC 프레임워크 만들기: Model 추가 (*중요) (0) | 2023.07.30 |
[스프링MVC 23] MVC 프레임워크 만들기: View 분리 (0) | 2023.07.29 |