이 글은 김영한 강사님의 강의를 참고하여 작성하였습니다.
이제 그동안 배웠던 MVC 프레임워크를 기반으로 더 나은 스프링에서 제공해주는 스프링 MVC에 대한 구조를 배워보자.
이전까지 우리가 만들었던 스프링 MVC는 몇몇의 이름만 조금 달라졌다고 보면 된다!
생각보다 명칭 외에는 달라지는 것이 없다.
그렇다면 크게 차이가 있을지 세세히 들어가보자
A. 맞다 매우 맞다. 그 이름이 '디스패치 서블릿'으로 바꼈다고 생각해도 무관하다!
DispacherServlet
도 부모 클래스에서HttpServlet
을 상속 받아서 사용하고, 서블릿으로 동작한다.
DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet- 스프링 부트는
DispacherServlet
을 ¹서블릿으로 자동으로 등록하면서 ²모든 경로( urlPatterns="/" )에 대해서 매핑한다.
참고:더 자세한 경로가 우선순위가 높다
=> 기존에 등록한 서블릿도 함께 동작
- '스프링 MVC는'
DispacherServlet
의 부모인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);
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
호출InternalResourceViewResolver
가 자동 등록 및 사용됨)InternalResourceView(JstlView)
를 반환하는데, 내부에 forward()
로직이 있다.)스프링 MVC는 분량도 코드도 많고 복잡하다. 그러나 실제 이 기능들을 직접 확장하고 나만의 컨트롤러를 만들 일은 없다 걱정 말자! 👍
=> 이미 필요로 하는 대부분의 기능이 다 구현되어 있다.
향후 이런 핵심 동작들을 알아두어야 문제의 원인 파악을 할 수 있고 , 확장이 필요할 때 어떤 부분을 확장하면 좋을지 감을 잡을 수 있다!
그저 너무 어려워 하지 않고 전체적인 구조를 파악하자
'핸들러 매핑'과 '핸들러 어댑터'가 어떤 것들이 어떻게 사용되는지 알아보자.
지금은 전혀 사용하지 않지만, 과거에 주로 사용했던 스프링이 제공하는 '간단한 컨트롤러'로 핸들러 매핑과 어댑터를 이해해보자.
과거 버전 스프링 컨트롤러
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse
response) throws Exception;
}
Controller 인터페이스는
@Controller
애노테이션과는전혀 다르다.
[OldController]
@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을 매핑할 것 이다
컨트롤러가 호출되려면 다음의 2가지 모두 필요하다.
HandlerMapping(핸들러 매핑)
핸들러 매핑에서 이 '컨트롤러'가 있는지 찾아서 존재해야 함.
예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요
HandlerAdapter(핸들러 어댑터)
핸들러 매핑을 통해서 찾은 '이 핸들러(컨트롤러)'를 실행할 수 있는 핸들러 어댑터가 필요하니 찾아준다.
예) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
그런데 걱정말자 우리 스프링은 똑똑해서 이미 필요한 대부분의 '핸들러 매핑' & '핸들러 어댑터'를 구현해두었다~! 👍😎
-> 개발자가 손수 만드는 일은 거의 없다.
그렇다면 방금까지 진행하던 OldController는 어떤 흐름으로 진행되었을까? 🤷♂️
OldController 를 실행하면서 사용된 객체는 다음과 같다.
'핸들러 매핑'으로 핸들러 조회
1-1. HandlerMapping
을 순서대로 실행해서, 핸들러를 찾는다.
1-2. 이 경우 '빈 이름으로 핸들러를 찾아야 하기 때문에'
-> 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping
가 실행에 성공하고, 핸들러인 OldController 를 반환한다.
핸들러 어댑터 조회
2-1. HandlerAdapter 의 supports()
를 순서대로 호출한다.
2-2. SimpleControllerHandlerAdapter 가 Controller
인터페이스를 지원하므로 대상이 된다.
핸들러 어댑터 실행
3-1. 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter
(어댑터)를 실행하면서 해당 핸들러 정보도 함께 넘겨준다.
3-2. SimpleControllerHandlerAdapter
는 핸들러인 OldController 를 내부에서 실행하고, 그 결과를
반환한다
- 가장 우선순위가 높다.
핸들러 매핑 :RequestMappingHandlerMapping
핸들러 어댑터 :RequestMappingHandlerAdapter
이다.- 이것이 바로 지금 스프링에서 주로 사용하는
애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다.
=> 실무에서는 99.9% 이 방식의 컨트롤러를 사용
A. 맞다 매우 맞다. 그 이름이 '디스패치 서블릿'으로 바꼈다고 생각해도 무관하다!
@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"); //null 이 아니네??
}
}
- 이제 View를 사용할 수 있도록 null이 아닌 '논리적 이름'을 넣었다.
그러니 aplication.properties
에 이 코드를 추가해줘야 한다~
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
- 스프링 부트는
InternalResourceViewResolver
라는 '뷰 리졸버'를 자동 등록 해준다 대신...
-> 저기 저application.properties
의 저 설정 정보를 통해 '등록' 된다!
이 사진은 스프링 부트가 '자동 등록'해주는 '뷰 리졸버' 일부이다.
new-form
이라는 논리 뷰 이름을 획득한다.new-form
이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.BeanNameViewResolver
는 new-form
이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다?InternalResourceViewResolver
가 호출된다.InternalResourceView
를 반환⭐InternalResourceView
는 JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용한다.view.render()
가 호출되고 , InternalResourceView
(뷰) 는 forward() 를 사용해서 JSP를 실행한다.JSP의 경우 forward()
로 해당 JSP로 이동(실행) 해야 렌더링 된다.
-> JSP 외의 '뷰 템플릿'은 forward()
없이 바로 렌더링 된다.
Thyeleaf 뷰 템플릿은 최근에는 '라이브러리'만 추가해도 이런 작업들을 모두 스프링 부트가 자동화를 해준다.
-> 원래는 ThymeleafViewResolver
를 등록해야 되지만
RequestMaapingHandlerMapping
RequestMappingHandlerAdapter
을 지원한다.@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
원래라면 비즈니스 로직을 담는 '서비스'라는 계층이 따로 있음.
- @Controller
- 내부에 @Component 가 있어서 스프링이 자동으로 '스프링 빈'으로 등록
=> 이걸 통해 스프링MVC에서는 '애노테이션 기반 컨트롤러'를 인식한다.- @RequestMapping
- 요청 정보를 매핑
-> 해당 url이 호출된다? => 이 애노테이션이 있는 메서드가 호출.- ModelAndView
- 모델과 뷰 정보를 담아서 반환한다.
@Component //컴포넌트 스캔을 통해 스프링 빈으로 등록
@RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
@Component로 스프링 빈 등록 , @RequestMapping 을 같이 '클래스'단에 있으면 아 넌 내가 처리할 수 있는 핸들러구만! 하고 인식
@RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
//스프링 빈 직접 등록
@Bean
SpringMemberFormControllerV1 springMemberFormControllerV1() {
return new SpringMemberFormControllerV1();
}
이렇게 다양하게 있다. 그러나 제일 편한건 @Controller를 클래스 단에 넣어주면 제일 편할 것이다~
@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;
}
}
mv.addObject("member", member)
addObject()
를 사용하면 된다.아까 앞에서 봤던 것처럼 @RequestMapping은 클래스 단위에서도 쓰고 메서드 단위에서도 쓴다.
=> 아 유연하게 사용이 가능하구나 😎😮👍
/**
* 클래스 단위 -> 메서드 단위
* @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"
이 중복적이게 되기 때문에 조합을 써봤다. 저 부분이 반복되기 때문에
클래스 레벨 @RequestMapping("/springmvc/v2/members")
메서드 레벨 @RequestMapping("/new-form")
-> /springmvc/v2/members/new-form
으로 간소화
메서드 레벨 @RequestMapping("/save")
-> /springmvc/v2/members/save
메서드 레벨이 클래스 레벨가 같은 경우..
@RequestMapping
만 url은 더 안 쓴다. -> /springmvc/v2/members
이런 식으로 인식 됨!
/**
* v3
* Model 도입
* ViewName 직접 반환
* @RequestParam 사용
* @RequestMapping -> @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("members", members);
return "members";
}
}
1. Model 파라미터
• save()
, members()
를 보면 Model을 파라미터로 받는다.
2. ViewName 직접 반환
• 뷰의 논리 이름을 반환할 수 있다.
-> 원래 ModelView 타입으로 반환해주어도 되지만, 편하게 String 타입으로 ViewName을 직접 반환해줘도 된다.
=> 스프링은 String으로 반환된 것을 보고 "아하 뷰 이름이구나" 하고 인식해준다.
3. @RequestParam 사용
• 스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있다.
• @RequestParam("username")
은 request.getParameter("username")
와 거의 같은 코드라 생각하자
• GET 쿼리 파라미터, POST Form 방식을 모두 지원
Get, Post, Put, Delete, Patch 모두 애노테이션이 존재.
ex) 조건 : URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면
이다. 그러나 이것을 보다 더욱 더 편하게 만들 수 있는 방법이 있다.
@GetMapping("/new-form")
으로 더 간추릴 수 있다.
ex)@GetMapping
,@PostMapping
,@PutMapping
....
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new
MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new
MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new
MemberListControllerV3());
//V4 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter()); //V4 추가
}
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) {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
//어댑터의 기능: ControllerV4의 String을 -> ModelView반환으로 만들어줌
ModelView mv = new ModelView(viewName);
mv.setModel(model);
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;
}
}
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
ControllerV4의 구현체인 handler만 처리해주는 어댑터를 조회.
-> 한 마디로 핸들러를 처리할 수 있는 어댑터를 조회하는 곳이다.
ControllerV4 controller = (ControllerV4) 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;
[제약] but 이곳에서는 ModelView로 반환해야 함
ControllerV4
는 String 타입의 '뷰 이름'을 반환.