Front Controller Pattern 소개
Spring MVC는 Front Controller 패턴을 기반으로 확장된 형태이다.
Front Controller 패턴을 이해한다면, Spring MVC 구조와 흐름을 이해할 수 있다.
전체적인 Front Controller 패턴의 구조는 다음과 같다.
여기서, front controller는 문지기 역할을 한다.
클라이언트가 HTTP 요청을 한다면 front controller가 각각의 handler(controller)을 호출하며, view를 렌더링하여 응답하는 역할을 한다.
이로 인해, handler에서는 servlet을 사용하지 않고 요청과 응답처리를 할 수 있다.
단계별로 각 구조를 하나씩 살펴보자.
1. 헨들러 매핑, 핸들러 어뎁터 초기화
서블릿 객체가 생성될 때, 핸들러 매핑과 어뎁터가 초기화한다.
private final Map<String, Object> handlerMapping = new HashMap<>();
private final List<HandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServlet(){
initHandlerMapping();
initHandlerAdapters();
}
private void initHandlerMapping() {
handlerMapping.put("/v1/members/form", new MemberFormControllerV1());
handlerMapping.put("/v1/members/save", new MemberSaveControllerV1());
handlerMapping.put("/v1/members", new MemberListControllerV1());
handlerMapping.put("/v2/members/form", new MemberFormControllerV2());
handlerMapping.put("/v2/members/save", new MemberSaveControllerV2());
handlerMapping.put("/v2/members", new MemberListControllerV2());
}
private void initHandlerAdapters() {
handlerAdapters.add(new HandlerAdapterV1());
handlerAdapters.add(new HandlerAdapterV2());
}
2. 핸들러 매핑을 이용한 핸들러 조회
HTTP 요청이 들어오면, URI에 따른 컨트롤러를 리턴한다.
ex. 회원 리스트를 조회하는 URI가 "/members"일 경우 MemberListController을 반환한다.
// FrontControllerServlet.java
@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;
}
...
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMapping.get(requestURI);
}
3. 핸들러 어댑터 조회
각 핸들러에 맞는 어뎁터를 반환한다.
이 떄 어뎁터는 다양한 종류의 컨트롤러를 사용할 수 있게 해준다.
ex. ControllerV1은 HandlerAdapterV1을 반환하며, ControllerV2은 HandlerAdapterV2을 반환한다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
HandlerAdapter adapter = getHandlerAdapter(handler);
...
}
private HandlerAdapter getHandlerAdapter(Object handler) {
for (HandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
3. 핸들러 호출 / ModelView 반환
어뎁터는 handle 함수를 호출하며 핸들러가 내부적으로 요청과 응답을 processing 하도록 한다.
그리고 핸들러로부터 얻은 데이터를 ModelView 클래스 형태로 반환한다.
ModelView mv = adapter.handle(request, response, handler);
4. viewResolver 호출 / View 반환
modelView에서 viewName을 얻고 viewResolver을 호출하여 뷰를 얻는다.
이때, viewName은 "/WEB-INF/views/" 밑의 경로를 말한다.
ex. 뷰 파일의 경로가 "/WEB-INF/views/members.jsp"일 경우 viewName은 "members"이다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
View view = viewResolver(mv.getViewName());
...
}
private View viewResolver(String viewName) {
return new View("/WEB-INF/views/" + viewName + ".jsp");
}
5. render 호출
실제로 뷰가 렌더되는 과정이다.
view.render(mv.getModel(), request, response);
이는 내부적으로 다음 코드를 실행한다.
model.forEach(request::setAttribute); // 데이터 보관
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response); // forward
전체 구조
위에서 소개된 구조의 전체 구조는 다음과 같다.
FrontControllerServlet
@WebServlet(name = "frontControllerServlet", urlPatterns = "/*")
public class FrontControllerServlet extends HttpServlet {
private final Map<String, Object> handlerMapping = new HashMap<>();
private final List<HandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServlet(){
initHandlerMapping();
initHandlerAdapters();
}
private void initHandlerMapping() {
handlerMapping.put("/v1/members/form", new MemberFormControllerV1());
handlerMapping.put("/v1/members/save", new MemberSaveControllerV1());
handlerMapping.put("/v1/members", new MemberListControllerV1());
handlerMapping.put("/v2/members/form", new MemberFormControllerV2());
handlerMapping.put("/v2/members/save", new MemberSaveControllerV2());
handlerMapping.put("/v2/members", new MemberListControllerV2());
}
private void initHandlerAdapters() {
handlerAdapters.add(new HandlerAdapterV1());
handlerAdapters.add(new HandlerAdapterV2());
}
@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;
}
HandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
View view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMapping.get(requestURI);
}
private HandlerAdapter getHandlerAdapter(Object handler) {
for (HandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private View viewResolver(String viewName) {
return new View("/WEB-INF/views/" + viewName + ".jsp");
}
}
HandlerAdapter
HandlerAdapter Interface
public interface HandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
HandlerAdapter (version 1)
public class HandlerAdapterV1 implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV1);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
ControllerV1 controller = (ControllerV1) handler;
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;
}
}
HandlerAdapter (version 2)
public class HandlerAdapterV2 implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV2);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
ControllerV1 controller = (ControllerV1) handler;
Map<String, String> paramMap = createParamMap(request);
Map<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;
}
}
Controller (version 1)
Controller Interface
public interface ControllerV1 {
ModelView process(Map<String, String> paramMap);
}
MemberFormController
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
MemberSaveController
public class MemberSaveControllerV1 implements ControllerV1 {
private final 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;
}
}
MemberListController
public class MemberListControllerV1 implements ControllerV1 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("show-members");
mv.getModel().put("members", members);
return mv;
}
}
Controller (version 2)
Controller Interface
public interface ControllerV2 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
MemberFormController
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
MemberSaveController
public class MemberSaveControllerV2 implements ControllerV2 {
private final 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";
}
}
MemberListController
public class MemberListControllerV2 implements ControllerV2 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
ModelView
@Setter
@Getter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
View
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);
}
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(request::setAttribute);
}
}
출처: 김영한, 「스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술」, 인프런
'Web > Spring Boot' 카테고리의 다른 글
스프링 MVC 요청과 응답 (0) | 2024.01.05 |
---|---|
스프링 MVC 구조 (0) | 2024.01.05 |
서블릿, JSP을 사용한 MVC 패턴 (0) | 2024.01.04 |
서블릿 이해하기 (0) | 2024.01.03 |
스프링부트 시작하기 (0) | 2024.01.03 |