일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 맛집
- 2839
- 알고리즘
- 2638
- mvc
- 고모네 콩탕
- 2589
- 백준
- 맛집 투어
- 쓰레드 풀
- 문자열 압축
- HTTP API
- Servlet
- dp
- 2020 KAKAO BLIND
- 포두부 보쌈
- 양꼬치
- 설탕 배달
- 스프링 MVC
- 호유동
- 다이나믹 프로그래밍
- Spring
- 스프링
- 서블릿
- 동적 프로그래밍
- 투어
- 1로 만들기
- 프로그래머스
- BFS
- 완도산회
- Today
- Total
프로그래밍 공방
[Spring] 4. 프레임워크 만들기 본문
프론트 컨트롤러 패턴
클라이언트들이 각 Controller를 호출하는게 아니라 Front Controller에 요청을 보내고 Front Controller가 각 요청에 맞는 컨트롤러를 찾아서 호출하는 패턴
Front Controller 패턴 특징
프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
공통 처리 가능
프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
* 스프링 웹 MVC의 핵심도 바로 Front Controller 이다(DispatcherServlet이 FrontController 패턴으로 구현되어 있음)
프론트 컨트롤러의 단계적 도입
프론트 컨트롤러 도입 - V1
기존 코드를 최대한 유지하면서 프론트 컨트롤러 구조를 도입
Front Controller v1의 코드는 아래와 같다
@WebServlet(name = "frontControllerServlet", urlPatterns = "/front-controller/*")
public class FrontControllerServlet extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/front-controller/members/new-form", new MemberFormController());
controllerMap.put("/front-controller/members/save", new MemberSaveController());
controllerMap.put("/front-controller/members", new MemberListController());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
Controller controller = controllerMap.get(requestURI);
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
Front Controller v1의 특징
- urlPatterns = "/front-controller/*" 로 url 패턴을 설정하여 /front-controller/를 포함한 하위 url들이 Front Controller를 먼저 거치게 한다
- controllerMap에 각 url들에 대한 컨트롤러를 매핑해서 매핑 정보를 관리한다
View 분리 - V2
v1에서는 컨트롤러에서 뷰로 이동하는 구조는 그대로이기 때문에 컨트롤러마다 뷰로 이동하는 부분에 중복이 있다
이 중복을 제거하고 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 생성한다
View를 별도로 처리하는 객체인 MyView를 생성해준다
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
컨트롤러에서 뷰로 이동하는 구조인 v1과 다르게 v2에서는 컨트롤러가 MyView를 반환하고 Front Controller에서 반환받은 MyView의 render를 호출해서 뷰로 이동한다
따라서 Controller는 아래와 같이 MyView를 생성해서 리턴하고
return new MyView("url");
Front Controller는 MyView를 리턴받아 render를 수행해준다
@WebServlet(name = "frontControllerServlet", urlPatterns = "/front-controller/*")
public class FrontControllerServlet extends HttpServlet {
/* ... */
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* ... */
MyView view = controller.process(request, response);
view.render(request, response);
}
}
View 를 처리하는 MyView 객체의 render() 를 호출하는 부분을 모두 일관되게해서 중복되는 코드를 정리할 수 있고 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 된다
Model 추가 - V3
서블릿 종속성 제거
컨트롤러가 서블릿 기술을 사용하게 된다면 구현 코드도 복잡해지고 테스트 코드 작성에도 어려움이 있다
요청 파라미터는 Map을 활용하여 넘기고 별도의 Model 객체를 생성해서 반환하게 하면 컨트롤러에서 서블릿 기술을 전혀 사용하지 않게 할 수 있다
뷰 이름 중복 제거
컨트롤러가 뷰의 논리 이름을 반환하고 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화 하자
이렇게 해두면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.
ex) /WEB-INF/views/temp.jsp -> temp
기존에는 HttpServletRequest를 사용해서 Parameter를 찾고 setAttribute로 데이터를 저장하고 뷰에 전달지만 Model을 직접 만들고 추가로 View 이름까지 전달하는 ModelView 객체를 아래와 같이 생성해서 서블릿 종속성을 제거하자
@Getter
@Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
종속성을 제거하기 위해 Controller에서 HttpServletRequest, HttpServletResponse를 사용하지 않으므로 아래와 같이 변경된다
* ModelView를 리턴하고 파라미터로 paramMap을 받는다
public interface Controller {
ModelView process(Map<String, String> paramMap);
}
컨트롤러를 구현한다면 아래와 같이 사용할 수 있다
public class TestController implements Controller {
/* ... */
@Override
public ModelView process(Map<String, String> paramMap) {
String name = paramMap.get("name");
User user = new User(name);
ModelView modelView = new ModelView("user-logical-url");
modelView.getModel().put("user", user);
return modelView;
}
}
- paramMap.get("name") : 파라미터 정보는 map에 담겨있다. map에서 필요한 요청 파라미터를 조회하면 된다
- modelView.getModel().put("user", user) : 모델은 단순한 map이므로 모델에 뷰에서 필요한 user 객체를 담고 반환한다
또한 Controller가 ModelView를 반환하므로 Front Controller는 아래와 같이 바꿔준다
@WebServlet(name = "frontControllerServlet", urlPatterns = "/front-controller/*")
public class FrontControllerServlet extends HttpServlet {
/* ... */
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
Controller controller = controllerMap.get(requestURI);
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
// paramMap
ModelView modelView = controller.process(paramMap);
String viewName = modelView.getViewName();
MyView view = viewResolver(viewName);
view.render(modelView.getModel(), request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
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;
}
크게 아래와 같이 3가지 변화가 있다
- Map<String, String> createParamMap(HttpServletRequest request)
HttpServletRequest에서 파라미터 정보를 꺼내서 Map으로 변환한다. 그리고 해당 Map( paramMap )을 컨트롤러에 전달하면서 호출한다.
- MyView viewResolver(String viewName)
컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경하고 실제 물리 경로가 있는 MyView 객체를 반환한다.
- view.render(modelView.getModel(), request, response)
종속성 제거를 위해 modelView에 뷰에 전달할 객체들을 담아두었기 때문에 MyView에 modelView 전달을 해줘야 하므로 MyView에 render를 아래와 같이 오버로딩 해준다
public class MyView {
/* ... */
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, request);
render(request, response);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
단순하고 실용적인 컨트롤러 - V4
실제 컨트톨러 인터페이스를 구현하는 개발자 입장에서 보면, 항상 ModelView 객체를 생성하고 반환해야 하는 부분은 실용성이 떨어지기 때문에 해당 부분을 ViewName만 반환하도록 수정해보자
public interface Controller {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
우선 위와 같이 반환 타입을 String으로 바꿔주고 ModelView를 반환하지 않기 때문에 Model에 대한 데이터를 설정할 Map<String, Object> model을 파라미터로 받아준다
@WebServlet(name = "frontControllerServlet", urlPatterns = "/front-controller/*")
public class FrontControllerServlet extends HttpServlet {
/* ... */
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* ... */
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
// paramMap
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
/* ... */
}
V3와 비교해보면 모델 객체를 직접 생성해서 전달해주고 viewName을 바로 사용하기 때문에 컨트롤러를 구현하는 개발자 입장에서 보면 좀 더 단순하고 실용적이다
* 프레임워크나 공통 기능이 수고로워야 사용하는 개발자가 편리해진다.
유연한 컨트롤러 - V5
다양한 타입의 컨트롤러를 지원하기 위해 어댑터 패턴을 적용할 수 있다
핸들러 어댑터
중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다. 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
핸들러
컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
- supports
어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드
- handle
어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환한다
실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환한다
이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다
HandlerAdapter의 구현은 아래와 같다
public class ControllerHandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
Controller controller = (Controller) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView modelView = new ModelView(viewName);
modelView.setModel(model);
return modelView;
}
/* ... */
}
위 코드에서 주의깊게 볼만한 부분은 Controller가 viewName을 반환하더라도 modelView를 생성해서 반환한다는 점이다. 이 부분이 어댑터 패턴의 핵심이라고 할 수 있다
Front Controller는 컨트롤러를 직접 호출하지 않고 어댑터를 사용해서 유연성을 가져간다
@WebServlet(name = "frontControllerServlet", urlPatterns = "/front-controller/*")
public class FrontControllerServlet extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServlet() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v3/members", new MemberListControllerV3());
handlerMappingMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v4/members", new MemberListControllerV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
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 modelView = adapter.handle(request, response, handler);
String viewName = modelView.getViewName();
MyView view = viewResolver(viewName);
view.render(modelView.getModel(), request, response);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if(adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter is not found. handler = " + handler);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
/* ... */
}
- initHandlerMappingMap
URI, 컨트롤러 Map 초기화, handlerMappingMap이 Map<String, Object> 타입으로 컨트롤러 타입의 제한이 없다
- initHandlerAdapters
사용할 어댑터 List 초기화
- getHandler
URI에 맞는 컨트롤러(핸들러)를 가져오는 메서드
- getHandlerAdapter
Adapter는 supports를 통해 컨트롤러를 지원하는지 알 수 있으므로 등록된 어댑터중에 현재 가져온 컨트롤러를 support 할 수 있는 어댑터를 찾아서 반환해준다
- adapter.handle(request, response, handler)
어댑터에 핸들러를 넘겨서 로직을 처리해준다
'개발 > 스프링' 카테고리의 다른 글
[Spring] 6-1. 스프링 MVC - Logging (0) | 2021.10.31 |
---|---|
[Spring] 5. 스프링 MVC - 구조 이해 (0) | 2021.10.27 |
[Spring] 3. Servlet, JSP, MVC 패턴 (0) | 2021.10.18 |
[Spring] 2. Servlet / 서블릿 (0) | 2021.10.15 |
[Spring] 1-5 자바 백엔드 웹 기술 역사 (0) | 2021.10.13 |