후라이
[Spring MVC] MVC 프레임워크 <2> 본문
여기까지 만들었던 MVC 프레임워크도 상당히 잘 설계된 컨트롤러입니다.
그런데, 모든 코드를 공개하진 않았지만, Controller interface를 구현하는 과정에 있어서 항상 ModelView 객체를 만들고, 반환해야하는 부분이 번거롭게 느껴집니다.
실제 개발하는 개발자들에게도 실용성 있는 코드가 되어야겠죠..?!
이제 우리는 더 편리하고 단순한 컨트롤러를 만들어 봅시다.
1. V4 : 실용적인 Controller로 개선
실제 Version3과의 구조를 비교해보면 크게 달라진 점은 없겠습니다만,
달라진 점은 Controller가 반환하는 값이 Model View가 아닌 단순히 ViewName만을 반환하게 된다는 것입니다.
public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/ String process(Map<String, String> paramMap, Map<String, Object> model);
}
해당 인터페이스에는 ModelView가 없습니다. model 객체도 파라미터로 전달되도록 수정했기 때문에 결과로는 뷰의 이름만 반환해주면 됩니다.
public class MemberListControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
그럼 실제 interface를 구현한 MemberListController에서도 paramMap과 model를 파라미터로 받기 때문에
딱히 ModelView에 대한 특별한 생성없이 그냥 model.put("members", members)만 하면 됩니다.
그리고 뷰 이름인 "members"를 반환하면 되겠습니다.
완전 간단해지죠?
Front Controller 단에서도
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
- Http 요청으로부터 파라미터 맵을 끌어오는 건 이전과 동일하며
- Map<String, Object> model을 추가 됩니다. 원래는 Model View를 생성해서 model을 불러왔었죠?
- Controller의 반환 값인 뷰 이름을 가지고 똑같이 viewResolver를 통해 물리주소로 변환합니다.
- 변환된 물리주소를 통해 rendering하면 됩니다.
전 게시글에서 언급을 안한 것 같아 말하자면, 원래 JSP는 request.getAttribute()로 데이터를 조회하는데, 우리는 Servlet을 Controller 내에서 쓰지 않고 로직을 설계하고 있으므로 뷰 객체의 render() 함수에서 model 정보도 함께 받도록 합니다.
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
그래서 render() 함수는 위처럼 받은 모델 정보에 대해 rendering하는 겁니다!!
2. V5 : 유연한 컨트롤러
만약, 지금까지 설계한 V1~V4까지의 컨트롤러 방식에 있어서
개발자마다 추구하는 개발 방식이 다를 수 있겠죠.
물론 V1, V2는 비효율적인 부분이 많았으므로 제외하고 어떤 개발자는 V3의 Controller 방식을 추구할 수도 있구요
어떤 개발자는 V4의 Controller 방식을 추구할 수도 있습니다.
우리는 유연한 컨트롤러 방식을 위해 "어댑터 패턴"을 도입합니다.
어댑터 패턴을 사용하면, Front Controller가 다양한 방식의 Controller를 처리할 수 있게 됩니다.
- 핸들러 어댑터 : 중간에 어댑터 역할을 하는 부분이 추가되었는데, 다양한 핸들러(=컨트롤러)를 호출할 수 있습니다.
여기서 말하는 컨트롤러는 단순히 MemberFormController, MemberSaveController, MemberListController가 아니라
V3 Controller, V4 Controller를 말하는 겁니다!! - 핸들러 : 컨트롤러의 이름을 더 넓은 범위로 표현한 것입니다. 어댑터가 있으면 꼭 컨트롤러의 개념이 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문입니다.
2.1) Adapter Interface
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
우선 어댑터의 인터페이스를 구상해봅시다. handler는 사실상 controller를 의미합니다.
어댑터가 해당 컨트롤러를 처리할 수 있는지의 여부를 boolean으로 판단하는 것 입니다.
2.2) ControllerV3HandlerAdapter
ModelView handle() : 어댑터가 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환하도록 합니다.
그럼 이전 게시글에서 했었던 V3 버전의 컨트롤러를 다시 리마인드 해보며 ControllerV3HandlerAdapter를 만들어봅시다.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(req);
ModelView mv = controller.process(paramMap);
return mv;
}
}
일단, 인터페이스에 맞게 코드를 작성하면 됩니다. supports 함수는 간단하니 넘어가고
handle() 함수를 보면, 우선 V3에서는 controller.process의 결과로 ModelView를 반환했었습니다.
Object 타입으로 받은 handler를 컨트롤러 V3으로 변환하고, 이후 로직은 동일합니다.
2.3) FrontController
자 그럼 FrontController는 어떻게 되는 걸까요?
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String,Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandleMappingMap();
initHandlerAdapters();
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Object handler = getHandler(req);
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(req, resp, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), req,resp);
}
private Object getHandler(HttpServletRequest req) {
String requestURI = req.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for(MyHandlerAdapter adapter : handlerAdapters) {
if(adapter.supports(handler)) {
return adapter;
}
}
}
}
설명에 필요한 코드만 가져온 거라 실행해도 돌아가진 않습니다,,ㅜ
우선, FrontController 코드가 상당히 복잡해진 것 처럼 보이겠지만,,어댑터 부분이 추가되어 그렇지 어렵진 않습니다!
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());
이 부분을 의미하는 겁니다. Http 요청으로 들어온 URL과 매핑하기 위해 따로 Map에 넣어준 것이에요.
service 함수 부분을 봅시다.
getHandler() :
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
요청으로받은 requestURI의 키 값과 동일한 controller를 앞서 설명한 handlerMappingMap에서 찾아옵니다.
그렇게 반환된 handler를 또 getHandlerAdapter에 갖다 넣고 있죠?
getHandlerAdapter() :
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
getHandlerAdapter에서는 찾은 controller가 handlerAdapters 내에서 호출 가능한 상태로 있는지를 확인합니다.
유료 강의라서 모든 코드를 넣지 않았기에 해석의 어려움이 있으시겠지만,,
handlerAdapters에는 우리가 앞서 예시로 만들었던 ControllerV3HandlerAdapter와 같이 어댑터 구현체들이 들어있습니다.
여기에 V3는 물론이거니와 V4 등등 더 많은 Handler Adapter들이 들어갈 수 있겠죠!
그래서 이 handlerAdapters라고 하는 리스트를 뒤지면서 내가 Mapping Map에서 찾아온 그 handler가 지금 호출 가능한 상태인지, 즉 V3 Controller가 지금 있어?? 하는 걸 불러오는 부분입니다.
그렇게 해서 불러진 handler는 당연히 Model View를 반환해야 하므로
ModelView mv = adapter.handle(request, response, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
이렇게 똑같이, 원래 V3 Controller에서 했었던 것 처럼 물리주소 변환 후 렌더링 하면 됩니다.
사실 이렇게 Controller 한 개만 가지고 테스트 하는 것보다
V4 Controller까지 도입한 상태에서 테스트해보면 감이 더 잘 잡힐 듯 합니다.
private void initHandleMappingMap() {
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());
}
스프링 MVC가 이미 많은 기능들을 지원해주고 있지만,
MVC 개념을 익히는데 있어 MVC 프레임워크를 직접 짜보니 느낌을 쉽게 습득할 수 있었네요:)
[참고] : 인프런-김영한 < 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 >
'Spring' 카테고리의 다른 글
[Spring MVC] MVC 프레임워크 <1> (1) | 2024.12.19 |
---|---|
[Spring MVC] 웹 시스템 | 서블릿 | HTML | HTTP API (0) | 2024.12.15 |
객체지향에서의 다형성, 그리고 OCP (2) | 2024.12.07 |
[Spring Boot] - 스프링 입문 (6) (1) | 2024.02.13 |
[Spring Boot] - H2 데이터베이스 연결 / Database not found (0) | 2024.02.12 |