스프링 MVC 가 어떤 문제를 해결하기 위해서,
어떤 과정을 거쳐서 지금의 모습을 갖추게 되었는지 이해한 내용을 기록해보고자 합니다.
너무 방대하고 어려워서 DispatcherServlet 말만 들어도 스트레스였는데 이게 이해되다니 정말 기쁘네요 ;ㅅ;
멋진 강의 제작해주신 김영한님, 함께 대화하며 학습해준 칙촉, 멋진 테코톡 발표해주신 3기 코기 감사드립니다.
HTTP Message
이미지 출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/Messages
HTTP 요청과 응답 메시지는 위와 같은 형식으로 이루어진 문자열입니다.
서블릿은 위와 같은 문자열을 파싱해서 HttpServletRequest, HttpServletResponse를 만들어줍니다.
사실 이것만 해도 꽤나 혁명적이라고 느껴집니다.
저 문자열을 일일이 파싱해서 해석하고 처리하고
또 다시 저 형식으로 문자열을 만들어내야 할 것을 생각하면 어찌나 끔찍한지요.
이번 포스팅에선 서블릿 이전 시대까지는 다루지 않습니다.
시작점이 바로 여깁니다. HTTP Message를 편하게 다룰 수 있게 해주는 서블릿의 존재!
순수 Servlet 시절 : Servlet 컨테이너에 서블릿 객체 등록하기
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username"); // 요청 값 꺼내기
String authorizationHeader = request.getHeader("authorization"); // 헤더 값 꺼내기
response.setContentType("text/plain"); // 응답 형식 설정
response.setCharacterEncoding("utf-8"); // 응답 인코딩 설정
response.getWriter().write("hello " + username); // HTTP Response 메시지 작성
}
}
HttpServlet 을 상속받은 클래스에 service 메서드를 오버라이드 합니다.
아마 이 시절엔 XML설정 파일을 통해 특정 URL로 오는 요청을 어떤 클래스가 처리하게 해줘 라고
서블릿 컨테이너에 알려줘야 했을 겁니다만, 이번 포스팅에선 거기까진 들어가지 않고,
애너테이션 기반으로 간단히 동작 방식만 알아보도록 할게요.
아무튼 지금의 컨트롤러와 유사한 형식이 보이실 겁니다.
urlPatterns 를 통해서 /hello 요청이 왔을 때 HelloServlet의 service 가 호출되는 것이죠.
service 메서드에 있는 request, response 매개변수는 서블릿에서 처리를 해준 결과입니다.
저는 HttpServletRequest가 Spring이 해주는 것인 줄 알았는데... 서블릿인 걸 이제야 알았습니다 ^^;
request에서 요청값을 꺼내는 모습은 현재와 유사해보입니다.
참고로 GET 요청의 QueryString과 Form 요청의 POST 요청이나
둘다 key=value&key2=value2 와 같은 동일한 형식을 띄기 때문에,
URL에 명시되는지, body에 명시되는지 차이만 있을 뿐
HttpServletRequest 를 통해 전달된 값을 꺼낼 땐 동일한 방법으로 꺼낼 수 있습니다.
(현재의 @RequestParam도 마찬가지입니다)
응답을 해주는 모습은 다소 다른데요, service 메서드의 리턴 타입이 void입니다.
따라서 response 객체를 통해 직접 응답값을 작성해줘야 합니다.
만약 동적인 HTML 문서를 응답하려 한다면 한 땀 한 땀 Java 코드 문자열로 작성해줘야 했죠.
HttpServlet을 상속하고 service 메서드를 재정의해서 만든 이 클래스 하나가 하나의 서블릿 객체가 됩니다.
그리고 서블릿 컨테이너에서는 요청이 왔을 때 이를 처리할 서블릿 객체를 생성하고 호출합니다.
싱글턴으로 관리되어 한 번 생성, 호출된 이후에는 재사용하여 호출만 하게 됩니다.
이 당시에는 여러 엔드포인트를 만드려면 엔드포인트마다 클래스가 늘어나게 되고,
그 클래스가 각각 서블릿 컨테이너에 서블릿 객체로 등록되는 방식이었습니다.
순수 Servlet 시절 : HTTP 메시지 Body 값 추출하기
@WebServlet(name = "requestBodyStringServlet", urlPatterns = "/request-body-string")
public class RequestBodyStringServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// HTTP 메시지 Body를 Stream으로 읽기
ServletInputStream inputStream = request.getInputStream();
// Stream을 문자열로 변환할 땐 인코딩 방식을 명시해야 한다
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
// {
// "name": "rich",
// "age": 22
//}
response.getWriter().write("ok");
}
}
앞서 논의되지 않는 부분들이 있는데요,
첫째는 HTTP Method 구분입니다. 어디에도 명시되지 않았죠 ㅎㅎ 아무 메서드로 호출해도 다 호출됩니다.
둘째는 GET과 Form의 POST 요청은 모두 HttpServletRequest.getParameter() 로 간단하게 꺼낼 수 있는데요,
JSON이나 XML 형식으로 HTTP 메시지 Body에 작성된 값은 어떻게 사용해야 할까요?
물론 저 정도로 꺼내서 쓸 수 있는 것만 해도 당시에는 감지덕지였을지도 모르겠습니다.(단 두 줄의 코드로 메시지 Body를 바로 문자열로 변환할 수 있다규?!~!)
아마 최초에는 XML이나 JSON 형식의 데이터를 메시지 Body에 담아 주고 받을 필요성 자체가 없었을지도 모르겠습니다.
아무튼 서블릿 시절, 서블릿 객체를 저렇게 하나 등록하고,
매번 매개변수를 HttpServletRequest 에서 꺼내서 사용하고,
HttpServletResponse에 응답할 내용을 직접 적어줘야 했습니다.
ObjectMapper.readValue(string, SomeObject.class)를 이용해 JSON을 객체로 파싱하는 것,
ObjectMapper.writeValueAsString(object) 를 이용해 객체를 문자열로 변환하는 것은 생략하겠습니다.
추가로 관련하여 새롭게 알게된 사실은 JSON의 경우, UTF-8 인코딩이 규약상 기본값이라는 것입니다.
따라서 read, write 관련 메서드에 StreamUtils와 달리 인코딩을 명시하지 않아도 되고,
Content-Type, produces 에도 application/json 이후에 ;charset=utf-8과 같이 인코딩을 명시하지 않아도 됩니다.
Front Controller 의 등장 ( DispatcherServlet, Handler )
@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();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
만약, 기존 순수 Servlet 방식으로 모든 URL 들에 매핑될 컨트롤러를 작성했는데,
모든 컨트롤러에 공통적으로 처리할 내용이 추가되어야 한다면 어떨까요?
모든 컨트롤러에 찾아가서 해당 내용을 추가해줘야 합니다.
만약 그 공통 관심사가 수정되어야 한다면? 마찬가지로 모두 수정해야 합니다.
대표적으로 로깅과 인증 기능이 이에 해당한다고 볼 수 있겠습니다.
Front Controller 의 필요성이 바로 여기에 있습니다.
하나의 입구를 두는 것입니다.
Front Controller는 모든 URL에 대한 요청을 받습니다.
그리고 내부적으로 여러 Controller에 대한 매핑정보를 가지고 있다가
이를 처리할 Controller를 찾아서 처리하게 한 뒤, 결과를 받아서 Servlet에게 응답을 보내는 겁니다.
공통 관심사에 대한 처리 외에도 이러한 방식은 한 가지 차이점이 더 있는데요,
기존 방식은 엔드포인트가 추가될 때마다 서블릿 컨테이너에 객체가 추가되는 방식이었습니다.
그러나 Front Controller 패턴을 적용한다면, Front Controller 하나만 서블릿 객체로 등록됩니다.
즉 Front Controller 에게 등록되는 Controller 들은
서블릿 컨테이너에 의해 관리되지 않고, 우리가 만든 Front Controller 에 의해 호출됩니다.
코드를 보면 Front Controller 는 controllerMap 이라는 인스턴스 변수를 가지고 있는데요,
Value의 자료형은 ControllerV1 입니다.
Value에 들어가는 구현체들은 Form, Save, List 등으로 다양한데요,
모두 ControllerV1 인터페이스를 구현한 구현체들입니다.
앞서 순수 Servlet에서 구성했던 service 메서드와 완전히 동일한, process 메서드를 구현했습니다.
MyView 도입을 통한 중복 코드 제거 ( ViewResolver )
Front Controller를 도입한 이후에도
특정 페이지로 이동시키기 위한 위의 3줄의 코드는 모든 컨트롤러에 중복으로 작성해야만 했습니다.
컨트롤러마다 달라지는 내용은 viewPath에 해당하는 문자열입니다.
이번엔 이 중복을 제거해봅시다.
먼저 ControllerV1 인터페이스를 V2로 변경합니다.
달라진 점은 리턴 타입 뿐입니다. 기존엔 리턴값이 없었는데, 이제는 MyView를 리턴합니다.
Front Controller는 리턴된 MyView 인스턴스의 render 메서드를 호출하기만 하면 됩니다.
- 페이지 이동을 위해 사용되던, 모든 컨트롤러에 있어야 했던 중복 코드
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
- FrontController에 등록되는 컨트롤러들이 구현할 메서드 process 메서드에 MyView 라는 리턴 타입이 새로 생겼습니다
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
- MyView 는 생성자 매개변수로 이동할 페이지의 정보를 받아서 render 호출 시 이동시킵니다
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);
}
}
- Front Controller의 service 메서드 중 변경된 내용
// Before : V1은 응답값이 없음. 페이지 이동 필요시 중복 코드 발생
controller.process(request, response);
// After : V2는 MyView를 리턴. 이동할 페이지 정보만 MyView에 담아 리턴하면 됨
MyView view = controller.process(request, response);
view.render(request, response);
Model 도입을 통한 중복 코드 및 서블릿 종속성 제거 ( Argument Resolver )
이번에도 중복의 제거입니다.
첫째는 MyView 생성자에 View의 이름을 전달할 때 중복되는 내용을 제거해봅시다.
가령 "/WEB-INF/views/new-form.jsp" 가 view 이름이라면, "new-form" 만 전달해도 되게 말이죠.
두번째는 서블릿 종속성을 제거해봅시다.
보다 쉽게 이야기하자면, 모든 컨트롤러들이 구현하는 인터페이스의 메서드 process를 변경하는 건데요,
HttpServletRequest, HttpServletResponse를 직접 받아서 처리하는 방식에서,
매개변수를 Map으로 받아서, 응답할 데이터와 view 이름을 하나의 객체로 응답하게 한다는 것입니다.
Front Controller가 모든 요청을 받아서 이를 분기해주기 때문에,
서블릿 컨테이너에 서블릿 객체로 등록되는 것은 Front Controller 하나입니다.
따라서 Front Controller에 의해 호출되는 Controller들이 구현하는 인터페이스,
process 는 매개변수로 HttpServletRequest, HttpServletResponse를 굳이 사용할 필요가 없습니다.
매개변수는 Front Controller가 HttpServletRequest에서 꺼내서 직접 전달해줄 수 있고,
응답값은 Controller의 응답값으로 받아서 Front Controller가 전달해줄 수 있기 떄문이죠.
만약 이런 변경으로 상세 컨트롤러들이 서블릿 종속성을 제거할 수 있다면,
테스트하기에도 더 쉬운 코드가 될 것입니다.
- 각 컨트롤러의 서블릿 종속성 제거
- 데이터를 Map으로 전달받고, View 정보와 응답데이터 Model을 ModelView에 담아서 리턴
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
- ModelView는 viewName과 model 로 구성
- model 은 응답에 사용할 데이터
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
// constructor, getter, setter ...
}
- Front Controller의 변경 내역
- 결과적으로 각 컨트롤러에서는 논리 viewName만 반환하면 되게 되었고, 서블릿 종속성이 제거되었습니다.
- 서블릿 종속성이 제거되었다는 것은, HttpServletRequest/Response 를 직접 다루지 않게 되었다는 의미입니다.
// Before : Front Controller V2는 각 컨트롤러에게 Request, Response를 직접 전달함.
// 응답에 보낼 데이터는 각 컨트롤러가 직접 Response에 담아야함. 서블릿 종속성 유지.
// view 정보는 모든 물리 주소를 각 컨트롤러가 모드 명시해야함.
MyView view = controller.process(request, response);
view.render(request, response);
// After : V3는 컨트롤러에게 HttpServletRequest에 있는 데이터 꺼내서 전달함.
// 응답에 보낼 데이터와 view 논리 주소를 ModelView에 담아서 반환. 서블릿 종속성 제거.
// ModelView에 있는 model에 해당하는 데이터를 Front Controller가 Response에 담음
// view 정보는 논리 주소만 리턴받아서 Front Controller가 이를 물리 주소로 만들어냄.
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);
// Front Controller의 viewResolver 메서드는 prefix, suffix 중복을 제거하기 위해 존재한다.
// 각 컨트롤러에서 반환한 ModelView의 논리 viewName을 물리 viewName으로 조합해준다.
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
// Front Controller의 createparamMap 메서드는 각 컨트롤러의 서블릿 종속성 제거를 위해 존재한다.
// HttpServletRequest 에 있는 모든 데이터를 Map에 담아서 전달하는 과정에서 사용된다.
// Argument Resolver?
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;
}
- MyView의 render 메서드의 변경내역
- ModelView의 Map<String, Object> model을 받아서, HttpServletRequest 에 모두 담습니다.
- (여기에서 Request에 담는 이유는, JSP 템플릿에서 사용하기 위함이니 View에 데이터를 전달한다는 정도로 이해합시다)
// 각 컨트롤러가 ModelView 에 Map<String, Object> model 에 담아둔 응답 데이터를
// Front Controller V3 에서 이를 MyView의 render에 전달합니다
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);
}
// View에서 사용될 데이터들을 request에 모두 담아둡니다
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
더 사용하기 쉬운 컨트롤러로 개선
이번 개선 목표는 각 컨트롤러에서 담당해야 하는 ModelView 생성 책임을 덜어내는 것입니다.
각 컨트롤러가 ModelView의 존재를 알 필요도 없게 만들어봅시다.
앞서와 마찬가지로 매개변수는 Map으로 받게 되어 서블릿은 모르는 상태가 유지됩니다.
이제는 응답에 사용되는 데이터를 직접 반환하는 것이 아니라, 매개변수로 전달된
또다른 Map에 데이터를 담기만 하고, 논리 view 정보를 문자열로 반환하게 해보죠.
인터페이스 시그니처가 다소 변경됩니다.
기존 Map 하나를 받아 ModelView를 반환하던 방식에서,
Map 두 개를 받아 String을 반환하는 방식으로 바뀝니다.
- 조금 더 사용하기 편리한 컨트롤러가 되기 위한 새로운 인터페이스 정의
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
- 각 컨트롤러가 ModelView 를 직접 생성해서 반환하던 기존 버전
@Override
public ModelView process(Map<String, String> paramMap) {
Member member = new Member(paramMap.get("username"), Integer.parseInt(paramMap.get("age")));
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
- 각 컨트롤러의 ModelView에 대한 종속성이 제거된 개선 버전
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
Member member = new Member(paramMap.get("username"), Integer.parseInt(paramMap.get("age")));
memberRepository.save(member);
model.put("member", member);
return "save-result";
}
- Front Controller 변경 내역
- 응답 데이터를 담을 Map을 Front Controller가 직접 생성해서 전달한다.
- 이로 인해 각 컨트롤러는 응답 데이터를 model 에 담기만 하면 되고, 논리 view를 단순 문자열로 반환하면 된다.
// Before : 기존 Front Controller. 각 컨트롤러가 ModelView를 반환해야 한다.
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);
// After : 개선된 Front Controller.
// 각 컨트롤러는 전달된 model에 값을 담기만 하면 되고,
// 논리 view 정보를 문자열로 반환하기만 하면 된다. ModelView에 대한 종속성도 제거되었다.
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
// render 메서드 시그니처는 변경되지 않았다. 동일하게 Map을 받아 처리하고 있다.
view.render(model, request, response);
두 종류의 컨트롤러 모두 사용하기 ( HandlerAdapter )
앞서 ModelView를 반환하는 컨트롤러와 String을 반환하는 컨트롤러 두 종류가 있었는데요,
이제는 FrontController가 이처럼 규격이 다른 컨트롤러를 모두 사용할 수 있게 만들어 보겠습니다.
이번 시도로 두 종류를 모두 사용할 수 있게 구조가 변경된다면,
추후 또다른 규격의 컨트롤러가 나타나더라도 유연하게 대응할 수 있는 구조가 될 것 같군요.
서블릿 객체로 등록되는 것은 FrontController 하나입니다.
FrontController는 요청 URL을 통해 이를 처리할 Controller를 찾아 호출하는 거죠.
이때, Controller에 응답값이 다름에도, FrontController가 처리를 할 수 있으러면,
FrontController와 각 컨트롤러 사이에 규격을 맞춰주는 어댑터가 필요하겠습니다.
FrontController는 ModelView를 응답받아야 이후의 처리가 가능합니다.
ModelView에는 응답에 사용할 데이터와 view 정보가 담겨있는데요,
어떤 형태의 컨트롤러에게 처리를 위임하더라도 이 두 정보는 항상 필요하기에,
어댑터를 통해 컨트롤러가 어떤 식으로 응답을 하던 ModelView로 일관된 응답을 할 수 있게 만들어야겠습니다.
이 경우 어댑터는 어떤 컨트롤러용 어댑터인지 정보가 추가로 필요합니다.
따라서 어댑터 인터페이스는 어떤 컨트롤러를 지원할 수 있는지 확인할 수 있는 메서드와,
컨트롤러에게 요청을 보내고 응답을 받아 ModelView를 반환하는 메서드 두 가지가 필요하겠습니다.
추가로 FrontController가 하던 매개변수를 추출해서 컨트롤러 호출시 매개변수로 전달하던 역할도
어댑터에게 맡겨버린다면 FrontController 보다 가벼워지면서도 확장에 유연한 설계가 가능할 것 같습니다.
또한 이젠 이름을 컨트롤러에서 핸들러로 변경할 수 있겠습니다.
어댑터에게 많은 역할을 위임함으로써 보다 다양한 형태를 처리할 수 있게 되었기 때문입니다.
컨트롤러의 역할을 넘어서서 어떠한 요청을 처리하는 존재로 보다 추상적인 존재로 격상될 수 있는 것이죠.
- FrontController와 핸들러 사이에서 역할을 수행하는 어댑터 인터페이스를 정의합니다.
- supports 메서드는 매개변수로 전달된 핸들러를 지원할 경우 true를 반환하게 구성해야 합니다.
- handle 메서드는 핸들러를 통해 요청을 처리하고, ModelView를 FrontController에게 반환하도록 구성해야 합니다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler)
throws ServletException, IOException;
}
- ModelView를 반환하던 핸들러를 담당할 어댑터는 ControllerV3에 대해선 support 가능합니다.
- HttpServletRequest로부터 ControllerV3가 필요로 하는 매개변수를 만들어서 전달하는 것도 어댑터의 역할입니다.
- SpringMVC에서는 이 역시 어댑터에서 ArgumentResolver로 별도 역할을 추후 분리해내게 됩니다.
- ControllerV3는 처리 결과로 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)
throws ServletException, IOException {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
}
- 논리 View 네임을 String으로 반환하는 V4 핸들러용 어댑터입니다
- ControllerV4는 support 한다고 응답할 것입니다
- 매개변수를 준비해서 핸들러를 호출하고, 결과적으로 ModelView를 FrontController에게 응답하는 것은 동일합니다
- 다만 ControllerV4가 String을 반환하기에, 이를 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)
throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
}
- FrontController 변경내역입니다
- 이제는 핸들러를 직접 호출하지 않습니다. 핸들러들은 각자 다양한 응답 자료형을 가질 것입니다
- 그러나 핸들러 어댑터들은 일관되게 ModelView를 반환해줍니다
- 따라서 FrontController는 어댑터가 핸들러를 호출하고 ModelView를 반환하도록 합니다
- 어댑터가 핸들러를 호출해야 하기 때문에, URI로 핸들러를 먼저 찾지만, 핸들러를 통해 어댑터를 찾아 어댑터를 호출합니다.
- 어댑터로부터 ModelView를 응답받은 이후는 기존과 동일합니다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// RequestURI를 통해 어떤 핸들러가 요청을 처리해야할지 탐색
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 발견된 핸들러를 담당하는 어댑터를 탐색
MyHandlerAdapter adapter = getHandlerAdapter(handler);
// 어댑터를 통해 핸들러를 호출하고, 일관되게 ModelView를 응답받습니다
ModelView mv = adapter.handle(request, response, handler);
// 이후 처리는 동일합니다
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
정리
FrontController는 SpringMVC에서 DispatcherServlet입니다.
서블릿 컨테이너에 유일하게 등록되는 서블릿 객체, 그것입니다!
DispatcherServlet의 상속 계층을 따라가다보면 HttpServlet이 보입니다.
태초에 이용했던 ServletController와 동일한 거죠!
DispatcherServlet 의 필드로는 List<HandlerMapping> handlerMappings,
List<HandlerAdapter> handlerAdapters 가 있는데요, 이제 이름을 보면 어떤 역할인지 감이 오실 것 같습니다.
바로 핸들러와 핸들러 어댑터 목록을 리스트로 가지고 있는 필드입니다.
이들을 순회하며 요청을 처리할 핸들러, 해당 핸들러를 담당하는 어댑터를 찾는 것이죠.
그리고 어댑터의 handle 메서드를 통해 핸들러를 호출하고 ModelAndView를 응답받습니다.
관련하여 실제 코드를 확인하기 원하시면 DispatcherServlet의 doDispatch 메서드를 살펴보시면 좋을 것 같습니다.
와.. 이렇게 드디어. 스프링이 저의 이야기가 되었네요.
이제는 스프링 디버깅하다 Read Only 영역을 만나더라도 흐름을 파악할 수 있을 것 같습니다.
여기까지 오도록 수고해주신 수많은 개발자분들, 그리고 이러한 지식을 정말 쉽게 전달해주신 김영한님 정말 감사드립니다.
결론
- 이 모든 과정이 중복 코드를 점진적으로 줄여나가는 과정으로 보였습니다.
- DI를 통한 객체지향으로만 스프링을 이해하고 있었는데,
HTTP 메시지, Servlet으로부터 Front Controller, DispatcherServlet의 탄생 배경을 알고 나니
비즈니스 로직에 집중할 수 있게 해준다는 게 어떤 건지 더욱 체감되는 것 같습니다. - 지금까지 DispatcherServlet 이야기만 나오면 어질어질하고.. 디버깅하다가도
Read Only 영역 나오면 빠르게 후퇴했었는데... 이제는 스프링의 구조가 저의 이야기가 된 것 같습니다. - 스프링 MVC 학습을 제안해주고 함께 해준 칙촉이 없었다면 이런 이해를 가질 수 없었을 것 같습니다.
학습 출처
우테코 4기 크루 칙촉과의 대화
'Java & Spring' 카테고리의 다른 글
의존이 복잡하게 얽힌 Bean들은 어떻게 생성될까? (4) | 2022.07.05 |
---|---|
🌱 Spring에 Handler가 등록되는 과정 (0) | 2022.06.21 |
🤔 객체지향 생활체조 돌아보기 (우테코 레벨2를 마치며) (0) | 2022.06.17 |
오라클 클라우드 Autonomous Database DB접속 로컬 + 서버 (0) | 2021.10.18 |
모의면접 복기 (2) - 가비지 컬렉션 Garbage Collection (0) | 2021.09.27 |