스프링 인프런 김영한
MVC 프레임워크 만들기
26. 프론트 컨트롤러 패턴 소개
직접MVC 프레임워크 만들기를 만들어볼 것이다.
반복적인 요소 등을 개선할 수 있다.
이걸 도입하기 위해서는 프론트 컨트롤러를 알아야한다.
이걸 도입해서 직접만들어볼 것이다. 단계적으로 업그레이드 할 것이다.
이러면 스프링과 유사한 구조가 된다.
단계별로 왜 사용하고 왜 도입하는지를 알게 된다.
직접 만들어보면 개발자로서의 능력도 업그레이드 될 것이다
26.1 FrontController 패턴 특징
프론트 컨트롤러 도입전에는 클라이언트가 공통로직을깔고 컨트롤 로직을 깔아야한다.
공통을 각각 모든 클라이언트에 만들어야한다.
프론트 컨트롤러가 잇다면 공통의 관심사를 두고 각각 필요한 로직을 각각처리하게 하면된다.
프론트 컨트롤러도 서블릿이다.
프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받는다.
프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출한다.
공통 처리 가능하게 된다.
프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
스프링 웹 MVC와 프론트 컨트롤러
스프링 웹 MVC의 핵심도 바로 FrontController이다.
스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있다.
27. 프론트 컨트롤러 도입 - v1
프론트 컨트롤러를 단계적으로 도입해보자.
이번 목표는 기존 코드를 최대한 유지하면서 프론트 컨트롤러를 도입하는 것이다.
먼저 구조를 맞추어두고 점진적으로 리펙터링 해보자.
클라이언트가 HTTP요청을하면 front에서받는다 이 매핑정보를 가지고 컨트롤러를 호출한다.
컨트롤러에서 jsp로 포워드한다. html을 응답한다.
27.1 프론트 컨트롤러
다형성을 활용하기 위해 컨트롤러를 인터페이스로 만들 것이다.
왜 인터페이스로 만드나?
서블릿과 비슷한 모양의 컨트롤러 인터페이스이다.
각 컨트롤러들은 이 인터페이스를 구현하게 될 것이다.
프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}27.2 멤버 등록 폼 구현 컨트롤러
같은 로직을 사용하고 같은 view로 포워드를 할 것이다.
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);
}
}27.3 멤버 저장 컨트롤러
항상 프로젝트 구조를 바꿀때는 단계적으로 하는게 좋다.
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);
}
}27.4 회원멤버 컨트롤러
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);
}
}27.5 프론트 컨트롤러
프론트 컨트롤러는 HttpServlet를 상속받아서 서블릿 자체가된다.
urlPatterns을 /front-controller/v1/*를 지정하면 어떤 컨트롤러를 호출하더라도 이것을 만나서 넣어주게 할 수있다.
요청 링크에 따라서 다른 컨트롤러 객체들을 생성해서 만들어준다.
생성자에 이것을 만들면 이 프론트 컨트롤러가 불러올때 매핑이 되게 된다.
request.getRequestURI()메소드로 요청된 uri부분을 받을 수있다.
이러면 map에서 꺼내서 사용할 수 있게 되는 것이다.
없으면 404응답을 주게 한다. HttpServletResponse객체에 상수로 정의되어 있다.
있다면 들어온 요청과 응답을 가지고 컨트롤러의 process함수를 실행시키게 된다.
@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);
}
}만약 http://localhost:8080/front-controller/v1/members의 경로로 요쳥이되면
requestURI에 front-controller/v1/members이 값이 들어가게 된다.
이 키를 가지고 Map<String, ControllerV1> controllerMap에 들어있는 맵을 꺼내게 된다.
각 컨트롤러들은 부모가 ControllerV1이기 때문에 구현클래스들을 꺼내서 사용할 수 있게 되는 것이다.
process메소드는 각 컨트롤러의 비즈니스 로직에 의해 처리되고 view를 포워드하게 되는 것이다.
이것이 프론트 컨트롤러이다.
그런데 이전보다 더 복잡해보이게 된다. 그런데 단계를 나아가게 되면 더깔끔한 코드가 되게 된다.
인터페이스가 중요하다 인터페이스를 각각 구현해서 그것을 불러와서 사용만 하면되는 것이다.
28. View 분리 - v2
모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고 깔끔하지 않다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);이 부분을 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만들자.
V2 구조
컨트롤러에서 과거에는view로 직접 포워드햇는데 myview라는 객체를 만들어서
프론트 컨트롤러에게 주고 이것으로 view를 호출하게 될것이다.
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);
}
}28.1 컨트롤러 인터페이스
기존에는 void를 반환햇는데 MyView를 반환하게 해준다.
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}28.2 구현 클래스
구현클래스에서는 viewpath로 포워드햇던것을 그대로 MyView객체의 생성자에 넣어서 반환해준다.
public class MemberFormControllerV1 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}public class MemberListControllerV1 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
return new MyView("/WEB-INF/views/members.jsp");
}
}public class MemberSaveControllerV2 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView 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);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}28.3 프론트 컨트롤러
@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();
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);
}
}ControllerV2의 반환 타입이 MyView 이므로 프론트 컨트롤러는 컨트롤러의 호출 결과로 MyView 를 반환한다.
그리고 view.render() 를 호출하면 forward 로직을 수행해서 JSP가 실행된다.
MyView.render()
public void render(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}프론트 컨트롤러의 도입으로 MyView 객체의 render() 를 호출하는 부분을 모두 일관되게 처리할 수 있다.
각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 된다.
화면을 렌더링하기 위한 view라는 것을 만든것이다.
29. Model 추가 - v3
서블릿 종속성 제거할것이다.
컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 꼭 필요한가 생각해보면 필요없다.
요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.
그리고 request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다.
우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경할수있게 된다.
이렇게 하면 구현 코드도 매우 단순해지고 테스트 코드 작성이 쉽다.
뷰 이름 중복 제거
컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
prefix와 .jsp가 같게 들어간다.
컨트롤러는 뷰의 논리 이름을 반환하고(new-form)
실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화할 것이다.
이렇게 해두면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.
/WEB-INF/views/new-form.jsp -> new-form
/WEB-INF/views/save-result.jsp -> save-result
/WEB-INF/views/members.jsp -> member컨트롤러는 ModelView를 반환하고 프론트컨트롤러가 viewResolver에게 호출해서
MyView를 반환하게 한다.
29.1 ModelView
지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다.
그리고 Model도 request.setAttribute() 를 통해 데이터를 저장하고 뷰에 전달했다.
서블릿의 종속성을 제거하기 위해 Model을 직접 만들고
추가로 View 이름까지 전달하는 객체를 만들것이다.
이번 버전에서는 컨트롤러에서 HttpServletRequest를 사용할 수 없다.
따라서 직접 request.setAttribute() 를 호출할 수 도 없다.
또한 Model이 별도로 필요하다.
참고로 ModelView 객체는 다른 버전에서도 사용하므로 패키지를 frontcontroller 에 둔다.
@Getter
@Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}29.2 컨트롤러 인터페이스
ModelView를 반환하고 파라미터로 string string을 받는다.
단순하게 map을 파라미터로 받는다. 서블릿 종속에서 벗어나고 프레임워크에만 종속되는 것이다.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}29.3 구현클래스
구현클래스는 논리적인 view이름만 반환한다.
파라미터는 paramMap에서 단순하게 키값으로 받게만 처리한다.
public class MemberFormControllerV3 implements ControllerV3{
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}public class MemberSaveControllerV3 implements ControllerV3 {
private 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;
}
}public class MemberListControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}29.4 프론트 컨트롤러
mv.getModel(),
프론트컨트롤러에서는 파라미터를 꺼내서 paramMap에 담아서 컨트롤러들에게 보내줘야한다.
ModelView는 논리이름을 진짜 이름으로 바꾸어줘야한다.
이 바꿔주는것을 viewResolver가 하게 하는 것이다.
@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();
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);
}
// 파라미터를 꺼내서 paramMap에 담아서 보내줌.
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");
}
}29.5 MyView
MyView는 받은 것 요청들을 다 정리해서 model과 함께 view로 보내줘야한다.
키밸류로 request에 setAttribute 다 넣어줘야한다.
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
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((key, value) -> request.setAttribute(key, value));
}
}뷰 리졸버
MyView view = viewResolver(viewName)
컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다.
그리고 실제 물리 경로가 있는 MyView 객체를 반환한다.
논리 뷰 이름: members
물리 뷰 경로: /WEB-INF/views/members.jsp
뷰리졸버를 나눠놓으면 경로가 변경되어도 프론트 컨트롤러만 고치면되는 것이다.
view.render(mv.getModel(), request, response)뷰 객체를 통해서 HTML 화면을 렌더링 한다.
뷰 객체의 render() 는 모델 정보도 함께 받는다.
JSP는 request.getAttribute() 로 데이터를 조회하기 때문에
모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다.
JSP로 포워드 해서 JSP를 렌더링 한다.
프론트 컨트롤러의 일이 점점 많아진다.
그렇지만 실제 구현한 클래스들이 계속 가벼워지고 있다.
컨트롤러들이 서블릿에 종속되지 않고 순수하게 자바코드로만 이루어지게 되는 것이다.
30. 단순하고 실용적인 컨트롤러 - v4
앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등 잘 설계된 컨트롤러이다.
그런데 실제 컨트톨러 인터페이스를 구현하는 개발자 입장에서 보면
항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거롭다.
좋은 프레임워크는 아키텍처도 중요하지만 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다.
소위 실용성이 있어야 한다.
이번에는 v3를 조금 변경해서 실제 구현하는 개발자들이 매우 편리하게 개발할 수 있는 v4 버전을 개발해보자
기본적인 구조는 V3와 같다.
대신에 컨트롤러가 ModelView 를 반환하지 않고 ViewName만 반환한다
30.1 인터페이스
paramMap만 넘겻는데 프론트컨트롤러가 모델까지 만들어서 넘겨준다.
그렇지만 view의 이름만 반환하면된다.
public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}30.2 구현클래스
ModelView를 생성하지 않고 그냥 이름만 반환한다.
model맵에 attribute를 넣어서 보내준다.
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}public class MemberSaveControllerV4 implements ControllerV4 {
private 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";
}
}public class MemberListControllerV4 implements ControllerV4 {
private 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";
}
}30.3 프론트 컨트롤러
프론트 컨트롤러에서 model을 만들어서 모델에 담아서 보내준다.
프레임워크에선 고친게 거의 없는데 개발자 입장에서는 modelview를 안만들어도
그냥 model에 put해서 넣어서 담아서 보내주기만 하면된다.
@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");
}
}정리
이번 버전의 컨트롤러는 매우 단순하고 실용적이다.
기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데
컨트롤러를 구현하는 개발자 입장에서 보면 이제 군더더기 없는 코드를 작성할 수 있다.
또한 중요한 사실은 여기까지 한번에 온 것이 아니라는 점이다.
프레임워크가 점진적으로 발전하는 과정 속에서 이런 방법도 찾을 수 있었다
31. 유연한 컨트롤러1 - v5
스프링 mvc를 이해하려면 어댑터를 이해해야한다.
지금까지의 문제는 모양이 고정되어있는게 문제이다.
v4를하다가 v1를 하는것이 불가능하다.
이문제를 해결할 수 있다.
하나의 컨트롤러에서 다양한 종류의 방식을 사용하고 싶다.
그런데 프론트 컨트롤러에 인터페이스가 박혀잇어서 타입이 안맞는게 들어갈 수가 없다.
어댑터 패턴을 사용하면 이문제를 해결할 수 있다.
지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.
ControllerV3 , ControllerV4 는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다.
마치 v3는 110v이고, v4는 220v 전기 콘센트 같은 것이다.
이럴 때 사용하는 것이 바로 어댑터이다.
어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경할 수 있다.
프론트컨트롤러와 컨트롤러사이에 핸들러 어댑터가 추가된다.
핸들러는 컨트롤러라고 이해하면된다.
핸들러 어댑터: 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다.
여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.
그 이유는 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.
핸들러에 들어오면 어떠한 종류도 해결할 수 있다.
31.1 MyHandlerAdapter
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException;
}핸들러 어댑터에는 두가지 기능이 있다.
boolean supports(Object handler)
handler는 컨트롤러를 말한다.
어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드다.
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
어댑터는 실제 컨트롤러를 호출하고 그 결과로 ModelView를 반환해야 한다.
실제 컨트롤러가 ModelView를 반환하지 못하면
어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.
31.2 ControllerV3를 어댑터
먼저 ControllerV3를 지원하는 어댑터를 구현하자
supports에게 지원할수 있는지물어보게 된다.
handler instanceof ControllerV3
ControllerV3타입이면 참을 반환하게 되는 것이다.
handle메소드에서는 파라미터를 받아서 paramMap에 담아서 ModelView를 반환하게 된다.
이 어댑터의 역할은 이 반환된것을 ModelView로 반환해야한다.
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;
}
}handler를 컨트롤러 V3로 변환한 다음에 V3 형식에 맞도록 호출한다.
supports() 를 통해 ControllerV3 만 지원하기 때문에 타입 변환은 걱정없이 실행해도 된다.
ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 된다
31.3 프론트컨트롤러
컨트롤러(Controller) 핸들러(Handler)
이전에는 컨트롤러를 직접 매핑해서 사용했는데
그런데 이제는 어댑터를 사용하기 때문에 컨트롤러 뿐만 아니라 어댑터가 지원하기만 하면, 어떤 것이라도 URL에 매핑해서 사용할 수 있다.
그래서 이름을 컨트롤러에서 더 넒은 범위의 핸들러로 변경했다
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/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 로 변경되었다.
핸들러 매핑
핸들러 매핑 정보인 handlerMappingMap 에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환한다.
핸들러를 처리할 수 있는 어댑터 조회
handler 를 처리할 수 있는 어댑터를 adapter.supports(handler) 를 통해서 찾는다.
handler가 ControllerV3 인터페이스를 구현했다면, ControllerV3HandlerAdapter 객체가 반환된다.
어댑터 호출
ModelView mv = adapter.handle(request, response, handler);
어댑터의 handle(request, response, handler) 메서드를 통해 실제 어댑터가 호출된다.
어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환한다.
ControllerV3HandlerAdapter 의 경우 어댑터의 모양과 컨트롤러의 모양이 유사해서 변환 로직이 단순하다.
32. 유연한 컨트롤러2 - v5
FrontControllerServletV5 에 ControllerV4 기능도 추가하는것이다.
프론트컨트롤러에서 v4를 처리하도록 해준다.
private void initHandlerMappingMap() {
// V3 추가
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());
// V4 추가
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()); // V4 추가
}32.1 ControllerV4HandlerAdapter
handler를 ControllerV4로 케스팅 하고, paramMap, model을 만들어서 해당 컨트롤러를 호출한다.
그리고 viewName을 반환 받는다.
어댑터변환
어댑터에서 이 부분이 단순하지만 중요한 부분이다.
어댑터가 호출하는 ControllerV4 는 뷰의 이름을 반환한다.
그런데 어댑터는 뷰의 이름이 아니라 ModelView 를 만들어서 반환해야 한다.
여기서 어댑터가 꼭 필요한 이유가 나온다.
ControllerV4 는 뷰의 이름을 반환했지만 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어 반환하게 된다.
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) {
ControllerV4 controller = (ControllerV4) 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;
}
}33. 정리
지금까지 v1 ~ v5로 점진적으로 프레임워크를 발전시켜 왔다.
지금까지 한 작업을 정리해보자.
v1: 프론트 컨트롤러를 도입
기존 구조를 최대한 유지하면서 프론트 컨트롤러를 도입
v2: View 분류
단순 반복 되는 뷰 로직 분리
v3: Model 추가
서블릿 종속성 제거
뷰 이름 중복 제거
v4: 단순하고 실용적인 컨트롤러
v3와 거의 비슷
구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공
v5: 유연한 컨트롤러
어댑터 도입
어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계
여기에 애노테이션을 사용해서 컨트롤러를 더 편리하게 발전시킬 수도 있다.
만약 애노테이션을 사용해서 컨트롤러를 편리하게 사용할 수 있게 하려면 어떻게 해야할까
바로 애노테이션을 지원하는 어댑터를 추가하면 된다
다형성과 어댑터 덕분에 기존 구조를 유지하면서, 프레임워크의 기능을 확장할 수 있다.
스프링 MVC
여기서 더 발전시키면 좋겠지만 스프링 MVC의 핵심 구조를 파악하는데 필요한 부분은 모두 만들어보았다.
지금까지 작성한 코드는 스프링 MVC 프레임워크의 핵심 코드의 축약 버전이고, 구조도 거의 같다.
스프링 MVC는 지금까지 우리가 학습한 내용과 거의 같은 구조를 가지고 있다
2023.05.31
실무에서도 크게 구조를 건드는 경우가 잇는데 처음 구조를 건들때는 구조만 건든다.
구조적인 큰거를 개선하고 디테일한것을 개선해야한다.
기존코드를 최대한 유지하고 문제가 없을때 세세한 부분을 고쳐나가야한다.
이런문제를 알게 되엇다.
배포를 하고 세밀한 부분을 고쳐나가는 방식이 좋다.
'기초단계 > SPRING' 카테고리의 다른 글
| 2023.06.05 Spring (0) | 2023.06.06 |
|---|---|
| 2023.06.03 Spring (0) | 2023.06.06 |
| 2023.05.30 Spring (0) | 2023.05.31 |
| 2023.05.29 Spring (0) | 2023.05.31 |
| 2023.05.27 Spring (0) | 2023.05.31 |