김영한 님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술'을 듣고 적은 글입니다.

기존에 우리가 만들었던 FrontController 구조와 유사하다.
DispacherServlet은 부모 클래스에서 HttpServlet을 상속받아 사용한다.
DispacherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
스프링 부트는 DispacherServlet을 서블릿으로 자동 등록하면서 모든 경로에 urlPatterns = "/" 에 대해 매핑한다.
<요청 흐름>
HttpServlt이 제공하는 service()가 호출된다는 것을 잊지말자HttpServlet의 service() 호출. 이때 FrameworkServlet에서 오버라이드 해놓았기에 FrameworkServlet의 service() 호출됨.FrameworkServlet.service()를 시작으로 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);
// Spring Framework에서 사용되는 메서드.클라이언트 요청에 대한 처리기(Handler)를 찾을 수 없는 경우 호출.
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);
}
ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환ViewResolver 호출 View 반환 : 논리 이름을 물리 이름으로 변환. 렌더링 역할을 하는 뷰 객체 반환지금은 애노테이션 기반 스프링 컨트롤러를 사용하지만 예전에는 Controller 인터페이스를 사용했다. Controller인터페이스와 @Controller는 완전히 다르다.
org.springframework.web.servlet.mvc.Controller
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
이 인터페이스를 호출하려면 어떻게 해야할까?
HandlerMapping과 HandlerAdapter가 필요하다.

0번에 해당하지 않으면 1번을 적용한다.

위의 방식과 동일하다.
예시)
package hello.servlet.web.springmvc.old;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
@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");
}
}
❶ 핸들러 매핑으로 핸들러 조회
HandlerMapping을 순서대로 실행해 핸들러 찾음BeanNameUrlHandlerMapping이 실행에 성공. 핸들러인 OldController 반환❷ 핸들러 어댑터 조회
HandlerAdapter의 supports() 순서대로 호출SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하기에 대상이 됨❸ 핸들러 어댑터 실행
SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨줌SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행하고 그 결과 반환결론
HandlerMapping = BeanNameUrlHandlerMapping
HandlerAdapter = SimpleControllerHandlerAdapter
스프링 부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동 등록한다. 이때, application.properties에 동록한 spring.mvc.view.prefix, spring.mvc.view.suffix 설정 정보를 사용해 등록한다.

InternalResourceViewResolver는 InternalResourceView를 반환한다. 이는 JSP처럼 forward()를 호출해 처리할 수 있는 경우에 사용한다.
InternalResourceViewResolver 는 만약 JSTL 라이브러리가 있으면 InternalResourceView 를 상속받은 JstlView 를 반환한다. JstlView 는 JSTL 태그 사용시 약간의 부가 기능이 추가된다.
다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 forward() 통해서 해당 JSP로 이동(실행)해야 렌더링이 된다. JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링된다.
가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter 이다.
RequestMappingHandlerMapping은 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.
package hello.servlet.web.springmvc.v1;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
/*
@Component // 덕분에 스프링 빈으로 등록
@RequestMapping
*/
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process(){
System.out.println("SpringMemberFormControllerV1.process");
return new ModelAndView("new-form");
}
}
이런 방식으로 회원 목록과 저장 클래스들도 만들어준다. 이때 ModelView로 반환하면 안된다. 꼭 ModelAndView로 바꿔주자.
하지만 이런 방식으로 하면 @RequestMapping이 클래스 단위가 아닌 메서드 단위로 붙은 것을 알 수 있다. 따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
package hello.servlet.web.springmvc.v2;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
/**
* 클래스 단위 -> 메서드 단위
* @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;
}
}
이방식도 좋지만, 항상 모델뷰 객체를 생성해서 넘겨줘야하는 등의 번거로움이 존재한다. 더 코드를 줄여보자 !
package hello.servlet.web.springmvc.v3;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
//@RequestMapping(value = "/new-form", method = RequestMethod.GET)//get인 경우에만 호출
@GetMapping("/new-form")
public String newForm() {
return "new-form";
}
//@RequestMapping(value = "/save", method = RequestMethod.POST) //저장은 get이 아닌 Post가 맞음
@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";
}
//@RequestMapping(method = RequestMethod.GET)
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
코드가 더욱더 간결해지고 깔끔해졌다.
그리고 우리는 여태 GET, POST 방식을 구분하지 않았다. 그렇기에 GET으로 보내든 POST로 보내든 결과는 출력된다. 하지만 각 메서드의 속성에 맞게 방식에 제한을 두는게 더 좋을 것이다. 그래서 우리는 @RequestMapping 혹은 @GetMapping, @PostMapping을 사용해 제한을 두었다.
원래는 @RequestMapping의 method 속성을 이용해 GET, POST 등을 지정해줬지만, 더 편리하게 사용하기 위해 @GetMapping 같은 친구들이 등장하게 되었다. @GetMapping이나 @PostMapping의 내부를 열어보면 @RequestMapping이 들어있는 것을 확인할 수 있다.