※ 이 포스트는 스프링 실습 과정에서 작성하기 때문에 정보가 부정확할 수 있는 부분이 있습니다.
따라서 참고만 해주시고 틀린 부분이 있을 경우 알려주시면 감사하겠습니다.
❗ 이번 포스트는 김영한님의 '스프링 MVC 1편 - 백엔드 웹개발 핵심 기술' 강의를 수강하고 배운 내용을 정리하여 작성하였습니다. 따라서 스프링에 대해 더 자세히 공부하고 싶으신 분은 인프런에서 해당 강의를 수강하시길 추천합니다. 또한 서블릿, mvc가 무엇인지 아예 모르시는 분들은 이전 포스트인 https://fadet-coding.tistory.com/34
를 보시거나 다른 기초 설명들을 보고 오시면 좋을 것 같습니다.
❗ 이전 글에서 이어집니다! 따라서 잘 이해가 안되신다면 이전 글을 읽고 와주세요!
* 잘 모르시는 기술은 로그인 필요 없이 이 곳에서 AI에게 물어보세요!
Index
1 mvc와 서블릿 version1 - FrontController 도입 초기
2 mvc와 서블릿 version2 - MyView
3 mvc와 서블릿 version3 - ModelView
4 mvc와 서블릿 version4 - 핸들러 어댑터
mvc와 서블릿 version1 - FrontController 도입 초기
이전 포스트에선 서블릿, JSP만 사용하는 것의 한계와 MVC의 도입, MVC에 FrontController가 도입되는 배경을 살펴봤습니다. 이번 포스트에선 FrontController의 진화 과정을 다뤄보겠습니다. FrontController의 도입 전 basic MVC의 구조를 모르신다면 https://fadet-coding.tistory.com/37를 보고 오시길 바랍니다.
헷갈리실까봐 먼저 version1의 경우 아직 모든 Controller가 서블릿이고 FrontController도 서블릿입니다. version1의 구조는 다음과 같습니다.
위 과정을 알아보기 위해 처음 도입할 FrontController를 살펴보겠습니다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/frontcontroller/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);
}
}
해당 프론트컨트롤러는 크게 두 파트로 구성되어 있습니다. 흐름에서 순서 1에 해당하는 URL 매핑 정보를 담당하는controllerMap에 해당하는 컨트롤러를 집어넣는 생성자 부분과 순서 2에 해당하는 요청이 오면 controllerMap의 URI와 대조해 해당하는 process 메서드를 호출하는 부분입니다. 순서 3에 해당하는 process메서드는 서블릿들 내부에서 이렇게 정의되어 있습니다. 앞으로 유저 등록 폼, 등록, 목록 출력 서블릿 중 목록 출력 서블릿을 대표로 보여드리겠습니다.
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);
}
}
process()의 로직은 이전 포스트 basic 버전 서블릿의 service()와 별다를 바가 없습니다. 이제부터 도입 전인 basic mvc와 구분되도록 프론트컨트롤러 도입 초기 버전을 mvc version 1이라고 하겠습니다. 아직 version1은 쓸데 없는 공통 과정이 서블릿 내에 살아 있으며 여러모로 불편합니다. 앞으로 이 프론트컨트롤러를 도입한 version1을 다듬어가며 스프링 MVC의 기본 구조를 더 살펴보겠습니다.
mvc와 서블릿 version2 - MyView
version1에선 다음과 같은 부분이 서블릿마다 항상 중복됩니다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
매번 viewPath를 항상 명시해줘야하는건 매우 불편할 뿐만 아니라 경로가 수정되면 항상 해당 서블릿을 찾아서 수정해줘야합니다. dispatcher를 호출한 뒤 forward하는 과정은 코드 전체가 통으로 중복됩니다. 이 부분을 해결하기 위해 version2를 도입하겠습니다,
version1과 다른 부분은 Controller가 직접 forward()를 호출하는 것이 아닌 컨트롤러는 MyView라는 위의 중복 과정을 처리하는 객체를 도입하는 순서3의 형태를 갖고 MyView가 jsp에 forward 요청을 하는 순서5를 진행합니다. MyView 클래스는 이렇게 생겼습니다. 헷갈리실까봐 얘기하는데 MyView는 jsp같은 직접적인 view가 아닌 jsp에 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);
}
}
위의 중복 과정을 포함하는 것 말고는 별다른 특징이 없습니다. 아래는 원래 forward() 요청을 직접하던 void process()가 MyView process()로 변경된 version2의 멤버 목록 컨트롤러입니다.
public class MemberListControllerV2 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
// 메서드의 return값이 void에서 MyView로 변경
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");
}
}
이제 view가 반환되어 forward() 요청을 하는 코드를 기존 FrontController에 작성해주면 됩니다.
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/frontcontroller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
/* ... 동일 ... */
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* ... 동일 ... */
MyView view = controller.process(request, response);
view.render(request, response);
}
}
마지막에 MyView에서 정의했던 render() 메서드가 호출되는 것을 볼 수 있고 컨트롤러에 매번 중복되게 작성되던 dispatcher 객체의 호출 코드는 render() 메서드로 처리됩니다.
mvc와 서블릿 version3 - ModelView
version2는 기존에 갖고 있던 중복 요소를 꽤나 잘 정리했습니다만 아직 이전에 생겼던 의문이 해결되지 않았습니다. 일부 서블릿은 response를 사용하지 않는데 인자로 받는 문제가 말이죠. 이는 HttpServletRequest, HttpServletResponse을 모든 서블릿들이 인자로 받기 때문에 생긴 문제기에 굳이 FrontController가 아닌 다른 서블릿들 모두 이 객체들을 직접 받아야하는 이유가 있을까 고민이 됩니다.
이것을 해결하려면 현재 request.setAttribute를 통해 model에 정보를 담는 과정 자체같은 request가 하는 일을 따로 분리시켜줘야 됩니다. 따라서 이제 request대신 model이라는 빈 객체 덩어리를 하나 만들게 됩니다. 고치는 김에 중복되는 코드가 더 있으면 정리해주는 간단한 작업도 같이 해주도록 합시다. 어? 가만보니 viewPath가 눈에 거슬립니다.
/WEB-INF/views/new-form.jsp
/WEB-INF/views/save-result.jsp
/WEB-INF/views/members.jsp
prefix인 경로 앞부분과 suffix인 .jsp가 viewPath 작성시 계속 중복됩니다. 따라서 이 물리 주소를 논리 이름으로 고치도록 합시다. 이 두 과정을 거치면 version3는 다음과 같을겁니다.
위의 흐름도에서 ver2와 변경된 점은 ModelView와 viewResolver 도입입니다. 지금까지 model에 객체를 담는 과정은 request.setAttribute를 통해 이뤄졌기에 모든 서블릿이 request, response 객체를 받아야했습니다. 하지만 순서3에서 반환되는 ModelView는 이 과정을 벗어나 말그대로 model 객체를 따로 관리하고 위에서 말한 view의 논리 이름까지 전달하는 클래스이기에 이 클래스의 도입으로 더 이상 그럴 필요가 없어집니다. 순서 4,5에 해당하는 viewResolver는 이렇게 반환된 view의 논리 이름을 받아 알맞는 view 경로를 반환해주는 메서드가 될 것입니다.
ModelView의 코드입니다. Getter와 Setter 코드는 생략했습니다.
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
ModelView는 보시다시피 Map타입의 model을 필드로 가지고 있습니다. 이제 해당 필드가 있으므로 더 이상 컨트롤러들은 request를 받아야하는 서블릿일 이유가 없고 프론트컨트롤러 단 하나만 서블릿이면 충분합니다! 아래는 컨트롤러들의 공통 인터페이스 변경전인 Ver2와 Ver3를 나열했습니다. 인자로 받는 paramMap은 좀 이따 설명하고 viewName은 기존 viewPath의 논리 이름입니다.
// ModelView 도입 전
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
// ModelView 도입 후
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
인터페이스가 변경되었기에 구현체가 오버라이드해야하는 메서드도 더 간단하게 수정 가능합니다.
public class MemberListControllerV3 implements ControllerV3 {
@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;
}
}
이제 각 컨트롤러들이 수정되었으니 요청을 처리하는 프론트컨트롤러의 로직을 수정하여 줍니다.
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
/* ... 동일 ... */
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
/* ... 동일 ... */
// 맨 아래 createParamMap 메서드 정의함
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<>();
req.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");
}
}
위 코드에서 컨트롤러가 반환되는 과정까지는 다를게 없습니다. 그 다음에 바로 request로 넘어온 사용자 정보를 받아 paramMap을 생성하는 코드가 나오는데 가독성을 위해 생성 메서드인 createParamMap을 아래로 빼줬습니다. 아래의 createParamMap을 보시면 paramMap이란 requeest 요청에 담겨있던 파라미터 정보들을 하나씩 빼서 Map으로 만드는 요청 파라미터 객체입니다.
이 요청 파라미터 객체인 paramMap을 비즈니스 로직인 process 메서드에 인자로 넣어 ModelView 객체를 반환 받습니다. 만들어진 ModelView 객체에서 viewName을 get으로 가져와 viewResolver에 인자로 넣습니다. viewResolver 메서드는 아래에 있다시피 viewName에 prefix, suffix를 붙여 MyView를 반환합니다.
아직 코드들을 보면 전 ver2에서 해주었던 forwad 요청이 아직 없습니다. 그 요청은 전 버전의 MyView가 똑같이 진행하지만 전 버전과 달라진건 MyView의 redenr()에 인자로 model이 추가되었다는 것입니다. 따라서 로직을 수정해주어야 합니다.
// version3의 MyView
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
/* version2의 MyView render()
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 = req.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
/* 정리 메서드 */
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
MyView의 역할은 JSP에 model을 전달하는 것입니다. version2에선 request에 model 정보가 담아진 채로 넘어왔기에 request를 바로 forward하는 로직이었지만 version3에선 model과 request가 각각의 인자로 분리되서 넘어오기 때문에 request를 forward하는 결과는 동일하지만 request.setAttribute해주던 과정을 거쳐야하고 해당 과정은 modelToRequestAttribute 메서드를 통해 진행하며 해당 메서드의 로직은 이전과 마찬가지로 따로 아래에 정리했습니다.
mvc와 서블릿 version4 - 핸들러 어댑터
version3까지 mvc구조를 다듬었기에 모든 컨트롤러가 서블릿이어야하던 기존 구조에서 프론트컨트롤러를 제외하고는 서블릿 종속성이 사라졌고 modelView의 도입으로 model을 좀 더 편하게 관리할 수 있게 되었습니다. 사실 mvc 구조의 핵심 기능은 앞서 알아본 프론트컨트롤러와 modelView, dispatcher, forward들이 어떻게 맞물리는지만 이해해도 충분히 파악했다고 할 수 있지만 한 가지 더 살펴볼 것이 남아있습니다.
현재 ver3의 ControllerV3 인터페이스의 process()는 다음과 같이 ModelView를 반환합니다.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
그런데 규모가 커지면 개발은 혼자하지 않습니다. 만약 다른 개발자가 ControllerV3Clone이라는 동일한 역할을 하는 컨트롤러 인터페이스를 작성했는데 그 개발자는 return값을 ModelAndView가 아닌 view의 논리 이름인 String으로 반환하게 코드를 작성했습니다.
public interface ControllerV3Clone {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
이런 경우 누가 잘못되었다고 말할 수는 없습니다. 코드에 있어서 정해진 답은 없기때문에 상황에 따라 서로 장단이 존재할 것입니다. 하지만 코드의 적합성 여부를 떠나서 이렇게 컨트롤러 인터페이스가 달라지게되면 기존 작성했던 프론트컨트롤러가 컨트롤러를 반환하는 로직은 더 이상 사용하지 못하게 됩니다. process를 거쳤을때 ModelView를 반환하게 짜여있던 로직은 String을 반환하면 돌아가지 않고 이 경우 프론트컨트롤러가 일정 컨트롤러에 대해 종속성을 가지게 되었다고 표현합니다. 하지만 우리는 프론트컨트롤러가 종속되지 않고 다양한 컨트롤러에 호환되는 로직을 만들고 싶습니다. 여기서 핸들러 어댑터라는 것이 도입됩니다.
우리가 일상에서 흔히 사용하는 콘센트는 220V입니다. 하지만 살다보면 110V를 사용하는 전자기기를 켜야할 일이 생기곤 하는데 이럴때는 보통의 콘센트를 사용하지 못합니다. 하지만 110V 어댑터가 구비되어 있다면 콘센트에 전원을 연결할 수 있습니다. 핸들러 어댑터도 이와 똑같습니다. 어떤 컨트롤러는 A를 반환하고 어떤 컨트롤러는 B를 반환하는데다 로직도 서로 조금 다르다고 할 때 프론트컨트롤러는 해당 컨트롤러에 맞는 어댑터를 사용해 컨트롤러들과 연결할 수 있게 됩니다.
여기서 핸들러란 웹 요청을 처리하는 객체를 통칭하는데 모든 컨트롤러는 이 핸들러에 해당됩니다. 위 흐름도를 보면 version3의 주요 로직과 달라진 부분은 존재하지 않지만 프론트컨트롤러가 직접 핸들러를 호출했던 것과 달리 프론트컨트롤러는 요청을 받아 요청에 맞는 핸들러어댑터를 찾고(순서1, 2) 그 어댑터를 호출하기만 하며(순서 3), 핸들러의 로직를 사용(순서 4)하여 ModelView 등을 반환하는 것(순서 5)은 핸들러 어댑터가 됩니다. 여기서 왜 이름이 컨트롤러 어댑터가 아니냐는 물음을 할 수도 있는데 스프링은 핸들러 어댑터를 사용함으로 앞서 말한 컨트롤러의 개념을 포함하는 모든 핸들러를 처리할 수 있도록 설계됐기 때문에 해당 클래스의 이름을 핸들러 어댑터로 사용합니다.
개념은 충분히 설명했으니 코드를 마저 보도록 합시다. 아래는 핸들러 어댑터의 인터페이스입니다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response,Object handler) throws ServletException, IOException;
}
앞서 설명한 것처럼 앞으로 controller와 handler는 동일한 의미처럼 받아 들이면 됩니다. 해당 인터페이스는 필드로 boolean을 반환하는 supports()과 ModelView를 반환하는 handle() 메서드를 필드로 가집니다. supports()는 해당 어댑터가 특정 컨트롤러를 지원하는지 판단하는 메서드이며 handler()의 경우 기존에 프론트컨트롤러가 핸들러에게 직접 받아왔던 ModelView를 대신 받아오는 메서드입니다. 이 코드의 handler()의 경우 return이 ModelView라 문제가 없겠지만 만약 핸들러의 return이 다른 객체면 어떻게 해야할까요? 당연히 어댑터 내에서 ModelView를 생성해서라도 반환하게끔 로직을 짜야합니다. 어댑터란 모든 상황에서도 작동하도록 생성되어야 합니다.
위 인터페이스를 통해 핸들러어댑터가 컨트롤러의 지원 여부를 파악하여 지원 가능하다면 ModelView를 반환하도록 정의했습니다. 이제 이 인터페이스를 구현하는 ControllerV3용 어댑터를 만들겠습니다.
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()는 ControllerV3를 처리할 수 있는지 판단합니다. 만약 지원한다면 handler() 메서드를 실행합니다. handler() 메서드를 보면 핸들러 객체를 인자로 받아 ControllerV3로 형 변환을 시킵니다. 그 다음 paramMap의 생성부터 ModelView를 반환하는 것은 이전과 같으니 넘어가겠습니다. 다만 ControllerV3는 원래 ModelView를 반환했기때문에 이렇게 로직이 간단하단 것만 기억하면 됩니다.
바로 전과 달리 이번엔 ModelView가 아닌 String을 반환하는 ControllerV3CloneHandlerAdapter를 작성해보겠습니다.
public class ControllerV3CloneHandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3Clone);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
ControllerV3Clone controller = (ControllerV3Clone) 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) {
/* ..동일.. */
}
핸들러를 형 변환하는 로직까진 이전과 동일하지만 ControllerV3Clone은 model을 추가 인자로 받으며 viewName을 반환하는 핸들러이므로 어댑터가 반환해야할 ModelView과 일치하지 않습니다. 따라서 handler() 메소드에는 viewName을 받아 ModelView를 반환하는 로직 하나를 넣어 해결해주며 이와 같이 다른 어댑터들도 return 값을 맞춰주는 로직을 추가하여 진짜 어댑터를 바꿔끼며 전자제품을 켜듯 코드를 작성해줍니다.
이제 어댑터를 만들었으니 프론트컨트롤러가 어댑터를 끼는 로직을 추가해줍니다. 해당 코드는 약간 기니 조금씩 나눠서 설명하겠습니다.
// FrontControllerServletV4 - part 1
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/frontcontroller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV4() {
initHandlerMappingMap();
initHandlerAdapters();
}
일단 프론트컨트롤러V4는 필드로 handlerMappingMap와 handlerAdapters를 가집니다. 해당 두 필드는 아래 생성자의 initX() 메소드와 같이 설명하겠습니다.
// FrontControllerServletV4 - part
private void initHandlerMappingMap() {
// v3
handlerMappingMap.put("/front-controller/v4/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v4/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v4/v3/members", new MemberListControllerV3());
// v3 Clone
handlerMappingMap.put("/front-controller/v4/v3c/members/new-form", new MemberFormControllerV3Clone());
handlerMappingMap.put("/front-controller/v4/v3c/members/save", new MemberSaveControllerV3Clone());
handlerMappingMap.put("/front-controller/v4/v3c/members", new MemberListControllerV3Clone());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV3CloneHandlerAdapter());
}
생성자 내의 initX() 메서드는 다음과 같습니다. handlerMappingMap의 경우 기존 ControllerMap과 동일하며 명칭만 변경되었습니다. handlerAdapters의 경우 List이며 프론트컨트롤러에서 사용하는 어댑터 리스트에 만들어둔 V3와 V3Clone 어댑터를 추가되는 initX() 과정을 거칩니다.
// FrontControllerServletV4 - part 3
@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");
}
}
마지막으로 프론트컨트롤러의 service() 메서드입니다. service() 메서드를 살펴보면 아래 정리된 getHandler 메서드로 request에서 handler를 가져옵니다. 가져온 handler가 없으면 예외를 던져주고 만약 있다면 List인 아까 handlerAdapters에서 해당 핸들러의 지원 여부를 판단 한 뒤 지원하는 어댑터가 존재한다면 핸들러에 맞는 어댑터를 반환하고 없다면 마찬가지로 예외를 반환합니다.
그 다음 가장 중요하게 살펴볼 마지막 과정은 전 version과 달리 프론트컨트롤러가 핸들러에게 직접 ModelView 반환을 받는 것이 아니라 어댑터에게 대신 반환받는다는 점입니다. 이것으로 이 프론트컨트롤러는 어댑터만 컨트롤러를 지원한다면 어떤 요청이든 호환이 가능해졌습니다.
이전 포스트에서 프론트컨트롤러를 사용한 MVC 패턴은 MV+C로 기억하면 편하다고 했던 이유를 이제는 아실 것이라 생각합니다. 또한 많은 분들이 눈치채셨겠지만 우리가 이번 포스트에서 다뤘던 FrontController가 다들 많이 접하셨던 DispatcherServlet입니다. 따라서 이번 포스트까지 쭉 읽으셨다면 사용하시는 스프링 기술 흐름이 대충 어떻게 돌아가는지 파악되셨으리라 생각합니다.
지금까지 Version4까지 컨트롤러를 수정해가며 스프링 MVC의 핵심 구조가 어떻게 돌아가는지 알아봤습니다. 여기까지 보셨다면 충분히 지금 사용되는 스프링 웹 MVC를 이해할 수 있습니다! 다음 포스트에선 왜 이번 포스트에서 이런 지루한 리팩토링 과정을 거쳤는지 알려드리겠습니다.
refer
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의
웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -
www.inflearn.com
'Spring' 카테고리의 다른 글
Spring_짧1_ControllerTest와 @ModelAttribute, @RequestBody (0) | 2022.07.18 |
---|---|
5_짧_th:field와 th:value (0) | 2022.06.25 |
Spring_정리4_Spring의 구조 훑어보기_MVC (0) | 2022.05.08 |
Spring_정리3_Spring의 구조 훑어보기_서블릿 (0) | 2022.05.05 |
Spring_정리2_Spring 이전 JAVA 웹 개발의 역사 훑어보기 (0) | 2022.04.29 |