김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
FrontController ➔ DispatcherServlet
handlerMappingMap ➔ HandlerMapping ( 인터페이스 )
MyHandlerAdapter ➔ HandlerAdapter ( 인터페이스 )
ModelView ➔ ModelAndView
viewResolver ( 메서드 ) ➔ viewResolver ( 인터페이스 )
MyView ( 클래스 ) ➔ View ( 인터페이스 )
핸들러 조회 : 요청 URL에 매핑된 핸들러( Controller )를 조회
핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터 조회
핸들러 어댑터 실행
핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행
ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환
viewResolver 호출
JSP의 경우 InternalResourceViewResolver
가 자동 등록되고, 사용된다
템플릿 엔진마다 다른 viewResolver가 호출
viewResolver는 view의 논리 이름을 물리 이름으로 바꾸고 렌더링 역할을 하는 View 객체를 반환
JSP의 경우 InternalResourceView(JstlView)
를 반환하는데, 내부에 forward()
로직이 있다
뷰 렌더링 : 뷰를 통해 뷰를 렌더링
DisPatcherServlet
은 HttpServlet
을 상속 받아서 사용, 서블릿으로 동작
스프링부트는 DispacherServlet
을 서블릿으로 자동으로 등록하면서 모든 경로( urlPatterns="/"
)에 대해서 매핑한다
서블릿이 호출되면 HttpServlet
이 제공하는 service()
가 호출
FrameworkServlet
이 service()
를 오버라이딩함
즉, FrameworkServlet.service()
에서 시작되고 여러 메서드를 거쳐 DispatcherServlet.doDispatch()
가 호출
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
// 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);
}
doDispatch()
request를 이용해 핸들러를 꺼낸다
핸들러를 처리할 수 있는 핸들러 어댑터를 찾아서 반환
핸들러 어댑터의 handle()
을 호출
그러면 핸들러 어댑터가 핸들러를 호출하고 결과를 받음
ModelAndView로 반환
반환된 ModelAndView를 processDispatchResult()
를 호출하면서 넘겨준다
processDispatchResult()
에서 뷰 렌더링 호출 ( render()
)
render()
에서 뷰 리졸버를 통해 view를 찾고 반환 & 뷰 렌더링
핸들러가 호출되기 위해 HandlerMapping, HandlerAdapter가 필요
HandlerMapping( 핸들러 매핑 )
핸들러 매핑에서 해당 컨트롤러를 찾을 수 있어야한다
ex> 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다
HandlerAdapter( 핸들러 어댑터)
핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요
ex> Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야한다
0 순위 : RequestMappingHandlerMapping
@RequestMapping
을 사용하는 어노테이션 기반의 컨트롤러에서 사용
가장 먼저 실행되는 객체
1순위 : BeanNameUrlHandlerMapping
스프링 빈의 이름으로 핸들러를 찾는 객체
URL 이름과 동일한 스프링 빈을 찾는다
0 순위 : RequestMappingHandlerAdapter
@RequestMapping
을 사용하는 어노테이션 기반의 컨트롤러에서 사용1순위 : HttpRequestHandlerAdapter
HttpRequestHandler
를 처리하는 어댑터 ( 객체 )2순위 : SimpleControllerHandlerAdapter
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
}
위에서 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다고 했다
이를 판단하는 기준이 위의 supports()
메서드인데 instanceof
로 인해 Controller 인터페이스를 호출할 수 있는지 여부를 확인할 수 있다
dispatcherServlet 이 핸들러 어댑터의 handle() 메서드를 실행한다
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
과거에 사용했던 컨트롤러
현재는 어노테이션 기반의 컨트롤러를 사용 ( @Controller
)
Controller 인터페이스와 @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 null;
}
}
@Component
/springmvc/old-controller
라는 이름의 스프링 빈으로 등록
빈의 이름으로 URL을 매핑
핸들러 매핑으로 핸들러 조회
HandlerMapping 을 순서대로 실행해서 핸들러를 찾는다
위의 경우 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요
빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping
가 실행에 성공하고 핸들러인 OldController
를 반환
핸들러 어댑터 조회
핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다
HandlerAdapter 의 supports()
를 순서대로 호출하여 Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾는다
위의 경우 Controller 인터페이스를 지원하는 SimpleControllerHandlerAdapter
가 찾아진다
핸들러 어댑터 실행
DispatcherServlet이 조회한 SimpleControllerHandlerAdapter
를 실행하면서 핸들러 정보도 함께 넘겨준다
SimpleControllerHandlerAdapter
는 핸들러인 OldController 를 내부에서 실행하고, 결과를 반환
public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("MyHttpRequestHandler.handleRequest");
}
}
BeanNameUrlHandlerMapping
와 HttpRequestHandlerAdapter
객체를 사용하여 실행된다@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");
}
}
ModelAndView 에 view 의 논리 이름을 넣고 반환하면 white label page 가 출력된다
view 를 찾지 못해서 white label page 가 출력된 것인데 view 를 찾지 못했다는 것은 viewResolver 가 작동하지 않았다는 것이다
즉, viewResolver 는 Controller 가 반환한 view 의 논리 이름을 가지고 view 를 찾는 기능을 하며 이를 위해서 viewResolver 를 만들어야한다
스프링에서는 여러가지 viewResolver 가 등록되어 있다
1순위 : BeanNameViewResolver
2순위 : InternalResourceViewResolver
< application.properties >
spring.mvc.view.prefix=/WEB-INF/views
spring.mvc.view.suffix=.jsp
InternalResourceViewResolver
가 자동으로 등록될 때 application.properties
에 등록한 설정 정보를 사용
spring.mvc.view.prefix
spring.mvc.view.suffix
@Bean
InternalResourceViewResolver internalResourceViewResolver() {
retrn new InternalResourceViewResolver("/WEB-INF/views", ".jsp");
}
핸들러 어댑터 호출
viewResolver 호출
new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출
BeanNameViewResolver
는 new-form
이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없기 때문에 다음으로 넘어감
InternalResourceViewResolver
가 호출 ( 2순위 )
InternalResourceViewResolver
InternalResourceViewResolver가
InternalResourceView를
반환view - InternalResourceView
InternalResourceView
는 JSP처럼 포워드 forward()
를 호출해서 처리할 수 있는 경우에 사용view.render()
view.render()
가 호출되고 InternalResourceView
는 forward()
를 사용해서 JSP를 실행DispatcherServlet은 viewResolver 목록을 가지고 있고, 핸들러 어댑터를 통해 반환된 논리적인 뷰 이름을 가지고, viewResolver 목록을 순회하며 view를 생성을 시도
다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 forward()
통해서 해당 JSP로 이동(실행)해야 렌더링이 된다 ( html로 된 화면이 그려진다 )
JSP를 제외한 나머지 뷰 템플릿들은 forward()
과정 없이 바로 렌더링
@RequestMapping
@RequestMapping
어노테이션을 사용하는 컨트롤러
RequestMappingHandlerMapping
: 핸들러 매핑 ( 컨트롤러를 찾는다 )
RequestMappingHandlerAdapter
: 핸들러 어댑터 ( 컨트롤러를 처리할 수 있는 어댑터를 찾는다 )
@RequestMapping
과 @Controller
@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 mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
RequestMappingHandlerMapping
스프링 빈 중에서 @RequestMapping
또는 @Controller
가 클래스 레벨에 붙어 있는 경우에 핸들러 매핑 정보로 인식
스프링 부트 3.0(스프링 프레임워크 6.0)부터는 클래스 레벨에 @RequestMapping
이 있어도 스프링 컨트롤러로 인식하지 않는다
오직 @Controller
가 있어야 스프링 컨트롤러로 인식한다
@RestController
는 해당 애노테이션 내부에 @Controller
를 포함하고 있으므로 인식 된다 @Controller
스프링이 자동으로 스프링 빈으로 등록 (내부에 @Component
애노테이션이 있어서 컴포넌트 스캔의 대상이 됨)
스프링 MVC에서 어노테이션 기반 컨트롤러로 인식
@Controller
대신 @Component
+ @RequestMapping
를 사용해도 됨
@RequestMapping
요청 정보를 매핑한다
메소드 단위이기 때문에 하나의 Controller에 메소드를 여러 개 넣을 수 있음
URL이 중복되기 때문에 클래스 레벨에 @RequestMapping
어노테이션을 붙여서 중복 제거 가능
@RequestMapping
과 메서드 레벨의 @RequestMapping
가 합쳐져서 매핑이 된다ModelAndView
모델과 뷰 정보를 담아서 반환
view의 논리 이름으로 ModelAndView 객체 생성
addObject()
로 model에 정보 추가
@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";
}
}
@RequestMapping
개선
@RequestMapping
을 가지고 있는 @GetMapping
, @PostMapping
을 사용해서 요청 정보를 매핑할 수 있다반환형 개선
ModelAndView
를 반환하는 것이 아니라 view 이름( String )으로 반환해도 된다
스프링이 반환되는 String을 view 이름으로 인식하고 진행
HttpServletRequest
개선
@RequestParam
으로 요청 파라미터를 직접 받을 수 있다
@RequestParam("username")
가
request.getParameter("username")
의 기능을 함
Model 개선
ModelAndView에 데이터를 담는게 아니라 Model
을 사용해서 데이터를 담을 수 있음
model.addAttribute()
로 추가 가능