MVC 프레임워크
1. 프론트 컨트롤러 패턴
[프론트 컨트롤러 도입 전]
프론트 컨트롤러 도입전에는 공통된 작업을 각 컨트롤러를 호출할 때마다 처리하였다.
[프론트 컨트롤러 도입 후]
프론트 컨트롤러 도입 시, 기존의 공통된 작업을 프론트 컨트롤러가 처리하고 필요한 컨트롤러를 실행한다.
2. 프론트 컨트롤러 도입 - 기본
프론트 컨트롤러는 다음과 같이 동작한다.
[프론트 컨트롤러 도입 - 기본]
우선 프론트 컨트롤러에서 URL을 매핑한 뒤 알맞은 컨트롤러를 조회 한다.
조회된 컨트롤러를 호출해서 로직을 실행시키고 컨트롤러에서 JSP로 모델을 넘긴다.
[컨트롤러 인터페이스 - 기본]
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 각 컨트롤러들은 이 인터페이스를 구현하면 된다.
프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가질 수 있다.
[회원 등록 컨트롤러 - 기본]
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
[회원 저장 컨트롤러 - 기본]
public class MemberListControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
컨트롤러는 새로 만든 컨트롤러의 인터페이스를 따를 뿐 기존과 거의 같은 형식이다.
[프론트 컨트롤러 - 기본]
@WebServlet(name = "frontControllerServletV1",
urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form",
new MemberFormControllerV1());
controllerMap.put("/front-controller/v1/members/save",
new MemberSaveControllerV1());
controllerMap.put("/front-controller/v1/members",
new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
· urlPatterns
- urlPatterns = "/front-controller/v1/*" : /front-controller/v1를 포함한 모든 하위 요청은 해당 서블릿(프론트 컨트롤러)에서 받아들인다.
· controllerMap
- 매핑 URL과 그에 따라 호출될 컨트롤러가 저장된다.
· service(request, response)
- requestURI를 조회해서 실제 호출할 컨트롤러를 controllerMap에서 찾는다. 만약 없다면 404 상태코드를 반환한다.
- 컨트롤러를 호출하고 process(request, response)를 호출해서 해당 컨트롤러를 실행한다.
※ JSP는 기존에 사용했던 양식 그대로 사용한다.
3. View 분리 & Model 생성
앞의 MVC 방식은 모든 컨트롤러에 View로 이동하는 부분이 있어 중복이 발생한다.
[View 이동 중복]
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
이를 해결하기 위해 MyView라는 별도의 객체를 만들어서 View로 이동하는 작업을 처리하게 한다.
또한 현재 방식은 컨트롤러가 HttpServletRequest와 HttpServletResponse를 사용하기 때문에 코드가 서블릿에 종속적이다.
이는 요청 파라미터 정보를 자바의 Map으로 넘기고 Model 부분은 별도의 Model 객체를 만들어서 반환하는것으로 해결 할 수 있다.
마지막으로 뷰 이름의 중복을 제거한다.
즉, 컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화 한다.
ex)/WEB-INF/views/new-from.jsp -> new-form (논리 이름)
이 방식을 사용하면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.
[MVC 구조 - View, Model 개선]
먼저 프론트 컨트롤러는 조회된 URL를 사용해 그에 맞는 컨트롤러를 조회하고 내부 로직을 실행한다.
컨트롤러는 View에 필요한 데이터(Model)와 View 이름(논리 이름)이 담긴 ModelView를 반환한다.
viewResorlver는 View 논리 이름을 실제 물리적 경로로 변환해서 MyView를 반환한다.
프론트 컨트롤러는 MyView를 사용해 View에 Model을 넘긴다.
[MyView]
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
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));
}
}
modelToRequestAttribute(model, request)는 model 객체에 담긴 데이터를 request에 저장한다.
render(model, request, response)는 model에 담긴 데이터를 request에 저장하고 request와 response를 viewPath로 이동시킨다.
MyView 덕분에 이제 각 컨트롤러는 데이터를 View로 이동시키는 작업을 하지 않아도 되고 단지 Model과 view의 논리이름(ModelView)만을 반환하면 된다.
[ModelView]
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
Model과 View의 논리이름을 담는 객체이다.
[회원 등록 폼 - View, Model 개선]
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
ModelView를 반환하고 HttpServletRequest, HttpServletResponse가 아닌 Map을 매개변수로 받는다.
등록 폼은 View에 전달할 데이터가 없기 때문에 ModelView에 View의 논리 이름만 담아서 반환한다.
[회원 저장 폼 - View, Model 개선]
public class MemberSaveControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
ModelView에 View의 논리 이름인 "save-result"를 담고, model에 "member" 데이터를 추가해서 반환한다.
[프론트 컨트롤러 - View, Model 개선]
@WebServlet(name = "frontControllerServletV3",
urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private Map<String, ControllerV3> controllerMap = new HashMap<>();
public FrontControllerServletV3() {
controllerMap.put("/front-controller/v3/members/new-form",
new MemberFormControllerV3());
controllerMap.put("/front-controller/v3/members/save",
new MemberSaveControllerV3());
controllerMap.put("/front-controller/v3/members",
new MemberListControllerV3());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyView viewResolver(String viewName) {
MyView view = new MyView("/WEB-INF/views/" + viewName + ".jsp");
return view;
}
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;
}
}
URI 매핑까지는 전과 같다.
request내의 요청 파라미터를 Map으로 옮긴다.
- createParamMap(HttpServletRequest) : HttpServletRequest에서 파라미터 정보를 꺼내 Map으로 변환한다.
컨트롤러에 요청 파라미터가 담긴 paraMap을 넘겨 로직을 실행시키고 ModelView를 반환 받는다.
ModelView에서 View의 논리 이름얻고 viewResolver를 사용해 View의 실제 경로로 MyView를 반환받는다.
- viewResolver(Stirng) : 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경하고 이를 이용해 MyView 객체를 반환한다.
마지막으로 MyView를 사용해 View에 model을 넘긴다.
MyView와 ModelView를 사용하므로써 프론트 컨트롤러는 복잡해졌지만 개별 컨트롤러는 굉장히 간결해졌다.
또한 이제 더이상 각 컨트롤러가 서블릿에 종속적이지 않다.
이처럼 새로운 MVC 프레임워크로 편의성이 많이 늘어나고 컨트롤러 부분의 코드가 간결해졌다.
그러나 아직 한가지 불편한점이 있다. 각 컨트롤러에서 매번 ModelView 객체를 생성하고 반환해야하는 부분이 조금 번거롭게 보인다.
따라서 이번에는 프론트 컨트롤러에서 ModelView 객체를 생성하고 각 컨트롤러에게 넘겨주는 방식을 적용해본다.
[컨트롤러 인터페이스 - model 개선]
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
이제 컨트롤러는 ModelView를 생성하지 않고 model을 매개변수로 받고 View의 논리 이름을 반환한다.
[회원 등록 폼 - model 개선]
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
이제 더이상 ModelView를 생성할 필요가 없다. 단지 View의 논리 이름을 반환하면 된다.
[회원 저장 폼 - 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";
}
}
ModelView를 생성하지 않고 매개변수로 받은 model에 데이터를 넣는다.
[프론트 컨트롤러 - model 개선]
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
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());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
// model 생성, controller에 전달
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
private MyView viewResolver(String viewName) {
MyView view = new MyView("/WEB-INF/views/" + viewName + ".jsp");
return view;
}
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;
}
}
전의 프론트 컨트롤러에서 model만 새로 생성해서 컨트롤러에 넘겨준다.
각 컨트롤러를 보면, 프론트 컨트롤러에서 model을 생성하는 공통 작업을 처리하는 것으로 컨트롤러의 코드가 더욱 깔끔해진것을 알 수 있다.
4. 유연한 컨트롤러
다양한 컨트롤러 인터페이스가 존재할 때 상황에 따라 유연하게 컨트롤러를 선택해서 개발할 수 있을까?
지금까지의 예제에서는 프론트 컨트롤러가 단 하나의 컨트롤러 인터페이스만 사용할 수 있었다.
이번엔 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 컨트롤러들을 처리할 수 있도록 변경해본다.
[MVC 구조 - 어댑터 패턴 적용]
이제 컨트롤러를 더 넓은 범위의 핸들러라 칭한다.
프론트 컨트롤러는 기존과 같이 바로 핸들러(컨트롤러)를 호출하는 것이 아니라 핸들러를 처리할 수 있는 핸들러 어댑터를 조회하고 핸들러 어댑터를 통해 핸들러를 호출하고 ModelView를 반환 받는다.
이제 핸들러가 어떤 인터페이스를 사용하든간에 프론트 컨트롤러는 핸들러 어댑터가 반환하는 ModelView를 받아 사용하면 된다.
[어댑터 인터페이스]
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException, IOException;
}
support(handler) : 어댑터가 핸들러를 처리할 수 있는지 판단하는 메서드이다.
handle(request, response, handler) : 핸들러를 호출하고 ModelView를 반환한다.
[핸들러 어댑터 - model 개선]
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 ServletException, IOException {
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;
}
}
개선된 핸들러(컨트롤러)는 요청 파라미터가 담긴 Map과 model을 매개변수로 받고 View의 논리 이름을 반환한다.
때문에 Map과 model을 생성해 핸들러(컨트롤러)에 넘긴다.
어댑터는 반드시 ModelView를 반환해야 하기 때문에 View의 논리 이름과 model을 담아 ModelView를 반환한다.
[프론트 컨트롤러 - 유연한 컨트롤러]
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
//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 ControllerV4HandlerAdapter());
}
@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 MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException(
"handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyView viewResolver(String viewName) {
MyView view = new MyView("/WEB-INF/views/" + viewName + ".jsp");
return view;
}
}
핸들러 어댑터를 담을 handleAdapters라는 리스트가 추가되었다.
gethandle(request) : URL과 핸들러를 매핑해 핸들러를 반환한다.
getHandlerAdapter(handler) : 핸들러를 지원하는 어댑터를 찾아서 반환한다.
프론트 컨트롤러는 이제 model이나 요청 정보를 가공하지 않아도된다.
단지 핸들러를 지원하는 어댑터를 찾아서 호출하고 ModelView를 받아서 View에 model을 넘겨주기만 하면 된다.
위와 같은 방식으로 다양한 핸들러를 지원하는 어댑터를 추가하면 유연하게 상황에 따라 핸들러를 선택할 수 있다.
'Spring > 스프링 MVC 기본' 카테고리의 다른 글
#6 스프링 MVC 기본 기능 (0) | 2021.07.07 |
---|---|
#5 스프링 MVC 구조 이해 (0) | 2021.06.30 |
#3 JSP & MVC 패턴 적용 (0) | 2021.06.28 |
#2 서블릿 (0) | 2021.06.23 |
#1 스프링 웹 애플리케이션 이해 (0) | 2021.06.23 |
댓글