프론트 컨트롤러
스프링 MVC의 Dispatcher Servlet은 프론트 컨트롤러(Front Controller) 패턴으로 구현되어 있다. 기존의 MVC 패턴을 사용하면 공통 코드가 여러 컨트롤러에 산재되는데 프론트 컨트롤러 패턴을 적용하면 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출함으로써 공통 코드를 한군데 모을 수 있다.
DispatcherServlet
스프링 MVC의 기본 서블릿이다. HttpServlet을 상속 받고 있다.
- DispatcherServlet → FrameworkServlet → HttpServletBean → HttpServlet
스프링 MVC를 사용하려면 DispatcherServlet을 등록해야한다. 스프링 부트 구동시 DispatcherServlet을 자동 등록하며 모든 경로(urlPatterns="/")에 대해 매핑한다.
요청 흐름
- DispatcherServlet 서블릿의 service() 메서드가 호출된다.
- service()를 시작으로 여러 메서드가 실행되고 DispatcherServlet.doDispatch()가 호출된다.
DispatcherServlet.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());
*/
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);
}
스프링 MVC 구조
- 핸들러 조회 : 핸들러 매핑을 통해 URL에 매핑된 핸들러(컨트롤러) 검색
- 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터 검색
- 핸들러 어댑터 실행 : 핸들러 어댑터 실행
- 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행
- ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해 반환
- viewResolver 호출 : 뷰 리졸버를 찾아 실행
- View 반환 : 뷰 리졸버는 뷰의 논리 이름을 받아 물리 이름(뷰 파일 경로)으로 바꾸고 렌더링 역할을 담당하는 뷰 객체 반환
- 뷰 렌더링 : 뷰 객체를 통해 뷰를 렌더링 한다.
스프링 MVC 인터페이스
SpringMVC는 DispatcherServlet 코드 변경을 하지 않고 원하는 기능을 변경하거나 확장할 수 있다. 이것이 가능한 이유는 스프링 MVC의 주요 구성 요소들이 인터페이스로 규격화 되어 있기 때문이다. 따라서 인터페이스를 확장해 원하는 커스텀 컨트롤러, 뷰 등을 만들 수 있다. 즉 스프링 MVC는 유연한 웹 어플리케이션 설계를 가능하게 한다.
주요 인터페이스 목록
- 핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
- 핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
- 뷰 리졸버 : org.springframework.web.servlet.ViewResolver
- 뷰 : org.springframework.web.servlet.View
핸들러 매핑, 핸들러 어댑터, 뷰 리졸버
Spring MVC에서 핸들러 매핑, 핸들러 어댑터가 어떻게 동작하는지 알아보자. 아래는 Spring MVC Controller 인터페이스를 구현한 간단한 컨트롤러이다.
package hello.servlet.web.springmvc.old;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@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");
}
}
위의 컨트롤러가 호출되려면 2가지가 필요하다.
- HandlerMapping 객체 : 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑 객체가 필요하다.
- HandlerAdapter 객체 : Controller 인터페이스를 실행 할 수 있는 어댑터 객체가 필요하다.
SpringBoot는 핸들러 매핑, 핸들러 어댑터, 뷰 리졸버 객체를 생성해 빈 객체로 등록한다. 숫자는 우선 순위를 뜻한다.
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @Requestmapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
0 = RequestmappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @Requestmapping에서 사용.
1 = HttpRequestHandlerAdapter : HttpRequesthandler 처리
2 = SimpleControllerHandlerAdapter: Controller 인터페이스(애노테이션 X) 처리
위의 OldController가 실행 되기 전 까지의 동작 과정은 다음과 같다.
- 핸들러 매핑으로 핸들러 조회 : 리스트에 보관된 핸들러 매핑 객체들을 순서대로 가져와 핸들러를 찾는다. BeanNameUrlHandlerMapping 객체에서 OldController 객체가 검색되어 해당 객체를 반환한다.
- 핸들러 어댑터 조회 : 리스트에 보관된 핸들러 어댑터 객체들을 순서대로 가져와 이전에 찾은 핸들러(OldController)를 실행 할 수 있는 어댑터를 찾는다. 이 중 SimpleControllerHandlerAdapter가 반환된다.
- 핸들러 어댑터 실행 : 반환된 어댑터(SimpleControllerHandlerAdapter)를 실행하면서 그 결과를 반환한다.
뷰 리졸버
Spring MVC에서 뷰 리졸버가 어떻게 동작되는지 알아보자. 아래와 같이 다양한 뷰 리졸버가 등록된다.
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아 반환한다.
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
위의 OldController가 실행된 후 동작 과정은 다음과 같다.
- ViewResolver 호출 : 핸들러 어댑터를 통해 얻은 뷰의 논리적 이름(new-form)으로 리스트에 보관된 뷰 리졸버 객체들을 순서대로 가져와 뷰를 찾는다. InternalResourceViewResolver에서 뷰를 찾아 반환한다.
- InternalResourceViewResolver : 해당 뷰 리졸버는 InternalResourceView를 반환한다. InternalResourceViewResolver는 논리적 뷰 이름에 접두사(prefix)와 접미사(suffix)를 붙여 물리 이름(경로)를 만든다.
- view.render() : view.render()가 호출되고 InternalResourceView는 forward()를 사용해 JSP를 실행한다.
RequestMapping
예전에는 위 OldController처럼 Component에 빈 이름으로 URL을 작성해서 사용했지만 이제는 @RequestMapping 애노테이션을 사용해 편리하게 컨트롤러 구현이 가능해졌다.
SpringMemberController
package hello.servlet.web.springmvc.v2;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@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);
Member save = 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;
}
}
- @Controller : 스프링이 자동으로 스프링 빈으로 등록한다. (내부에 @Component 애노테이션이 있다.) 또한 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다. (핸들러 맵핑 객체로 RequestMappingHandlerMapping, 핸들러 어댑터 객체로 RequestMappingHandlerAdapter가 사용된다.)
- @RequestMapping : 해당 URL 요청을 처리할 수 있는 컨트롤러
- ModelAndView : 모델과 뷰 정보를 담아 반환한다.
SpringMemberControllerV2
V2버전에서는 아래의 기능을 사용하여 개발자가 조금 더 편하게 개발 할 수 있다.
- Model 도입
- viewName 직접 반환
- @RequestMapping → @GetMapping, @PostMapping 등
- @RequestParam
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)
@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)
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
- Model model : 기존의 ModelAndView 객체를 대신한다.
- viewName 직접 반환 : 애노테이션 기반의 컨트롤러는 ModelAndView뿐 아니라 ViewName을 직접 반환해도 동작한다.
- @RequestParam : HTTP 요청 파라미터를 @RequestParam으로 받을 수 있다. @RequestParam("username")은 request.getParameter("username")과 거의 같다. 때문에 Get, Post Form 방식 모두 지원한다.
- @GetMapping, @PostMapping : HTTP Method로 구분해 URL를 맵핑할 수 있다
'Spring > Spring MVC' 카테고리의 다른 글
스프링 검증 (1) (0) | 2023.03.27 |
---|---|
스프링 MVC 기본 기능 (0) | 2023.02.28 |
스프링 MVC (1) (0) | 2023.02.17 |
Spring Boot에서 Servlet 사용하기 (0) | 2023.02.16 |
서블릿 (0) | 2023.02.14 |