[Spring] MVC 1편 (8) - MVC 프레임워크 만들기 v4,v5
v1, v2, v3
[Spring] MVC 1편 (7) - MVC 프레임워크 만들기 v1, v2, v3
📌 프론트 컨트롤러 ✅ 프론트 컨트롤러 도입 전 프론트 컨트롤러 도입 전에는 각 컨트롤러에서 공통 로직을 구현해줘야 했다. ✅ 프론트 컨트롤러 도입 후 모든 요청 메세지는 공통 로직이 구
suaring.tistory.com
📌 MVC 프레임워크 만들기
✅ v4 - 단순하고 실용적인 컨트롤러
v3 컨트롤러는 서블릿의 종속성을 제거하고 뷰 경로의 중복을 제거하여 잘 설계된 컨트롤러이다. 그러나 개발자 입장에서 항상 컨트롤러에서 ModelView 객체를 생성해서 반환해야 하는 부분이 번거롭다. 이번 버전에서는 조금 더 실용적인 방법으로 리팩토링 해본다.
기본적인 구조는 v3와 같지만, 각 컨트롤러는 이제 ModelView를 반환하지 않고 viewName 즉, 뷰의 논리 이름만 반환한다.
ControllerV4 인터페이스
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
- 각 컨트롤러는 뷰의 이름만 반환하기 때문에 반환타입을 String으로 설정한다.
- 또 각 컨트롤러가 객체를 저장할 model 맵을 파라미터로 전달한다.
회원 저장 컨트롤러
public class MemberSaveControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> 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";
}
}
- 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.
- 파라미터로 넘어온 모델에 값을 넣어주고 뷰 이름만 반환해주면 된다.
FrontControllerV4
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);
- 모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다.
- 컨트롤러의 process() 메서드를 실행하고, 반환받은 viewName으로 논리 이름을 물리 이름으로 바꾼 MyView 객체를 생성해서 render()를 호출한다.
✅ v5 - 유연한 컨트롤러
만약 v3, v4 컨트롤러를 모두 사용하고 싶다면? 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다. 여기에 어댑터 패턴을 적용하면 다양한 방식의 컨트롤러를 사용할 수 있게 된다.
프론트 컨트롤러는 핸들러(컨트롤러) 매핑 정보에서 요청 URL를 기반으로 핸들러를 조회한다. 다음으로 조회한 핸들러를 지원할 수 있는 핸들러 어댑터를 핸들러 어댑터 목록에서 찾는다. 이제 프론트 컨트롤러는 컨트롤러를 직접 호출하지 않고, 핸들러 어댑터가 대신 호출해준다. 프론트 컨트롤러는 핸들러 어댑터가 반환해준 넘어온 ModelView 객체를 가지고 논리 이름을 물리 이름으로 바꾸어 렌더링해주면 된다.
MyHandlerAdapter
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException, ServletException;
}
- boolean supports(Object handler)
- 어댑터가 해당 컨트롤러를 처리할 수 있으면 true를 반환한다.
- ModelView handler(HttpServletRequest request, HttpServletResponse response, Object handler)
- 어댑터는 컨트롤러를 호출하고 그 결과로 ModelView를 반환한다.
- 컨트롤러가 ModelView를 반환하지 않는 경우, 어댑터가 직접 생성해서 반환한다.
ControllerV3HandlerAdapter
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3); // ControllerV3의 구현체일 경우 true 반환
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException, ServletException {
ControllerV3 controller = (ControllerV3) handler; // supports 메서드로 확인해주고 handle을 호출하기 때문에 캐스팅해도 괜찮음
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;
}
}
- ModelView를 리턴하는 ControllerV3를 지원하는 핸들러 어댑터이다.
- 먼저 매개변수로 들어오는 핸들러를 처리할 수 있는 핸들러 어댑터인지 검사한다. (ControllerV3의 구현체인지 확인)
- 처리할 있는 핸들러라면 HTTP 요청으로부터 paramMap을 생성하고, 이것을 컨트롤러에게 넘겨주면서 컨트롤러를 실행한다.
- 컨트롤러의 process를 호출한 후 반환값인 ModelView를 프론트 컨트롤러에 전달한다.
ControllerV4HandlerAdapter
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 IOException, ServletException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> 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<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
FrontControllerV5 - 핸들러 매핑 정보, 핸들러 어댑터 목록
public class FrontControllerServletV5 extends HttpServlet {
// private Map<String, ControllerV4> controllerMap = new HashMap<>(); 기존 컨트롤러 맵
private final Map<String, Object> handlerMappingMap = new HashMap<>(); // Map의 value 타입을 Object로 설정해서 모든 컨트롤러 지원
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());
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());
}
}
- 먼저 기존처럼 핸들러 매핑 맵을 생성한다. 이때 특정 Controller 인터페이스로 Value 값을 설정하지 않고, Object로 설정해서 모든 컨트롤러가 Value로 들어올 수 있다.
- 핸들러 어댑터를 담아놓을 리스트를 생성한다.
- FrontController는 생성될 때 handlerMappingMap과 handlerAdapters를 가지고 있다.
FrontControllerV5 - 메서드
@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();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) { // 매핑 정보를 사용해서 핸들러를 찾음
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) { // 리스트에서 for문으로 handler를 support하는 어댑터를 찾음
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");
}
- service() 메서드가 실행되면 request로 들어온 URL를 기반으로 핸들러 매핑 맵에서 컨트롤러를 찾는다.
- 어댑터 목록에서 루프를 돌려 핸들러를 지원하는지 확인하고, 지원하는 어댑터를 가져온다.
- 어댑터의 handle() 메서드를 호출하고 반환된 ModelView를 렌더링한다.
→ 이제 어댑터를 사용하기 때문에 컨트롤러 뿐만 아니라, 어댑터가 지원하기만 하면 어떤 것이든 URL에 매핑하여 사용할 수 있다! (확장성 up)
강의 링크
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의
웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -
www.inflearn.com