- 해당 게시물은 인프런 - "스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술" 강의를 참고하여 작성한 글 입니다.
- 유료강의이므로 자세한 내용은 없고, 간단한 설명 위주로 정리했습니다.
강의 링크 -> 김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(유료강의)
FrontController 패턴
- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
- 요청에 맞는 컨트롤러를 찾아서 호출
- 공통 처리 가능
- 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
프론트 컨트롤러 기능을 도입할 것이다.
프론트 컨트롤러가 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져가기 위해,
서블릿과 비슷한 모양의 컨트롤러 인터페이스를 생성한다.
src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
인터페이스를 구현한 회원 등록 폼, 회원 저장, 회원 목록 조회 컨트롤러를 만든다.
src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// MvcMemberFormServlet과 동일한 내용
}
}
src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1
public class MemberSaveControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// MvcMemberSaveServlet과 동일한 내용
}
}
src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1
public class MemberListControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// MvcMemberListServlet과 동일한 내용
}
}
클라이언트의 요청을 받고, 매핑 정보로 컨트롤러를 호출해주는 프론트 컨트롤러를 생성한다.
src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServletV1
프론트 컨트롤러는 서블릿이므로 서블릿 어노테이션을 추가한다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
매핑 정보를 담을 HashMap을 만들어 준다.
private Map<String, ControllerV1> controllerMap = new HashMap<>();
// mapping 정보
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
...
}
URI를 조회해 호출할 컨트롤러를 Map에서 찾고, 만약 없으면 404 상태 코드를 반환한다.
컨트롤러를 찾으면 controller.process(request, response);
을 호출해서 해당 컨트롤러를 실행한다.
// ex) /front-controller/v1/members/new-form
String requestURI = request.getRequestURI();
// URI로 controllerMap에서 호출할 컨트롤러 찾음
ControllerV1 controller = controllerMap.get(requestURI);
// 해당 controller에 대한 mapping 정보가 없으면
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404
return;
}
// 컨트롤러 호출
controller.process(request, response);
http://localhost:8080/front-controller/v1/members/new-form에 들어가면 정상적으로 실행되는 것을 확인할 수 있다.
모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않아 별도로 뷰를 처리하는 객체를 만들 것이다.
src/main/java/hello/servlet/web/frontcontroller/MyView
public class MyView {
// 뷰 경로
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
// rendering, 뷰로 이동
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2
v1에서 만든 컨트롤러 인터페이스와 다르게, v2에서는 MyView를 반환해준다.
왜냐하면 v1에서는 컨트롤러에서 알아서 view를 호출했기 때문이다.
// MyView 반환
MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
MyView로 인해 뷰로 이동하는 3줄의 코드를 1줄로 줄일 수 있게 되었다.
src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2
// 기존 코드
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
↓
// 변경 코드
return new MyView("/WEB-INF/views/new-form.jsp");
똑같이 MemberSaveControllerV2
, MemberListControllerV2
컨트롤러를 만들어,
MyView 객체를 생성하고, 뷰 이름을 넣어 반환해준다.
기존 v1 코드의 내용과 똑같으며 대신 v1에서 컨트롤러를 호출 했던 부분을
MyView를 호출하여, 뷰(JSP) 로 이동하는 코드로 변경한다.
src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServletV2
// 기존 코드
controller.process(request, response);
↓
// 변경 코드
MyView view = controller.process(request, response); // view: return new MyView("...");
view.render(request, response); // rendering, 뷰로 이동
http://localhost:8080/front-controller/v2/members/new-form에 들어가면 정상적으로 실행되는 것을 확인할 수 있다.
서블릿 종속성 제거
뷰 이름 중복 제거
ModelView
- 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체
- 컨트롤러에 HttpServletRequest를 사용할 수 없어 request.setAttribute()도 호출할 수 없다.
src/main/java/hello/servlet/web/frontcontroller/ModelView
ModelView에는 view의 논리 이름과, Model이 있다.
private String viewName; // view 논리 이름
private Map<String, Object> model = new HashMap<>(); // model
그리고 viewName에 대한 생성자를 만들고, viewName과 model의 getter와 setter를 만들어 준다.
src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3
v3의 컨트롤러 인터페이스는 방금 위에서 만든 ModelView를 반환해준다.
// modelview 반환
ModelView process(Map<String, String> paramMap);
src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3
파라미터 정보는 map에 담겨있어, map에서 필요한 요청 파라미터를 조회한다.
// 기존 코드
String username = request.getParameter("username");
↓
// 변경 코드
String username = paramMap.get("username");
view의 논리적 이름을 넣어, ModelView를 생성하고,
모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 넣고 반환한다.
// 기존 코드
request.setAttribute("member",member);
return new MyView("/WEB-INF/views/save-result.jsp");
↓
// 변경 코드
ModelView mv = new ModelView("save-result");
mv.getModel().put("member",member);
return mv;
ModelView로 인해 서블릿 종속성을 제거하였고, 뷰 이름 중복도 제거하였다.
이를 활용해 MemberFormControllerV3
, MemberListControllerV3
컨트롤러를 만들어, ModelView 객체를 생성해 반환해준다.
v3의 프론트 컨트롤러에서는 paramMap을 만들어 주고, 논리 이름으로 실제 경로를 찾을 수 있게 만들어야 한다.
src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServletV3
요청한 request에 있는 모든 정보를 꺼내, paramMap에 넣어주고
controller를 호출해, 논리 이름이 들어간 ModelView를 반환해준다.
// paramMap
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
}
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에 있는 논리 이름을 viewResolver를 통해 실제 경로 이름을 반환한다.
// 논리 이름 -> 실제 경로 이름
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
}
// 실제 경로
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
model을 함께 넣어 view를 rendering 한다.
view.render(mv.getModel(), request, response);
그러나 render를 할 때, 파라미터 3개에 대한 메서드가 없어 새로 추가해준다.
src/main/java/hello/servlet/web/frontcontroller/v3/MyView
JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, model에 있는 데이터를 꺼내 request.setAttribute() 로 request에 model 데이터를 담는다.
// v3 rendering
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);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
그러면 http://localhost:8080/front-controller/v3/members/new-form 에 들어가면 정상적으로 작동하는 것을 확인할 수 있다.
위에서 만든 v3는 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거로워, 좀 더 편리하게 v4를 만들 것이다.
v4는 v3와 다르게 컨트롤러가 ModelView가 아닌 ViewName을 반환한다.
src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4
param로 paramMap과 model을 받아 컨트롤러를 호출하고, ViewName을 반환받는다.
String process(Map<String, String> paramMap, Map<String, Object> model);
src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4
ModelView를 생성하고, model에 데이터를 보관할 필요없이
param으로 받은 model에 바로 데이터를 보관하고 viewName을 반환해준다.
// 기존 코드
ModelView mv = new ModelView("save-result");
mv.getModel().put("member",member);
return mv;
↓
// 변경 코드
model.put("member", member);
return "save-result";
이를 활용해 MemberFormControllerV4
, MemberListControllerV4
컨트롤러를 만들어, viewName 반환해준다.
src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServletV4
v4에서는 프론트 컨트롤러에서 컨트롤러를 호출할 때, param으로 model을 넘겨주기 때문에
model을 생성하여 컨트롤러를 호출하고, viewName을 반환 받는다.
// 기존 코드
ModelView mv = controller.process(paramMap);
↓
// 변경 코드
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
그래서 반환받은 viewName으로 바로 실제 경로 이름을 찾고, rendering 한다.
MyView view = viewResolver(viewName);
view.render(model, request, response);
그러면 http://localhost:8080/front-controller/v4/members/new-form에 들어가면 정상적으로 작동하는 것을 볼 수 있다.
만약 ControllerV3, ControllerV4 방식 둘 다 사용하고 싶을 때,
어탭터 패턴을 사용하여 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경할 수 있다.
핸들러 어댑터
중간에 어댑터 역할을 하는 어댑터
다양한 종류의 컨트롤러를 호출해줌
핸들러
컨트롤러의 이름을 더 넓은 범위
해당하는 종류의 어댑터만 있으면 다 처리
src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter
어댑터가 해당 컨트롤러를 처리할 수 있는지 boolean 타입으로 판단해주고,
어댑터가 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환하는 인터페이스를 만들어 준다.
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException;
핸들러를 호출하여, ModelView를 반환하는 핸들러 어댑터를 생성한다.
src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter
핸들러(컨트롤러)가 ControllerV3를 상속받는지 확인하는 메서드를 만든다.
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
핸들러(컨트롤러)를 ControllerV3에 맞게 변환하고,
ControllerV3는 paramMap을 받아 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;
}
src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5
모든 핸들러(컨트롤러) 가 들어갈 수 있는 map과 핸들러 어탭터가 들어있는 List 생성한다.
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());
...
}
// 핸들러 어댑터 등록
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
그러면 핸들러 매핑 정보를 통해 핸들러(컨트롤러) 를 조회하고,
핸들러 어댑터 목록에서 핸들러(컨트롤러)를 처리할 수 있는 핸들러 어탭터를 조회한다.
Object handler = getHandler(request);
MyHandlerAdapter adapter = getHandlerAdapter(handler);
}
// URI에 대한 handler(controller)를 handlerMappingMap에서 찾아서 반환
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)) { // 어댑터가 handler를 지원하는지
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
그리고 핸들러 어댑터로 핸들러(컨트롤러) 를 호출하고, modelView 반환한다.
ModelView mv = adapter.handle(request, response, handler);
프론트 컨트롤러는 반환받은 modelView로 viewResolver를 호출해 Myview를 반환하고, render(model) 호출한다.
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
// 논리 이름 -> 실제 경로 이름
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
http://localhost:8080/front-controller/v5/v3/members/new-form에 들어가면 정상적으로 작동되는 것을 확인할 수 있다.
ControllerV4도 사용할 수 있도록 기능을 추가할 것이다.
src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter
ControllerV4를 지원하는 어댑터를 만들어 준다.
ControllerV3HandlerAdapter와 다른 점은
ControllerV4는 param으로 paramMap, model을 받아 논리 이름 viewName 반환한다는 점이다.
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
그러나 어댑터는 ModelView로 반환해야 하므로, 어댑터가 ModelView 형식으로 맞추어 반환해준다.
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5
핸들러 매핑에 ControllerV4를 사용하는 핸들러(컨트롤러)를 추가하고,
해당 컨트롤러를 처리하는 어댑터인 ControllerV4HandlerAdapter도 추가한다.
private void initHandlerMappingMap() {
...
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
...
}
private void initHandlerAdapters() {
...
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
그러면 http://localhost:8080/front-controller/v5/v4/members/new-form에 들어가면 정상적으로 작동하는 것을 확인할 수 있다.
지금까지 김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(유료강의) 강의를 참고하여 MVC 프레임워크 만들기 에 대해 공부하였다.