https://wngml56.tistory.com/238
와 이어지는 글입니다.
이번에는 한 개의 프론트 컨트롤러에서 여러 종류의 컨트롤러를 처리할 수 있는 어댑터 패턴에 대해 작성하겠습니다.
목차
유연한 프론트 컨트롤러 - v5
만약 ControllerV3와 ControllerV4 모두를 사용하고 싶다면 어떻게 해야 할까요?
public interface ControllerV3 {
//paraMap을 파라미터로 받고 ModelView를 반환한다.
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
//paramMap과 model을 파라미터로 받고, String을 반환한다.
String process(Map<String, String> paramMap, Map<String, Object> model);
}
FrontControllerServletV4
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());
}
- 코드를 보면 FrontControllerServletV4는 ControllerV4만 사용할 수 있도록 작성되어 있습니다.
- 만약, ControllerV4 대신 ControllerV3를 controllerMap에 put 한다면 컴파일 오류가 발생합니다.
- 이를 어댑터 패턴을 사용해 해결할 수 있습니다.
어댑터 패턴
- 지금까지 우리가 개발한 프론트 컨트롤러는 한 가지 방식의 컨트롤러 인터페이스만 사용할 수 있습니다.
- ControllerV3와 ControllerV4는 완전히 다른 인터페이스입니다.
- 마치 v3는 110v이고, v4는 220v 전기 콘센트와 같습니다. 이럴 때 사용하는 것이 바로 어댑터입니다
- 어댑터 패턴을 사용해 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변환함으로써 유연한 컨트롤러 설계가 가능합니다.
v5 구조
모든 HTTP 요청은 FrontController에서 받습니다.
- 핸들러 map에서 요청 URL에 맞는 컨트롤러를 찾습니다.
(핸들러는 컨트롤러를 의미합니다. 컨트롤러의 이름을 더 넓은 범위의 핸들러로 변경했습니다.) - 핸들러 어댑터 목록에서 핸들러에 맞는 어댑터를 조회합니다.
- 프론트 컨트롤러가 핸들러 어댑터를 호출합니다.
- 핸들러 어댑터가 핸들러를 호출합니다.
- 핸들러 어댑터는 로직에 맞춰 ModelView를 반환합니다. (v3도 v4도 ModelView를 반환하도록 변환합니다.)
- 프론트 컨트롤러는 반환된 View를 viewResolver를 통해 논리 이름을 물리 이름으로 변환합니다.
- 반환받은 MyView를 호출하고, MyView는 request에 model를 담아 View(JSP)를 렌더링합니다.
참고 핸들러
컨트롤러의 이름을 더 넓은 범위의 핸들러로 변경했습니다.
그 이유는 어댑터가 있으면 꼭 컨트롤러의 개념뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문입니다.
참고 핸들러 어댑터
프론트 컨트롤러와 핸들러 사이에 어댑터 역할을 하는 핸들러 어댑터가 추가되었습니다.
중간에서 로직에 맞게 변환해주는 이 핸들러 어댑터 덕분에 다양한 종류의 컨트롤러를 호출할 수 있습니다.
이제 코드로 구현해봅시다.
MyHandlerAdapter - 인터페이스
어댑터용 인터페이스입니다.
public interface MyHandlerAdapter {
//어댑터의 컨트롤러 처리 가능 여부를 반환합니다.
boolean supports(Object handler);
//실제 컨트롤러 호출 메서드입니다. 그 결과로 ModelView를 반환해야 합니다.
//컨트롤러가 ModelView를 반환하지 않는다면, 어댑터가 직접 ModelView를 생성해서 반환해야 합니다.
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
먼저, ControllerV3용 어댑터를 작성해봅시다.
ControllerV3HandlerAdapter
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
//ControllerV3을 처리할 수 있는 어댑터를 뜻합니다.
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
//ControllerV3 호출
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
//Object로 들어온 handler를 V3로 변환합니다. supports 메서드를 통해 호출 전 검증하기 때문에 걱정없이 변환할 수 있습니다.
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
//ControllerV3은 원래 ModelView를 반환하기 때문에, 어댑터에서 특별히 변환해주는 작업은 없습니다.
ModelView mv = controller.process(paramMap);
return mv;
}
//request의 파라미터를 map에 담아 반환합니다.
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
Enumeration<String> paramNames = request.getParameterNames();
while(paramNames.hasMoreElements()) {
String name = paramNames.nextElement();
paramMap.put(name, request.getParameter(name));
}
return paramMap;
}
}
FrontControllerServletV5
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
//핸들러 맵
//value가 v3, v4 모두 받을 수 있도록 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());
}
//어댑터 등록
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
//현재는 ControllerV3 핸들러 어댑터만 생성되었습니다.
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//요청 URL에 맞는 handler를 찾습니다.
Object handler = getHandler(request);
if(handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//handler를 처리할 수 있는 adpater를 찾습니다.
MyHandlerAdapter adapter = getHandlerAdapter(handler);
//handler 어댑터를 호출하고, ModelView를 반환받습니다.
ModelView mv = adapter.handle(request, response, handler);
//반환받은 view를 사용하여 MyView를 생성합니다.
MyView view = viewResolver(mv.getViewName());
//model과 view를 사용하여 jsp를 렌더링합니다.
view.render(mv.getModel(), request, response);
}
//요청 URI에 맞는 handler를 반환합니다.
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
//handler에 맞는 어댑터를 찾아 반환합니다.
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");
}
}
FrontControllerServletV5 - ControllerV4 추가
이제 FrontcontrollerServletV5에 ControllerV4도 추가해봅시다.
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());
//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());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter()); //V4 어댑터 추가
}
ControllerV4HandlerAdpater
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
//ControllerV4를 처리할 수 있는 어댑터를 뜻합니다.
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
//ControllerV4 호출
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
//ControllerV4에 맞춰 paramMap과 model을 만들어 컨트롤러를 호출합니다.
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
//viewName을 반환받습니다.
String viewName = controller.process(paramMap, model);
//어댑터 변환 -> view이름과 model을 사용해 ModelView로 변환합니다.
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
Enumeration<String> paramNames = request.getParameterNames();
while(paramNames.hasMoreElements()) {
String name = paramNames.nextElement();
paramMap.put(name, request.getParameter(name));
}
return paramMap;
}
}
어댑터 변환
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
- 어댑터에서 이 부분은 단순하지만 중요한 부분입니다.
- ControllerV4는 ModelView가 아니라 단순 View의 이름을 반환합니다. 어댑터는 이것을 ModelView로 만들어 형식에 맞추어 반환합니다.
- 이것이 어댑터가 꼭 필요한 이유입니다. 마치 110v 전기 콘센트를 220v 전기 콘센트로 변경하는 것과 같습니다.
어댑터와 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;
}
정리
어댑터를 사용함으로써 FrontController의 코드는 거의 손대지 않고, ControllerV3, ControllerV4 모두 처리할 수 있도록 변경했습니다.
여기에 애노테이션을 사용한 컨트롤러를 사용하고 싶다면, 애노테이션을 지원하는 어댑터만 추가하면 됩니다.
다형성과 어댑터 덕분에 기존 구조를 유지하면서 손쉽게 프레임워크의 기능을 확장할 수 있습니다.
스프링 MVC
사실 지금까지 작성한 코드는 스프링 MVC 프레임워크의 축약 버전이고, 구조도 거의 같습니다.
직접 만든 MVC 프레임워크 구조
SpringMVC 구조
직접 만든 프레임워크 → 스프링 MVC
- FrontController → DispatcherServlet
- handlerMapping → HandlerMapping
- MyHandlerAdpater → HandlerAdapter
- ModelView → ModelAndView
- viewResolver → viewResolver
- MyView → View
출처
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
'Backend > Spring | SpringBoot' 카테고리의 다른 글
[SpringBoot] HTTP 요청 조회 애노테이션 (0) | 2023.08.25 |
---|---|
[SpringBoot] 스프링 MVC 기본 애노테이션(@Controller, @RequestMapping, @PathVariable) (0) | 2023.08.25 |
[Servlet] 회원 관리 웹 애플리케이션 만들기2 (프론트 컨트롤러 패턴) (0) | 2023.08.18 |
[Servlet] 회원관리 웹 애플리케이션 만들기1 (+JSP, MVC 패턴) (0) | 2023.08.17 |
[Servlet] HttpServletRequest, HttpServletResponse (0) | 2023.08.17 |