강의를 들으며 생각 정리
지금까지 MVC 패턴을 점진적으로 개선했다. 그리고 최종적으로 개선된 패턴은 스프링 MVC와 구조가 매우 유사하다. 배운 내용을 바탕으로 스프링 MVC의 구조를 알아보자.
스프링 MVC 전체 구조
직접 만든 MVC 프레임워크와 스프링 MVC를 비교해보자.
구조는 거의 완벽히 유사하다. 세부적인 부분에 차이가 있다면,
직접 만든 MVC -> 스프링 MVC
FrontController -> DispatcherServlet (프론트 컨트롤러)
handlerMappingMap -> HandlerAdapter (핸들러 매핑 정보)
MyHandlerAdapter -> HandlerAdapter (핸들러 어댑터 목록)
ModelView -> ModelAndView (모델뷰)
MyView -> View (뷰)
직접 만든 구조들이 -> 스프링 MVC에서는 이렇게 지원한다.
<DispatcherServlet 구조>
스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있다. 스프링 MVC의 프론트 컨트롤러인 DispatcherServlet이 바로 스프링 MVC의 핵심이다.
- 서블릿 등록
디스패처 서블릿도 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작한다.
(상속 구조를 따라가다 보면 HttpServlet이 보인다.)
스프링 부트는 톰캣 서버에 디스패처 서블릿을 자동 등록하면서 모든 경로(urlPatterns="/")에 대해서 매핑한다.
+) 참고 : 더 자세한 경로가 우선순위가 높기 때문에 기존에 등록한 서블릿도 함께 동작한다.
- 요청 흐름
서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다. 스프링 MVC는 디스패처 서블릿의 부모인 FrameworkServlet에서 service()를 오버라이드하고 이 메서드를 시작으로 여러 메서드가 호출되면서 결국 DispatcherServlet().doDispatch()가 호출된다.
-> 바로 이 doDispatch() 메서드가 디스패처 서블릿의 핵심이다.
<DispatcherServlet().doDispatch() -> 일부>
...
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 핸들러 어댑터 조회
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
...
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
...
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable 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);
...
}
...
기존에 만들었던 v5 프론트컨트롤러의 service() 메서드와 매우 유사한 구조를 띄고 있다.
<동작 순서>
- 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
- 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
- 핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다.
- 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다.
- ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
- viewResolver 호출 : 뷰 리졸버를 찾고 실행한다.
- View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
- 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링한다.
기존에 프론트 컨트롤러를 잘 이해했다면 위 과정을 무리 없이 이해할 수 있을 것이다.
- 인터페이스 살펴보기
스프링 MVC의 큰 강점은 디스패처 서블릿 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다는 점이다. 예를 들어, 스프링은 어떤 형식의 뷰를 사용할지 모르기 때문에(JSP or Thymeleaf) ViewResolver와 View 모두 인터페이스로 제공한다. 이 밖에 HandlerMapping, HandlerAdapter 모두 인터페이스로 제공한다.
+) 정리
이처럼 스프링은 웹 애플리케이션을 만들 때 필요로 하는 대부분의 기능이 이미 다 구현되어 있다. 그래도 이렇게 핵심 동작방식을 알아두어야 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 쉽게 파악하고, 문제를 해결할 수 있다. 또한 어떤 부분을 확장해야 할지 감을 잡을 수도 있다.
핸들러 매핑과 핸들러 어댑터
핸들러 매핑과 핸들러 어댑터를 이해해보자.
지금은 전혀 사용하지 않지만, 과거에 주로 사용했던 스프링이 제공하는 컨트롤러로 핸들러 매핑과 핸들러 어댑터가 어떻게 사용되었는지 보자.
<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;
}
}
+) 참고로 @Controller과 다른 라이브러리를 import 해야 한다.
-> import org.springframework.web.servlet.mvc.Controller
@Component("name") : 이 컨트롤러는 "name"이라는 이름의 스프링 빈으로 등록되었다. 여기서는 URL 경로를 빈의 이름으로 정했다.
이제 localhost:8080/springmvc/old-controller를 호출하면 출력문이 화면에 나타난다.
<이 컨트롤러는 어떻게 호출될 수 있을까?>
컨트롤러가 호출되려면 2가지가 필요하다.
1. 핸들러 매핑 : 핸들러 매핑에서 해당 컨트롤러를 찾을 수 있어야 한다.
2. 핸들러 어댑터 : 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
여러 핸들러 매핑과 핸들러 어댑터를 스프링이 이미 대부분 구현해두었기 때문에 개발자가 직접 만드는 일은 거의 없다.
<스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터 - 일부>
- HandlerMapping
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
- HandlerAdapter
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
숫자가 작을수록 우선순위가 높다. 핸들러 매핑, 핸들러 어댑터 모두 우선순위 순서대로 찾고 만약 없으면 다음 순서로 넘어간다.
다시 본론으로 넘어가서 과연 Controller 인터페이스는 어떻게 컨트롤러를 호출할 수 있을까?
- 핸들러 매핑 : 현재 @RequestMapping 기반을 사용하지 않기 때문에 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerAdapter이 실행에 성공하고 핸들러인 OldController를 반환한다.
- 핸들러 어댑터 : 현재 @RequestMapping과 HttpRequestHandler를 사용하지 않기 때문에 SimpleControllerHandlerAdapter가 Controller를 지원하므로 대상이 된다. (supports() 메서드로 찾게 된다)
- 핸들러 어댑터 실행 : 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter을 통해 OldController를 실행하고 그 결과를 반환한다.
정리 - OldController 핸들러 매핑, 어댑터
HandlerMapping = BeanNameUrlHandlerMapping
HandlerAdapter = SimpleControllerHandlerAdapter
<HttpRequest>
이번에는 Controller 인터페이스가 아닌 다른 핸들러를 알아보자. HttpRequestHandler 핸들러는 서블릿과 가장 유사한 형태의 핸들러이다.
<MyHttpRequestHandler>
@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");
}
}
localhost:8080/springmvc/request-handler를 접속하면 핸들러를 호출할 수 있다.
HttpRequestHandler는 어떻게 컨트롤러를 호출할 수 있을까?
- 핸들러 매핑 : 역시 현재 @RequestMapping 기반을 사용하지 않고 빈 이름으로 핸들러를 찾아야 하기 때문에 BeanNameUrlHandlerAdapter이 실행에 성공하고 핸들러인 MyHttpRequestHandler를 반환한다.
- 핸들러 어댑터 : @RequestMapping은 지원하지 않지만 다음 우선순위인 HttpRequestHandlerAdapter가 HttpRequestHandler를 지원하므로 대상이 된다.
- 핸들러 어댑터 실행 : 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter를 통해 MyHttpRequestHandler를 실행하고 그 결과를 반환한다.
정리 - MyHttpRequestHandler핸들러 매핑, 어댑터
HandlerMapping = BeanNameUrlHandlerAdapter
HandlerAdapter = HttpRequestHandlerAdapter
+) @RequestMapping
지금까지 핸들러 매핑과 어댑터의 구조적으로 어떻게 호출되는지 알기 위해 과거의 방식을 알아봤지만, 사실 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter이다. 모두 @RequestMapping이 지원하는 방식인데 이것이 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다. 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
뷰 리졸버
이번에는 뷰 리졸버에 대해서 자세히 알아보자.
<OldController - ModelAndView를 반환하도록 변경>
@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");
}
}
실행해보면 컨트롤러를 정상 호출하지만, Whiterlabel Error Page 오류가 발생한다. 즉, 화면에 출력할 파일이 없는 것이다.
<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 설정 정보를 사용해서 등록한다.
<결과>
이제 등록 폼이 정상 출력되는 것을 확인할 수 있다.
<스프링 부트가 자동 등록하는 뷰 리졸버 - 일부>
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
OldController는 어떤 뷰 리졸버를 호출할까?
- 핸들러 어댑터 호출 : 핸들러 어댑터를 통해 "new-form"이라는 논리 뷰 이름을 획득한다.
- 뷰리졸버 호출 : new-form이라는 뷰 이름으로 뷰 리졸버를 순서대로 호출한다.
- BeanNameViewResolver는 new-form이라는 이름의 스프링 빈을 찾아야 하는데 없다.
- InternalResourceViewResolver가 호출된다.
- InternalResourceViewResolver : 이 뷰 리졸버는 InternalResourceView를 반환한다.
- InternalResourceView는 JSP처럼 포워드를 호출해서 처리할 수 있는 경우에 사용한다.
- view.render()가 호출되고 InternalResourceView는 포워드를 사용해서 JSP를 호출한다.
+) 참고
다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 포워드를 통해서 JSP로 이동해야 렌더링이 된다. 나머지 뷰 템플릿들은 포워드 과정 없이 바로 렌더링 된다.
타임리프 뷰 템플릿을 사용하면 ThymeleafViewResolver를 등록해야 한다. 최근에는 스프링부트가 이런 작업도 모두 자동화해준다.
지금까지 MVC 프레임워크의 구조에 대해 자세히 알아봤다. 이제 본격적으로 스프링 MVC를 시작해보자.
스프링 MVC - 시작하기
스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서 매우 유연하고 실용적이다.
@RequestMapping
+) 과거에는 스프링 프레임워크가 MVC 부분이 약해서 스프링을 사용하더라도 MVc 기술은 스트럿츠 같은 다른 프레임워크를 사용했었는데 @RequestMapping 기반의 애노테이션 컨트롤러가 등장하면서, MVC 부분도 스프링의 완승으로 끝이 났다.
RequestMappingHandlerMapping - 핸들러 매핑
RequestMappingHandlerAdapter - 핸들러 어댑터
앞서 보았듯이 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 위와 같다. 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원한다. 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
그럼 이제 본격적으로 애노테이션 기반의 컨트롤러를 사용해보자. 지금까지 만들었던 프레임워크에서 사용했던 컨트롤러를 @RequestMapping 기반의 스프링 MVC 컨트롤러로 변경한다.
<SpringMemberFormControllerV1>
@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-from")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
- @Controller : 스프링이 자동으로 스프링 빈으로 등록한다. -> 내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 된다.
- @RequestMapping : 해당 URL이 호출되면 이 메서드가 호출된다. 메서드의 이름은 임의로 지어도 좋다.
- RequestMappingHandlerMapping는 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.
따라서 다음 코드도 동일하게 동작한다.
@Component
@RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-from")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
-> @Component로 스프링 빈으로 등록되고 @RequestMapping이 있기 때문에 RequestMappingHandlerMapping 핸들러 매핑을 가져올 수 있다.
- 혹은 컴포넌트 스캔 없이 스프링 빈으로 직접 등록해도 동작한다.
@RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-from")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
<ServletApplication>
//스프링 빈 직접 등록
@Bean
SpringMemberFormControllerV1 springMemberFormControllerV1() {
return new SpringMemberFormControllerV1();
}
빈은 수동 빈으로 등록하고 @RequestMapping을 통해 핸들러 매핑을 가져온다.
-> 결국 중요한 것은 @RequestMapping!!
@RequestMapping으로 핸들러 매핑과 핸들러 어댑터 모두 가져올 수 있기 때문에 반드시 붙여줘야 한다.
+) @Controller 역시 핸들러 매핑과 핸들러 어댑터 모두 가져올 수 있다. 보통 이 방식을 가장 많이 사용한다.
이제 나머지 코드도 추가해보자.
<SpringMemberSaveControllerV1 - 회원 저장>
@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);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
- ModelAndView는 모델에 데이터를 추가할 때 addObject()를 사용한다.
- @RequestMapping 역시 request, response를 받아 로직을 수행할 수 있다.
<SpringMemberListControllerV1 - 회원 목록>
@Controller
public class SpringMemberListControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members")
public ModelAndView process() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members", members);
return mv;
}
}
스프링 MVC - 컨트롤러 통합
@RequestMapping을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다. 따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
<SpringMemberControllerV2>
@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;
}
@RequestMapping
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members", members);
return mv;
}
}
- 이처럼 하나의 컨트롤러 클래스에 기능별로 여러 메서드들을 추가할 수 있다. 물론 어느정도 연관성이 있는 컨트롤러끼리 묶는 것이 좋다.
- URL에 /spring/v2/members라는 부분에 중복이 있다. 이러한 중복을 제거하기 위해 코드에서 어떤 방식을 사용했는지 확인할 수 있다.
-> 클래스 레벨에 @RequestMapping을 두면 메서드 레벨과 조합이 된다.
스프링 MVC - 실용적인 방식
이전에 MVC 프레임워크를 만들 때 v3는 ModelView를 개발자가 직접 생성해서 반환했기 때문에, 불편했던 기억이 있다. 물론 v4를 만들면서 실용적으로 개선한 기억도 있다.
스프링 MVC는 개발자가 편리하게 개발할 수 있도록 수 많은 편의 기능을 제공한다. 여기서는 이전에 v3->v4 단계를 스프링 MVC에 적용해 볼 것이다.
-> 실무에서는 지금 이 방식을 주로 사용한다.
<SpringMemberControllerV3>
@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";
}
}
- Model 파라미터 & ViewName 직접 반환
v4에서 연습했던 것처럼 컨트롤러에서 ModelAndView를 직접 생성하지 않고 모델을 파라미터로 전달함으로써 뷰의 논리적 이름만을 String으로 반환하는 방식을 사용했다.
- @RequestParam 사용
스프링은 HTTP 요청 파라미터를 @RequestParam으로 받을 수 있다.
@RequestParam("username")은 request.getParameter("username")와 거의 같은 코드라 생각하면 된다.
- Get, Post
일반적인 @RequestMapping은 Get, Post 어떤 HTTP 메소드가 와도 전부 지원하기 때문에 이를 구분해줄 필요가 있다.
@RequestMapping(value = "/new-form", method = RequestMethod.GET)
이렇게 method 속성을 추가해주면 된다. 그리고 이것을 더욱 편리하게 사용하려면
@GetMapping("/new-form")
이렇게 단순하게 @GetMapping으로 코드를 줄일 수 있다. Post 방식의 경우 @PostMapping을 사용하면 된다.
<정리>
이렇게 스프링 MVC는 반환값이 String이면 알아서 ViewName으로 파악하거나 인자로 Model을 넣어도 알아서 모델로 취급하는 등 특정한 input, output이 고정되어있지 않고 유연하다. 또한 파라미터 @RequestParam으로 파싱하는 등의 개발자가 유연하고 실용적으로 사용할 수 있는 기능들이 매우 많다.
다음에는 스프링 MVC의 애노테이션 기반 기능들을 중심으로 자세히 하나하나 살펴볼 것이다.
'java > spring' 카테고리의 다른 글
[SpringMVC] 스프링 MVC - 기본 기능 (0) | 2021.05.11 |
---|---|
[Spring] 로깅 (0) | 2021.05.10 |
[SpringMVC] MVC 프레임워크 만들기 (0) | 2021.04.23 |
[SpringMVC] 서블릿, JSP, MVC 패턴 (0) | 2021.04.23 |
[SpringMVC] 서블릿 (0) | 2021.04.21 |