IT recording...
[스프링 MVC1] 4. MVC 프론트 컨트롤러 패턴 본문
원문 링크
https://adorable-aspen-d23.notion.site/MVC1-4-MVC-c091e2d264854b0fbc735b0ae2ab3e96
김영한님의 [스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 강의를 듣고 작성한 글입니다.
Front Controller
- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받는다.
- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출한다.
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
- 공통 처리가 가능하다.
1. 프론트 컨트롤러 V1
@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 {
String requestURI = request.getRequestURI(); // /front-controller/v1/members/new-form 이런 부분 얻어옴
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response); //controller 호출
}
}
- controllerMap 을 이용하여 url주소와 해당하는 컨트롤러를 매핑해놓는다.
- 이후 요청이 들어오면 controllerMap을 뒤져 알맞은 컨트롤러를 찾는다.
- 해당 컨트롤러의 process를 실행한다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
public class MemberSaveControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//argument
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
//save
Member member = new Member(username,age);
memberRepository.save(member);
//model에 데이터 보관하기
request.setAttribute("member",member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
2. 프론트 컨트롤러 - V2
: 모든 컨트롤러에 뷰로 이동하는 부분의 중복이 존재하므로 이를 없애보자
→ 뷰를 처리하는 객체를 생성한다.
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 interface ControllerV2 {
**MyView** process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
- 각 컨트롤러에서 MyView를 반환한다.
public class MemberSaveControllerV2 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//save
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username,age);
memberRepository.save(member);
//model에 데이터 보관하기
request.setAttribute("member",member);
**return new MyView("/WEB-INF/views/save-result.jsp")**;
}
}
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/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(); // /front-controller/v2/members/new-form 이런 부분 얻어옴
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
**MyView view** = controller.process(request, response);//controller 호출
**view.render(request,response);**
}
}
⇒ MyView객체의 render() 호출 부분을 일관되게 처리할 수 있다.
⇒ 각각의 컨트롤러는 MyView객체를 생성만 해서 반환하면 된다.
3. 프론트 컨트롤러 - V3
- 서블릿 종속성 제거→ 요청 파라미터 정보는 자바의 Map을 이용해서 넘기고, request 대신 별도의 Model을 이용한다.
- : 모든 컨트롤러가 HttpServletRequest, HttpServletResponse를 필요로 하지는 않는다. 없애보자!
- 뷰 이름 중복 제거→ viewResolver 사용
- : prefix, suffix를 제거하고 뷰의 논리적 이름만 넘겨줘도 되게 하자!
@Getter
@Setter
public class ModelView {
**private String viewName;
private Map<String,Object> model = new HashMap<>();**
public ModelView(String viewName) {
this.viewName = viewName;
}
}
public interface ControllerV3 {
**ModelView** process(Map<String,String> paramMap); //request,response를 삭제함으로써 servlet에 종속적이지 않게 됨
}
public class MemberSaveControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(**Map<String, String> paramMap**) {
//param 받아오기
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;
}
}
@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(); // /front-controller/V3/members/new-form 이런 부분 얻어옴
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
**//파라미터 뽑기
Map<String, String> paramMap = createParamMap(request);**
//ModelView에 뷰의 논리이름과 + jsp에서 필요로하는 데이터 넣어옴
**ModelView** mv = controller.process(paramMap);//controller 호출
//viewResolver호출해서 논리 이름 -> 실제 주소로 변경하기
String viewName = mv.getViewName();
MyView myView = **viewResolver(viewName);**
//렌더링
myView.render(mv.getModel(),request,response); //model 정보도 함께 전달
}
private MyView viewResolver(String viewName) {
MyView myView = new MyView(**"/WEB-INF/views/" + viewName + ".jsp"**); //중복되는 부분 해결
return myView;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
//paramMap
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
⇒ 컨트롤러에서 서블릿 종속성을 제거했으므로 구현이 매우 단순해지고, 테스트 코드 작성이 쉬워진다.
→ HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해주면 된다.
→ 응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView객체를 반환한다.
4. 단순하고 실용적인 컨트롤러 - V4
: 개발자 입장에서 항상 ModelView객체를 생성하고 반환해야 하기 때문에 개발자들이 편리하게 사용할 수 있는 버전을 만들어보자!
public interface ControllerV4 {
/**
*
* @param paramMap
* @param model
* @return viewName
*/
String process(**Map<String,String> paramMap, Map<String,Object> model**);
}
public class MemberSaveControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
//param 받아오기
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); //모델은 put으로만, 따로 전달X
**return "save-result";**
}
}
@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(); // /front-controller/v4/members/new-form 이런 부분 얻어옴
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<>(); //추가
//controller 호출, viewName 리턴해옴, model은 파라미터로 넘어옴
**String viewName** = controller.process(paramMap,model);//controller 호출
//viewResolver호출해서 논리 이름 -> 실제 주소로 변경하기
MyView myView = **viewResolver**(viewName);
//렌더링
myView.render(model,request,response); //model 정보도 함께 전달
}
private MyView viewResolver(String viewName) {
MyView myView = new MyView("/WEB-INF/views/" + viewName + ".jsp"); //중복되는 부분 해결
return myView;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
//paramMap
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
⇒ 각 컨트롤러에서 ModelView를 생성하는 것이 아니라 뷰의 논리 이름만 반환하면 되게 돼서 단순하고 실용적이게 되었다. (By. 기존 구조에서 모델을 파라미터로 넘기고 뷰의 논리 이름 반환 아이디어)
5. 유연한 컨트롤러 - V5
: 어떤 개발자는 ControllerV3를 사용하고 싶고, 어떤 개발자는 ControllerV5를 사용하고 싶다면 어떻게 해야할까?
public interface ControllerV3 {
**ModelView** process(**Map<String, String> paramMap**);
}
public interface ControllerV4 {
**String** process(**Map<String, String> paramMap, Map<String, Object> model**);
}
⇒ 어댑터 패턴 사용!
: 다른 인터페이스를 사용하는 컨트롤러들을 한 프론트 컨트롤러에서 사용할 수 있도록 호환이 가능하게 변경해보자.
- 핸들러 어댑터 : 어댑터 역할을 해주어서 다양한 종류의 컨트롤러를 호출할 수 있다.
- 핸들러 : (구 컨트롤러) 어댑터가 존재하기 때문에 꼭 컨트롤러의 개념이 아니어도 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
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) throws ServletException, IOException {
**ControllerV3 controller** = (ControllerV3) handler;
//V3방식
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
//paramMap
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
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);
Map<String,Object> model = new HashMap<>(); //추가
//V4방식
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
//paramMap
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
private MyView viewResolver(String viewName) {
MyView myView = new MyView("/WEB-INF/views/" + viewName + ".jsp"); //중복되는 부분 해결
return myView;
}
}
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String,Object> handlerMappingMap = new HashMap<>(); //어떤 버전의 컨트롤러도 다 object에 들어갈 수 있다.
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());
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 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);**
//어댑터에서 handle 호출
**ModelView mv = adapter.handle**(request, response, handler);
//공통 부분
//viewResolver호출해서 논리 이름 -> 실제 주소로 변경하기
String viewName = mv.getViewName();
MyView myView = viewResolver(viewName);
//렌더링
myView.render(mv.getModel(),request,response); //model 정보도 함께 전달
}
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(); // /front-controller/V3/members/new-form 이런 부분 얻어옴
Object handler = handlerMappingMap.get(requestURI);
return handler;
}
private MyView viewResolver(String viewName) {
MyView myView = new MyView("/WEB-INF/views/" + viewName + ".jsp"); //중복되는 부분 해결
return myView;
}
}
- handlerMappingMap에 해당하는 url과 해당하는 controller(handler)를 매핑해놓는다.
- 핸들러를 찾았으면 그 핸들러가 우리가 제공하는 adapter에 적용되는 핸들러인지 검사한다.
- 어댑터의 handle을 호출한다.
- 그 handle에서는 해당하는 핸들러의 모양에 맞춰서 process함수를 호출한다.
- Adapter가 제공한 데이터를 받은 FrontController는 이후 공통적인 부분을 수행한다.
6. 스프링 MVC
: 스프링 MVC는 앞에서 우리가 구축한 프레임워크와 매우 유사하게 설계되어 있다.
다음 시간에는 이 프레임워크를 스프링이 애노테이션을 어떻게 기가막히게 써서 개발하기 편하게 했는지 살펴보자.
'Spring' 카테고리의 다른 글
[스프링 MVC1] 6. MVC 기본 기능 (0) | 2022.01.15 |
---|---|
[스프링 MVC1] 5. MVC 패턴 (0) | 2022.01.14 |
[스프링 MVC1] 3. 서블릿,JSP,MVC 패턴 (0) | 2022.01.14 |
[스프링 MVC1] 2. 서블릿 (0) | 2022.01.14 |
[스프링 MVC1] 1. 웹 애플리케이션 이해 (0) | 2022.01.14 |