0. 개요
- 이전 포스팅에서 전체적인 Front Controller의 흐름에 대해서 알아보았다.
- 그러나 하나의 디자인 패턴이라 정의하기에는 구조적으로 불편한 부분이 많다.
- 이 부분을 어떻게 개선하는지, 최종적으로 Front Controller가 어떻게 작동하는지 알아보자.
* 본 포스팅은 Front Controller 시리즈의 내용입니다. 이전 포스팅을 읽는 것을 권장합니다.
1. ModelView 대신 view 이름(String)을 반환
- 이전 포스팅에서 각 Controller는 ModelView 객체를 생성하고 반환했다. 그러나 이는 SRP에 위배된다.
- Controller가 비즈니스 로직의 수행과 ModelView의 생성까지 책임지고 있기 때문이다.
- 그러므로 ModelView를 더 이상 사용하지 않고, JSP 이름(String)을 그대로 반환하도록 한다.
- 그렇다면 기존에 ModelView의 Model 역할은 어떻게 처리되는 것일까?
- 단순하게 Front Controller에서 paraMap과 model을 생성하여 Controller에게 넘기는 방식을 사용한다.
→ paraMap: 요청으로부터 넘어온 URL parameter를 저장하기 위한 자료구조
→ model: Controller에서 view에게 전달할 데이터 객체를 저장하기 위한 자료구조
- Controller는 Front Controller로부터 넘어온 model을 사용하여 view에게 전달할 데이터 객체를 저장한다.
- Controller가 model에 저장한 내용은 그대로 담겨있게 된다.
- 아래의 코드를 보면, 이해가 더욱 쉬울 것이다.
public interface Controller {
/**
*
* @param paraMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
@WebServlet(name = "frontController", urlPatterns = "/front-controller/*" )
public class FrontController 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);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
}
- ModelView를 제거한 구조는 다음과 같다.
2. 유연하지 않은 Controller
- Front Controller를 제외한 각 Controller는 하나의 인터페이스로부터 파생된 구현체다.
- 그러므로 각 Controller는 동일한 구조를 가지며, 동일한 비즈니스 로직 메서드의 이름을 갖는다.
- 그러나 개발을 하다 보면 모든 Controller가 동일한 구조를 가질 수는 없다.
ex) Controller에서 다른 형태의 값이 반환되는 경우
- 그러므로 필요에 따라 구조가 다른 Controller를 사용할 수 있어야 하며,
- Front Controller가 이를 처리하는데 문제가 없어야 한다.
- 이와 같은 이유로 유연한 Controller가 필요한 것이다.
- 현재까지의 Front Controller의 구조는 유연하지 않은 Controller를 사용한다.
- 어떻게 해야 서로 다른 구조를 가진 Controller를 유연하게 사용할 수 있을까?
a) Adapter 패턴
- Adapter는 이름 그대로 변환기의 역할을 한다.
- 마치 110V의 전기 콘센트에서 220V의 제품을 사용하기 위해 필요한 어뎁터처럼 말이다.
- Adapter 패턴을 사용하면 Front Controller에서 다양한 구조의 Controller를 처리할 수 있다.
b) Adapter 패턴의 구조
- Front Controller에 Adapter 패턴을 적용하면 다음과 같은 구조를 형성한다.
- 우선 각 Adapter의 역할에 대해서 알아보자.
c) HandlerMapping
- 기존의 Front Controller에서 URL을 이용하여 각 Controller를 매핑하였다.
- 이와 동일한 역할을 하는 것이 HandlerMapping이다.
- HandlerMapping은 Client의 요청 URL과 매핑되어 있는 Controller를 선택한다.
- 여기서 선택된 Controller를 Handler라고 부른다.
d) HandlerAdapter
- HandlerAdapter는 선택된 Handler의 비즈니스 로직을 실행하는 역할이다.
- 선택된 Handler(= Controller)에 따라서, 그 구조에 맞게 처리할 수 있어야 한다.
- 그러므로 HandlerAdapter는 Handler의 구조에 맞는 처리방법(= 비즈니스 로직 메서드)을 제공한다.
- 즉, Handler가 보유한 비즈니스 로직 처리 메서드를 호출하는 역할이다.
3. Adapter 패턴의 동작원리
- Adapter를 사용하는 Front Controller가 어떻게 동작하는지 하나씩 알아보자.
- 아래 코드에서는 서로 다른 구조를 갖는 Controller A와 Controller B가 사용되었다.
- Controller A는 ModelView를 반환하는 구조를 가졌다.
- Controller B는 View 이름을 그대로 반환하는 구조를 가졌다.
- 아래의 코드를 읽기 전에 다음을 명심하도록 하자.
- Controller A는 ModelView 객체를 반환하고, Controller B는 view 이름(String)을 직접 반환한다.
- 서로 다른 구조를 일괄 처리할 수 있도록, HandlerAdapter는 공통적으로 ModelView를 사용하도록 설계하였다.
- 아래의 전체 코드는 Adapter 패턴을 이용하여 Controller를 처리하는 과정을 보여준다.
- Client의 요청으로 Controller A의 구현체를 처리하는 과정이라고 생각하고 가볍게 훑어보자.
@WebServlet(name = "frontController", urlPatterns = "/front-controller/*")
public class FrontController extends HttpServlet {
// controller를 받아오는 Map인데, 현재는 모든 종류의 controller를 허용해야한다.
// 그러므로 Map의 value를 Object 타입으로 만든다. (Object가 최상위 클래스이기 때문)
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontController() {
initHandlerMappingMap(); // url에 따라 controller 객체를 생성하는 메소드
initHandlerAdapter(); // 받아온 controller의 버전에 맞게 기능을 수행하는 객체를 생성하는 메소드
}
private void initHandlerMappingMap() {
// 컨트롤러 A
handlerMappingMap.put("/front-controller/a/members/new-form", new MemberFormControllerA());
handlerMappingMap.put("/front-controller/a/members/save", new MemberSaveControllerA());
handlerMappingMap.put("/front-controller/a/members", new MemberListControllerA());
// 컨트롤러 B
handlerMappingMap.put("/front-controller/b/members/new-form", new MemberFormControllerB());
handlerMappingMap.put("/front-controller/b/members/save", new MemberSaveControllerB());
handlerMappingMap.put("/front-controller/b/members", new MemberListControllerB());
}
private void initHandlerAdapter() {
handlerAdapters.add(new HandlerAdapterControllerA());
handlerAdapters.add(new HandlerAdapterControllerB());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.url에 따른 controller 객체를 받아온다. (handler == controller 객체)
Object handler = getHandler(request);
if(handler == null) { // 예외처리 - 매핑에 없는 url이 요청됐을 때
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 2. handler(controller 객체)의 버전에 맞는 기능을 수행하는 handlerAdapter를 반환한다.
HandlerAdapter adapter = getHandlerAdapter(handler);
// 3. adapter의 기능을 호출 / handle(): handler의 버전과 동일한 컨트롤러의 기능을 수행)
// => handle()에서 paraMap을 생성하여 url 파라미터를 가져오고, 이 정보를 해당 컨트롤러에게 넘김
ModelView mv = adapter.handle(request, response, handler);
// 4. Controller A - ModelView로부터 저장된 JSP 이름을 가져온다.
String viewName = mv.getViewName();
// 5. Controller A - 반환받은 JSP 이름을 기반으로 JSP 파일의 절대경로를 생성하는 과정(아래쪽에 메소드 있음)
// 생성된 JSP 파일의 절대경로를 View 객체에 저장함
MyView view = viewResolver(viewName);
// 6. Controller A - view 객체에 저장된 절대경로를 이용하여 JSP를 forwarding 한다.
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
// 1. request 객체의 요청 url를 가져온다.
String requestURI = request.getRequestURI();
// 2. 요청 url과 매칭하는 Controller 객체를 handlerMappingMap에서 가져온다.
return handlerMappingMap.get(requestURI);
}
private HandlerAdapter getHandlerAdapter(Object handler) { // handler = controller 객체
for (HandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) { // supports(): handler와 동일한 버전의 adapter인지 판별한다.
return adapter; // 찾고있는 버전의 adapter가 맞다면 이를 반환
}
}
// 오류처리 - handler와 일치하는 버전의 adapter가 없는 경우
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
a) 동작원리
1. Request
- Client로부터 Front Controller에 요청이 들어온다.
2. Handler 조회
- 요청 객체의 request URL을 이용하여 HandlerMapping으로부터 Controller(= handler)를 조회한다.
Object handler = getHandler(request);
private Object getHandler(HttpServletRequest request) {
// 1. request 객체의 요청 url를 가져온다.
String requestURI = request.getRequestURI();
// 2. 요청 url과 매칭하는 Controller 객체를 handlerMappingMap에서 가져온다.
return handlerMappingMap.get(requestURI);
}
3. HandlerAdapter 조회
- 조회된 Controller(= handler)를 처리할 수 있는 HandlerAdapter를 HandlerAdapter 목록으로부터 조회한다.
HandlerAdapter adapter = getHandlerAdapter(handler);
private HandlerAdapter getHandlerAdapter(Object handler) { // handler = controller 객체
for (HandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) { // supports(): handler와 동일한 버전의 adapter인지 판별한다.
return adapter; // 찾고있는 버전의 adapter가 맞다면 이를 반환
}
}
// 오류처리 - handler와 일치하는 버전의 adapter가 없는 경우
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
}
- getHandlerAdapter() 메서드를 살펴보자.
- handlerAdapters라는 자료구조에는 Controller A와 Controller B를 처리할 수 있는 HandlerAdapter가 모두 들어있다.
- 반복문을 돌려 handlerAdapters로부터 HandlerAdapter를 하나씩 꺼내어 검증과정을 거친다.
- 위의 코드에서는 이 검증을 위해 supports() 메서드를 사용한다.
public class HandlerAdapterControllerA implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
// controller A의 객체가 넘어오면 true를 반환한다.
return (handler instanceof ControllerA);
}
public class HandlerAdapterControllerB implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
// controller B의 객체가 넘어오면 true를 반환한다.
return (handler instanceof ControllerB);
}
- HandlerAdapter는 조회된 handler를 본인이 처리할 수 있는지를 판단하는 supports() 메서드를 갖고 있다.
- 위의 supports() 메서드를 살펴보면, 해당 handler가 특정 Controller 인터페이스의 구현체인지 검사한다.
- 이 검사를 통해, HandlerAdapter가 해당 handler를 처리할 수 있는지 판단한다.
4. 비즈니스 로직 실행
- 해당 handler를 처리할 수 있는 HandlerAdapter를 찾았다면, 해당 HandlerAdapter를 통해 비즈니스 로직을 실행한다.
ModelView mv = adapter.handle(request, response, handler);
public interface HandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
- 비즈니스 로직을 실행하기 위해서 adapter의 handler이라는 메서드를 사용한다.
- 이는 모든 HandlerAdapter 구현체가 보유하고 있는 메서드로, HandlerAdapter 인터페이스에 의해 정해진 이름이다.
- 모든 HandlerAdapter의 비즈니스 로직을 처리하는 메서드가 동일한 이름을 가질 수 있도록 한 것이다.
5. View 처리
- 이후의 과정은 ModelView를 사용했던 방식과 동일한 처리과정을 거친다.
- Controller A는 ModelView를 반환하므로, ModelView로부터 view 이름을 꺼내와서 render()를 실행한다.
// 1. adapter에서 비즈니스 로직을 수행하면 ModelView 객체를 반환한다.
ModelView mv = adapter.handle(request, response, handler);
// 2. 반환된 ModelView의 객체로부터 view 이름을 꺼내온다.
String viewName = mv.getViewName();
// 3. viewResolver 메서드를 통해 view의 절대경로를 생성하고 반환 받는다.
MyView view = viewResolver(viewName);
// 4. 반환된 view 객체에게 model과 request, response 객체를 전달한다.
view.render(mv.getModel(), request, response);
3. 결론
- 여기까지 Adapter 패턴을 이용한 유연한 Controller에 대해 알아보았다.
- Adapter 패턴까지 온전히 이해했다면, Spring MVC의 구조를 이해한 것과 마찬가지다.
- Spring MVC의 구조 또한 Adapter 패턴을 사용한 Front Controller를 기반하기 때문이다.
- 다음 포스팅에서는 Spring MVC의 구조에 대해서 본격적으로 알아보려고 한다.
- Spring MVC는 Front Controller에서 확장된 형태이기 때문에 각각의 특징을 이해해야 한다.
- 더불어 Spring MVC가 사용하는 명칭이 조금씩 다르므로, 이에 대해서도 알아보도록 하자.
'Back-end > Spring MVC 개념' 카테고리의 다른 글
12. Annotation 기반의 Spring MVC (0) | 2022.03.09 |
---|---|
11. Spring MVC의 특징 (0) | 2022.03.08 |
9. Front Controller (1) (0) | 2022.03.04 |
8. Forward와 Redirect (0) | 2022.03.03 |
7. MVC 1의 동작원리와 한계점 (0) | 2022.03.02 |
댓글