
스프링 MVC의 전체 구조는 위의 사진과 같고 이 글에서 하나하나 알아 볼 예정이다!
DispacherServlet란 스프링 MVC의 프론트 컨트롤러이며 스프링 MVC의 핵심이다.

DispacherServlet은 FrameworkServlet을 상속받고 FrameworkServlet은 서블릿을 만들기 위해서 상속받는 HttpServlet을 상속받는다.
스프링 부트는 DispacherServlet을 서블릿으로 자동으로 등록하면서 모든 경로( urlPatterns="/" )에 대해서 매핑한다.
무조건 DispacherServlet을사용하게 되는 것은 아니고, 더 자세한 경로가 우선순위가 높다.
그래서 기존에 등록한 서블릿도 함께 동작한다.
서블릿이 호출되면 HttpServlet 이 제공하는 serivce()메서드가 호출된다.
스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()메서드를 오버라이드 해두었다.
FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면 DispacherServlet.doDispatch()가 호출되는 것이다.
지금부터 DispacherServlet 의 핵심인 doDispatch()코드(최대한 간단히 설명하기 위해 예외처리, 인터셉터 기능은 제외한)를 분석해보자.
protected void doDispatch(HttpServletRequest request, HttpServletResponse
response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
ModelAndView mv = null;
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv,
dispatchException);
}
private void processDispatchResult(HttpServletRequest request,
HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView
mv, Exception exception) throws Exception {
// 뷰 렌더링 호출
render(mv, request, response);
}
protected void render(ModelAndView mv, HttpServletRequest request,
HttpServletResponse response) throws Exception {
View view;
String viewName = mv.getViewName();
// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);
}
스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이, 위에서 설명한 대부분의 기능들을 인터페이스로 원하는 기능을 변경하거나 확장할 수 있다는 점이다.
이 인터페이스들만 구현해서 DispatcherServlet 에 등록하면 커스텀 컨트롤러를 만들 수도 있다.
핸들러 매핑: org.springframework.web.servlet.HandlerMapping
핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter
뷰 리졸버: org.springframework.web.servlet.ViewResolver
뷰: org.springframework.web.servlet.View
스프링 MVC는 코드 분량도 매우 많고, 복잡해서 내부 구조를 다 파악하는 것은 쉽지 않다.
사실 해당 기능을 직접 확장하거나 나만의 컨트롤러를 만드는 일은 없으므로 걱정하지 않아도 된다.
컨트롤러가 호출되려면 다음 2가지가 필요하다.
HandlerMapping(핸들러 매핑)
핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.
예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
HandlerAdapter(핸들러 어댑터)
핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
예) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다.
개발자가 직접 핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다.
많지만 여기서는 간략하게 몇개만 설명하겠다.
0순위 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용한다.
(@RequestMapping란 요청 URL을 어떤 method가 처리할지 mapping해주는 Annotation이다)
1순위 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
(@Component("/springmvc/request-handler")를 붙인 핸들러를 만들면 스프링부트에서 자동으로 저 url을 bean으로 등록한다.)
사용할 핸들러 매핑으로 핸들러는 조회할 때, HanderMapping을 순서대로 실행해서 우선순위가 높은 순서로 핸들러를 찾는다.
0순위 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용한다.
1순위 = HttpRequestHandlerAdapter : HttpRequestHandler 처리한다.
2순위 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용)처리한다.
사용할 핸들러 어댑터를 조회할 때, HandlerAdapter 의 supports() 를 순서대로 호출한다.
가장 우선순위가 높은 핸들러 어댑터를 실행하면서 핸들러의 정보도 함께 넘겨주어서 핸들러 어댑터 내부에서 핸들러를 실행하고 그 결과를 반환한다.
가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter 이다.
@RequestMapping의 앞글자를 따서 만든 이름인데, 이것이 바로 지금 스프링에서 주로 사용하는
애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다.
실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return new ModelAndView("new-form");
}
}
위와 같이 컨트롤러를 만들고 뷰를 사용하기 위해 return new ModelAndView("new-form"); 코드를 추가했을 때, http://localhost:8080/springmvc/old-controller로 들어가면 Whitelabel Error Page가 나온다.
스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록하는데, 이때
application.properties 에 등록한 spring.mvc.view.prefix , spring.mvc.view.suffix 정 정보를 사용해서 등록하면 원하는 view가 나온다.
1순위 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성
기능에 사용)
2순위 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
지금까지 배운 내용을 바탕과 저번에 만들었던 new-form.jsp파일을 이용해서 회원 등록 폼을 만들어보자
@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/memers/new-form")
public ModelAndView process(){
return new ModelAndView("new-form");
}
}
@Controller은 2가지의 기능을 한다.
1. @Controller내부에는 @Component가 있어서 자동으로 스프링 빈으로 등록한다.
2. RequestMappingHandlerMapping은 스프링 빈중에서 @RequestMapping이나 @Controller가 클래스 레벨에 붙어 있는 경우 매핑 정보로 인식한다.
그러므로 @Controller대신 @Component와 @RequestMapping를 클래스 레벨에 붙여도 된다.
@RequestMapping은 요청 정보를 매핑해서 해당 URL이 호출되면 이 메서드가 호출된다.
애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.
ModelAndView는 모델과 뷰 정보를 담아서 반환한다.
@Controller
public class SpringMemberSaveControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
@RequestMapping을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다.
따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
클래스 단위로 만들었던 컨트롤러를 매서드 단위로 통합하여 하나의 컨트롤러로 만들어보자.
@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/new-form")
public ModelAndView newForm() {
return new ModelAndView("new-form");
}
@RequestMapping("/save")
public ModelAndView save(HttpServletRequest request, HttpServletResponse
response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mav = new ModelAndView("save-result");
mav.addObject("member", member);
return mav;
}
@RequestMapping
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mav = new ModelAndView("members");
mav.addObject("members", members);
return mav;
}
}
일부 겹치는 url("/springmvc/v2/members")을 클래스 단위로 처리하고 덧붙여지는 url을 메서드 단위로 처리하여 컨트롤러를 통합했다.
스프링 MVC는 개발자가 편리하게 개발할 수 있도록 수 많은 편의 기능을 제공한다.
실무에서는 지금부터 설명하는 방식을 주로 사용한다.
위의 회원 등록 폼의 뷰를 반환하는 newForm메서드에서 뷰의 논리 이름을 직접 반환할 수 있다.
@RequestMapping("/new-form")
public String newForm() {
return "new-form";
}
위의 회원 저장 폼의 뷰를 반환하는 save메서드에서 HttpServletRequest객체와 HttpServletResponse객체를 매개변수로 사용해서 메서드 내에서 파라미터를 받아오는 대신 처음부터 파라미터를 매개변수로 받아올 수 있다.
@RequestParam이라는 어노테이션을 사용해서 자료형도 설정이 가능하다.
그리고 ModelAndView를 반환하는 대신 매개변수에 Model을 넣어서
@RequestMapping("/save")
public String save(
@RequestParam("username") String username,
@RequestParam("age") int age,
Model model
) {
Member member = new Member(username, age);
memberRepository.save(member);
model.addAttribute("member",member);
return "save-result";
}
그리고 ModelAndView를 반환하는 대신 매개변수에 Model을 넣어서 model에 attribute를 추가해준다.
@RequestMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("member",members);
return "members";
}
@RequestMapping 은 URL만 매칭하는 것이 아니라, HTTP Method도 함께 구분할 수 있다.
예를 들어서 URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이 처리하면 된다.
이것을 @GetMapping , @PostMapping 으로 더 편리하게 사용할 수 있다.
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@GetMapping("/new-form")
public String newForm() {
return "new-form";
}
@PostMapping("/save")
public String save(
@RequestParam("username") String username,
@RequestParam("age") int age,
Model model
) {
Member member = new Member(username, age);
memberRepository.save(member);
model.addAttribute("member",member);
return "save-result";
}
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("member",members);
return "members";
}
}