출처 : https://inf.run/c5AZ
이번 강의 분량은 상대적으로 분량이 많고 한 번 흐름을 놓치면 이해하기 어렵다. 잘 따라가보자.
*) 프론트 컨트롤러 패턴
각각의 컨트롤러에 공통된 부분들이 있다 가정해보자.
클라이언트는 호출할 때 각각의 컨트롤러를 호출하게 된다.
하지만 공통적인 부분을 묶어서 따로 컨트롤러를 만들어주고 해당 부분을 거치고 고유 역할을 하는 컨트롤러로 가게 한다면?
공통 처리가 가능하다.
프론트 컨트롤러에만 서블릿을 넣어주고 나머지 컨트롤러는 서블릿을 사용할 필요가 없어지는 것이다.
즉, 스프링 웹 MVC에서의 핵심은 바로 FrontController이다.
이 포스트에선 FrontController를 단계적으로 도입하면서 어떻게 발전해나가는지를 이해하며 필요성을 느껴보자.
v1...v5까지는 그저 지은 가명에 불과하다.
v1 구조)
public interface ControllerV1{
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
각각의 컨트롤러들은 해당 인터페이스를 구현하면 된다.
프론트 컨트롤러는 이 인터페이스를 호출함으로써 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
우리는 예시로 회원 등록 컨트롤러, 저장 컨트롤러, 목록 컨트롤러 이렇게 3개를 구현해보려한다.
등록 컨트롤러)
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 MemberSaveControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.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);
}
}
이제 FrontController을 구현해보자.
FrontControllerV1)
@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 = "/front-controller/v1/*"를 통해 /v1 하위 모든 요청을 해당 프론트 컨트롤러가 받아들인다.
그 이후 requestURI를 조회해서 실제 호출할 컨트롤러를 Map에서 찾는다.
그 이후 process(request,response)를 호출해 해당 컨트롤러를 실행한다.
View 분리 - v2)
위의 세 구현 컨트롤러들에서 중복이 있는 부분이 있다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
이 부분을 따로 분리해서 이를 처리하는 객체를 만들어보자.
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);
}
}
ControllerV2
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
return 값이 MyView 객체로 바뀌었다
그러면 이제 해당 구현 컨트롤러들은 MyView 객체를 반환시켜주면 된다. 어떻게 바뀌었는지 비교해보자.
등록 컨트롤러)
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
이제 해당 컨트롤러들은 복잡하게 dispatcher.foward()등을 호출 안해도 된다.
단순히 MyView 객체를 생성하고, 거기에 뷰 이름만 넣어서 반환해주면 된다.
FrontControllerV2)
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/frontcontroller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
private Map<String, ControllerV2> controllerMap = new HashMap<>();
public FrontControllerServletV2() {
controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView view = controller.process(request, response);
view.render(request, response);
}
}
Model 추가 - V3)
컨트롤러입장에서 HttpServletRequest, HttpServletResponse가 필요할까?
해당 파라미터 정보들을 Map으로 넘겨주면 해당 구조에서 컨트롤러가 몰라도 작동할 수 있다.
즉, 별도의 Model 객체를 만들어서 넣어주고, model로 넘겨주면 된다.
또, 지정하는 뷰 이름에 중복이 있다.
뷰의 논리적인 이름만 반환하게 하고, 중첩되는 실제 물리 위치 이름은 프론트 컨트롤러에서 처리하도록 하자.
서블릿의 종속성을 제거(HttpServletRequest)하기 위해 Model을 직접 만들고, View 이름을 전달하는 객체를 만들어보자
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객체로 구현했다.
ControllerV3)
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해주면 된다.
응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 된다.
등록 폼)
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
FrontControllerServletV3)
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/frontcontroller/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 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;
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
v3의 불편한 점은 개발자가 model을 계속 생성해줘야한다는 것이다.
이를 개선한 것이 v4다. 모델을 반환받는 것이 아닌, ViewName만 받아서 ViewResolver을 호출하려한다.
v4)
public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
FrontControllerServletV4
@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);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
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;
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
Map<String,Object> model = new HashMap<>(); 을 통해
모델 객체를 프론트 컨트롤러에 생성해서 넘겨준다.
유연한 컨트롤러 - V5)
만약 어떤 개발자가 ControllerV3 방식으로 개발하고 싶고,
다른 개발자는 ControllerV4 방식으로 개발하고 싶다면 어떻게 해야할까?
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
지금까지 우리가 만든 FrontController는 한 가지 방식의 컨트롤러 인터페이스만 활용할 수 있었다.
ControllerV3과 ControllerV4는 완전히 다른 인터페이스니까.
이럴 때 사용하는 것이 Adapter이다.
어댑터 패턴을 활용해 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자.
핸들러 어댑터): 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
핸들러): 컨트롤러의 개념을 더 확장하기 위해 변경했다. 이제 어댑터가 있기 때문에 어떠한 것이든
해당하는 종류의 어댑터만 있으면 다 처리할 수 있다.
MyHandlerAdapter
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
supports함수는 어댑터가 해당 handler를 처리할 수 있는지 판단하는 메소드이다.
handle함수의 경우 실제 컨트롤러를 호출하고 객체로 ModelView를 반환한다.
CotrollerV3을 지원하는 어댑터를 구현해봤다.
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
ControllerV3 controller = (ControllerV3) 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;
}
}
먼저 supports로 handler안에 ControllerV3가 있는지 판단한다
그 이후 Object 객체이므로 해당 형식으로 캐스팅해주었다.
supports를 통해 만약 없으면 종료되므로 예외처리는 굳이 해줄필요가 없다.
이제 해당 request들을 가진 modelview를 반환해주었다.
FrontControllerServletV5
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/frontcontroller/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() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
@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);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
private final Map<String,Object> handlerMappingMap = new HashMap<>();
매핑 정보의 값이 ControllerV3, ControllerV4와 같은 인터페이스에서 아무 값이나 받을 수 있는 Object로 변경되었다.
해당 맵에서 URL에 매핑된 컨트롤러 객체를 찾아서 반환한다.
간단히 지금까지 한 걸 정리해보자.
v1: 프론트 컨트롤러를 도입. 기존 구조를 최대한 유지하면서 프론트 컨트롤러를 도입
v2: View 분류. 단순 반복 되는 뷰 로직 분리
v3: Model 추가. 서블릿 종속성 제거, 뷰 이름 중복 제거
v4: 단순하고 실용적인 컨트롤러. v3와 거의 비슷 구현 입장에서
ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공
v5: 유연한 컨트롤러 어댑터 도입 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계
'스프링 강의 필기 > 스프링 MVC 1편' 카테고리의 다른 글
7) 스프링 MVC - 웹 페이지 만들기 (0) | 2023.01.27 |
---|---|
6) 스프링 mvc 기본 기능 (0) | 2022.08.03 |
5) 스프링 MVC - 구조 이해 (0) | 2022.07.30 |
3) 서블릿, JSP, MVC 패턴 (0) | 2022.07.27 |
1) 웹 애플리케이션 이해 (0) | 2022.07.22 |