[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션5 스프링 MVC - 구조 이해
◼️ 스프링 MVC 전체 구조
🟢 DispatcherServlet
- 스프링 MVC 핵심, 웹 요청 처리 주체
- 스프링 MVC의 프론트 컨트롤러
- 스프링 부트는 내부적으로 DispatcherServlet을 자동으로 등록하고, 모든 경로(/)에 대해 매핑
🟢 DispatcherServlet 구조
DispatcherServlet → FrameworkServlet → HttpServletBean → HttpServlet // 상속구조
🟢 DispatcherServlet 요청 흐름
- 서블릿이 호출되면 HttpServlet 이 제공하는 service() 가 호출됨
- 스프링 MVC는 DispatcherServlet 의 부모인 FrameworkServlet 에서 service() 를 오버라이드 해 두었음
- FrameworkServlet.service() 를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.doDispatch() 가 호출됨
- DispatcherServlet 핵심은 doDispatch(). 여기서 핸들러 조회~뷰 렌더링 과정 진행
🟢 동작 구조
- 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회
- 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회
- 핸들러 어댑터 실행: 핸들러 어댑터를 실행
- 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행
- ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환
- viewResolver 호출: 뷰 리졸버를 찾고 실행 (JSP의 경우 InternalResourceViewResolver 가 자동 등록되고 사용됨)
- View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환. (JSP의 경우 InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 로직이 있음)
- 뷰 렌더링: 뷰를 렌더링
🟢 주요 인터페이스
- 핸들러 매핑: org.springframework.web.servlet.HandlerMapping
- 핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter
- 뷰 리졸버: org.springframework.web.servlet.ViewResolver
- 뷰: org.springframework.web.servlet.View
◼️ 핸들러 매핑과 핸들러 어댑터
과거 스프링은 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을 빈의 이름으로 매핑했다. 위 링크로 서버를 켜고 접속해보면 콘솔에 OldController.handleRequest 이 출력된다.컨트롤러가 호출된 것을 알 수 있다.
그럼 컨트롤러는 어떻게 호출된걸까?
- HandlerMapping(핸들러 매핑) 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다. 위 코드에선, 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
- HandlerAdapter(핸들러 어댑터) 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다. 위 코드에선, Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다. 개발자가 직접 핸들러 매핑과 핸들러 어댑 터를 만드는 일은 거의 없다.
🟢 스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터
- HandlerMapping
- RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
- BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
- HandlerAdapter
- RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
- HttpRequestHandlerAdapter : HttpRequestHandler 처리
- SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
다시 위 예시를 보고 흐름을 따라보자.
🔶 Controller 인터페이스 실행흐름
- 먼저 빈 이름으로 핸들러를 찾는 BeanNameUrlHandlerMapping가 동작하고 핸들러 OldController가 반환된다.
- 그 후 디스패치 서블릿이 컨트롤러 인터페이스를 담당하는 SimpleControllerHandlerAdapter를 조회하고,이 HandlerAdapter의 supports()를 호출한다. (handler instanceof Controller이므로 지원대상이된다.)
- SimpleControllerHandlerAdapter 는 핸들러인 OldController 를 내부에서 실행하고, 그 결과를 반환한다.
🟢 HttpRequestHandler
@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");
}
}
서블릿과 가장 유사한 형태의 핸들러 HttpRequestHandler 를 사용해보자.
🔶 HttpRequestHandler 인터페이스 실행흐름
- 먼저 빈 이름으로 핸들러를 찾는 BeanNameUrlHandlerMapping가 동작하고 핸들러 MyHttpRequestHandler 가 반환된다.
- 그 후 디스패치 서블릿이 MyHttpRequestHandler를 담당하는 HttpRequestHandlerAdapter를 조회하고,이 HandlerAdapter의 supports()를 호출한다. (handler instanceof HttpRequestHandler 이므로 지원대상이된다.)
- HttpRequestHandlerAdapter는 핸들러인 MyHttpRequestHandler 를 내부에서 실행하고, 그 결과를 반환한다.
🟢 @RequestMapping
최근 대부분 핸들러 매핑과 어댑터를 RequestMappingHandlerMapping , RequestMappingHandlerAdapter 을 사용한다.
◼️ 뷰 리졸버
🟢 스프링 부트 자동 등록 뷰 리졸버 (순서대로 호출)
- BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환. (예: 엑셀 파일 생성 기능 에 사용)
- InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환
실습
@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"); // 수정된 부분 !!!
}
}
View를 사용할 수 있도록 return 코드를 수정했다. URL을 실행해보면 컨트롤러는 정상 호출되지만, Whitelabel Error Page 오류 가 발생한다.
application.properties에 아래를 추가하고 다시실행하면, 화면이 정상 출력된다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
작동방식을 보자.
위와같이 등록하면, 스프링은 아래처럼 자동으로 등록해준다.
- 핸들러 어댑터 호출 및 논리 뷰 이름 획득: 클라이언트 요청이 들어오면, Spring MVC 핸들러 매핑.요청을 처리한 후, 컨트롤러는 논리뷰 이름 반환 'new-form'
- ViewResolver 호출: 'new-form' 스프링 빈으로 등록X → InternalResourceViewResolver 호출
- InternalResourceViewResolver: 이 리졸버는 논리 뷰 → 실제 뷰 경로. InternalResourceView 객체 반환.
- InternalResourceView: JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용 (렌더링,JSP파일 실행)
- view.render(): InternalResourceView는 render() 메서드로 JSP 파일 실행.
🪄 InternalResourceViewResolver 는 만약 JSTL 라이브러리가 있으면 JstlView 를 반환한다. (InternalResourceView 상속,약간의 기능 추가)
JSP는 forward()를 통해서 해당 JSP로 이동(실행)→렌더링된다. JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링 된다.
◼️ 스프링 MVC - 시작하기
🟢 @RequestMapping
- RequestMappingHandlerMapping
- RequestMappingHandlerAdapter
- 실무에서는 이 방식의 어노테이션 컨트롤러를 99.9% 사용
@Controller // 스프링이 자동으로 스프링 빈으로 등록 (내부에 @Component 존재)
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form") // URL-메서드 매핑
public ModelAndView process() {
return new ModelAndView("new-form"); // 모델과 뷰 정보 담아 반환
}
}
컴포넌트 스캔 X 스프링 빈으로 직접 등록 방법
@Bean
SpringMemberFormControllerV1 springMemberFormControllerV1() {
return new SpringMemberFormControllerV1();
}
@RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form") // URL-메서드 매핑
public ModelAndView process() {
return new ModelAndView("new-form"); // 모델과 뷰 정보 담아 반환
}
}
🪄 스프링 부트 3.0(스프링 프레임워크 6.0)부터는 클래스 레벨에 @RequestMapping 이 있어도 스프링 컨트롤러로 인 식하지 않는다. 오직 @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;
}
}
참고로, ModelAndView 를 통해 Model 데이터를 추가할 때는 addObject() 를 사용한다. 이 데이터는 이후 뷰를 렌더링 할 때 사용한다.
◼️ 스프링 MVC - 컨트롤러 통합
클래스 레벨에 @RequestMapping 을 두면 메서드 레벨과 조합이 된다.이를통해 중복을 줄일 수 있다.
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {
//메서드 레벨 @RequestMapping("/new-form") => /springmvc/v2/members/new-form
}
◼️ 스프링 MVC - 실용적인 방식
/**
* 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";
}
🟢 위 코드에서 개선된 사항
- Model 파라미터: save() , members() 를 보면, Model을 파라미터로 받음
- ViewName 직접 반환: ModelandView가 아닌 뷰의 논리 이름을 반환
- @RequestParam 사용: 스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있음
- @GetMapping , @PostMapping: @GetMapping...etc 코드는 @RequestMapping 애노테이션을 내부에 가지고 있음
'Spring' 카테고리의 다른 글
[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션7 스프링 MVC - 웹 페이지 만들기 (0) | 2024.04.24 |
---|---|
[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션6 스프링 MVC - 기본 기능 (1) | 2024.04.21 |
[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션4 MVC 프레임워크 만들기 (0) | 2024.04.14 |
[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션3 서블릿, JSP, MVC 패턴 (0) | 2024.04.13 |
[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션2 서블릿 (0) | 2024.04.12 |