-이전 시간에 단계별로 만든 'MVC 프레임워크'와 '스프링 MVC'를 비교해본다.
-스프링MVC에서 핸들러매핑, 뷰리졸버가 어떻게 사용되는지 확인한다.
-컨트롤러 사용시 컨트롤러 인터페이스 방식과 애노테이션 방식을 학습하며 실무에서 사용되는 법을 학습한다.
-모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture4)
5.스프링 MVC 구조 이해
1.스프링 MVC 전체 구조
직접만든 MVC프레임워크와 스프링 MVC를 비교해보자.
- FrontController -> DispatcherServlet
-
handlerMappingMap -> HandlerMapping
-
MyHandlerAdapter -> HandlerAdapter
-
ModelView -> ModelAndView
-
viewResolver -> ViewResolver(인터페이스)
-
MyView -> View(인터페이스)
MVC 프레임워크와 마찬가지로 스프링 MVC도 프론트 컨트롤러 패턴을 사용하며 DispatcherServlet이 그 역할을 한다.
DispatcherServlet은 스프링부트가 톰캣 가동시 자동으로 등록되며 모든 경로를 받도록 되어 있다.(urlPattern="/")
즉, 내부적으로 HttpServlet을 상속 받아 사용한다.
서블릿이 호출되면 HttpServlet의 service()가 실행된다. service()는 내부적으로 GET, POST 등을 판단 후 doGet(), doPost()등을 실행한다.
2.핸들러매핑과 핸들러어댑터
과거에는 Controller 인터페이스가 사용되었다. 이 인터페이스로 핸들러매핑, 핸들러어댑터를 알아본다.
아래와 같이 Controller 인터페이스를 상속한 OldController를 만들어보자.
[OldController.java]
//@Controller 이전에 사용한 Controller 인터페이스
//@Component : 스프링 빈 이름을 URL로 맞추어 사용 가능
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
//뷰리졸버를 만들어주자 -> application.properties
return new ModelAndView("new-form");
}
}
@Component에 이름을 주어 /springmvc/old-controller라는 이름의 스프링 빈을 만들어 등록했다.
http://localhost:8080/springmvc/old-controller로 접속 가능하다. 논리명 "new-form"으로 리턴했는데 호출이 정상적으로 된다.
이 컨트롤러가 호출되는 방법은 다음과 같다.
우선 핸들러매핑을 통해 컨트롤러를 찾을 수 있어야 한다. 이 컨트롤러는 스프링 빈이므로 스프링 빈 이름으로 핸들러매핑한다.
그리고 핸들러 어댑터를 통해 찾은 핸들러를 실행할 수 있는 어댑터가 필요하다. 여기서는 Controller 인터페이스를 실행할 수 있는 핸들러어댑터가 필요하다.
스프링은 기본적으로 대부분의 핸들러매핑, 핸들러어댑터를 구현해 두었다. 스프링부트는 이것들을 자동으로 등록한다.
- HandlerMapping
- 0순위 : RequestMappingHandlerMapping / 애노테이션 기반 컨트롤러인 @RequestMapping에서 사용
- 1순위 : BeanNameUrlHandlerMapping / 스프링 빈 이름으로 핸들러를 찾음 -> OldController
- HandlerAdapter
- 0순위 : RequestMappingHandlerAdapter / 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
- 1순위 : HttpRequestHandlerAdapter / HttpRequestHandler 처리
- 2순위 : SimpleControllerHandlerAdapter / Controller 인터페이스 처리 -> OldController
매핑은 순위별로 진행한다. 0순위에서 처리 불가하면 1순위로 넘어가는 식이다.
디스패처 서블릿이 처음에 핸들러매핑에서 핸들러를 찾고, 핸들러어댑터를 찾아 handle()을 실행한다.
핸들러 조회 -> 핸들러어댑터조회 -> 핸들러어댑터 실행 순이다.
가장 우선순위가 높은 핸들러매핑과 핸들러어댑터는 RequestMappingHandlerMapping이다. @RequestMapping 애노테이션 컨트롤러를 지원한다.
3.뷰리졸버
위의 코드에서 View를 사용할 수 있도록 다음과 같은 코드가 있다.
return new ModelAndView("new-form");
실행하면 Whitelabel Error Page가 나오는데 뷰리졸버 설정을 해줘야 한다. 스프링부트는 InternalResourceViewResolver라는 뷰리졸버를 자동등록한다. 이 때 application.properties에 prefix, suffix을 설정해주어 사용 가능하다.
#스프링부트 뷰리졸버(InternalResourceViewResolver 자동 등록 / 아래 prefix, suffix 사용해서 등록)
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
그러면 실제로 "/WEB-INF/views/new-form.jsp"로 실행이 된다.
스프링부트는 자동 등록하는 뷰리졸버들이 있다.
- 1순위 : BeanNameViewResolver / 빈 이름으로 뷰를 찾아 반환한다(예 : 엑셀파일생성 기능에 사용)
- 2순위 : InternalResourceViewResolver / JSP를 처리할 수 있는 뷰를 반환한다.
위의 OldController 실행 순서는 간단하게 다음과 같다.
- 핸들러어댑터 호출 : 핸들러어댑터를 통해 new-form이라는 논리뷰이름을 획득
- ViewResolver 호출 : new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출(new-form 빈이름 매칭 없으므로 InternalResourceViewResolver) 호출
- InternalResourceViewResolver가 InternalResourceView 반환
- view.rendor()가 호출되어 InternalResourceView의 forward() 실행하여 JSP 실행
참고로 InternalResourceViewResolver는 JSTL 라이브러리가 있으면 InternalResourceView를 상속받은 JstlView를 반환한다.
라이브러리를 까보니 아래와 같이 구현되어 있다.
[InternalResourceViewResolver.java]
public class InternalResourceViewResolver extends UrlBasedViewResolver {
//jstl클래스가 존재하는지, 로드할 수 있는지 체크
private static final boolean jstlPresent = ClassUtils.isPresent(
"javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());
@Nullable
private Boolean alwaysInclude;
/**
* Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}:
* by default {@link InternalResourceView}, or {@link JstlView} if the JSTL API
* is present.
*/
//스프링부트 실행시 Bean등록되면서 실행
public InternalResourceViewResolver() {
Class<?> viewClass = requiredViewClass();
if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
setViewClass(viewClass);
}
//...
@Override
protected Class<?> requiredViewClass() {
return InternalResourceView.class;
}
}
4.스프링 MVC - 시작하기
스프링이 제공하는 컨트롤러는 애노테이션 기반으로 유연하며 실용적이다. @RequestMapping이 대표적이다.
@RequestMapping은 앞서 보았듯이 가장 우선순위가 높은 핸들러매핑, 핸들러어댑터를 실행한다.
- RequestMappingHandlerMapping
- RequestMappingHandlerAdapter
이제 애노테이션 기반의 컨트롤러를 만들어볼텐데, v1,v2,v3 순서대로 만든다. 그러나 v1은 v2를 각각만든 것과 거의 동일하므로 바로 v2로 넘어간다.
[SpringMemberControllerV2.java]
@Controller
//@Controller는 @Component + @ReuqestMapping이다. @RequestMappingHandlerMapping 대상이됨.
@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.getModel().put("member", member);
mv.addObject("member", member);
return mv;
}
@RequestMapping
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
//mv.getModel().put("members", members);
mv.addObject("members", members);
return mv;
}
}
위에 주석에서 언급했듯이 @Controller는 내부에 @Component가 있어 컴포넌트 스캔 대상이 되며 @RequestMapping도 존재하여 요청 정보를 매핑한다.
즉, RequestMappingHandlerMapping은 스프링 빈 중에 @RequestMapping이나 @Controller 컨트롤러를 매핑 정보로 인식한다.
다음 코드도 동일하게 동작한다.
@Component
@RequestMapping
public class SpringMemberControllerV2 {
//...
}
그리고 Model에 데이터 추가시 getModel().put()보다 addObject()로 편하게 사용할 수 있다.
다음으로 V3은 더욱 편리하게 쓸 수 있도록 개선하였다. 다음과 같다.
[SpringMemberControllerV3.java]
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
//@RequestMapping(value = "/new-form", method = RequestMethod.GET)
@GetMapping("/new-form")
public String newForm() {
return "new-form";
}
//@RequestMapping(value = "/save", method = RequestMethod.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)
@PostMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
우선 논리명 반환시 viewName을 직접 반환할 수 있도록 변경하였다.
그리고 Model파라미터를 추가해서 사용할 수 있는데 save(), members()를 보면 Model을 파라미터로 받는 것을 확인할 수 있다. 이 model을 사용해 쉽게 JSP에 보낼 데이터를 추가할 수 있다.
그리고 @RequestParam을 사용하여 http 요청 파라미터를 받을 수 있다. 굳이 HttpServletRequest의 getParameter("username") 등을 사용할 필요가 없다.
마지막으로 @RequestMapping 대신에 GET, POST 여부에 따라 @GetMapping, @PostMapping을 사용할 수 있다.
'개발자 일지 > Spring' 카테고리의 다른 글
[스프링]@ResponseBody 역할, 쓰는이유, 대체 어노테이션(@RestController) 알아보기 (0) | 2024.09.10 |
---|---|
[개인학습]스프링부트 + Swagger + JPA + MySQL 설정 및 테스트 (2) | 2022.10.10 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(7) (3) | 2022.07.03 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(6) (0) | 2022.06.18 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(4) (0) | 2022.06.04 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(3) (2) | 2022.05.31 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(2) (0) | 2022.05.16 |
[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(1) (0) | 2022.05.12 |