기존 MVC 패턴의 공통처리 문제와 코드중복을 해결하기 위한 프레임워크를 직접 설계해보았다. 프론트 컨트롤러의 도입으로 공통처리가 가능해지고. 뷰 리졸버와 MyView의 도입으로 컨트롤러는 뷰 상대경로만 반환할 수 있게 되었으며, 어댑터 패턴의 도입으로 ModelView 반환 방식과 문자열 변환 방식을 동시에 처리할 수 있게 되었다.
스프링 MVC는 위 구조와 매우 유사한 구조를 갖는다 하였다. 그러면 스프링 MVC는 어떻게 이루어져 있을까?
스프링에는 DispacherServlet이라는 프론트 컨트롤러가 있다. 스프링부트는 DispacherServlet을 서블릿으로 자동등록하고, URI "/" 와 매핑한다.
디스패쳐 서블릿은 핸들러 매핑을 뒤져 URI와 매핑되는 컨트롤러를 찾고, 어댑터들을 뒤져 컨트롤러 처리를 위한 어댑터를 찾는다.
핸들러 매핑은 URI를 이용해 핸들러를 찾는 것을 말하며. @RequestMapping 으로 찾기, 스프링 빈 이름으로 찾기 등의 방식이 있다.
핸들러 어댑터는 URI와 매핑된 핸들러를 처리하는 어댑터를 말하며. 어노테이션 방식의 처리 어댑터, 인터페이스 방식의 처리 어댑터 등이 있다.
뷰 리졸버는 컨트롤러가 입력한 상대경로를 절대경로로 바꿔주며. 기본적으로 접두사에 /templates 접미사에 .html를 추가해준다. 다른 경로나 .jsp 로 포워드 해주려면 application.properties에 직접 접두사와 접미사를 추가해야한다.
상황가정: @RequestMapping 애노테이션을 이용해 핸들러와 URI를 매핑해놓았다.
@Controller
public class Example {
@RequestMapping("/ex1")
public String exController() {
return "view1";
}
}
클라이언트가 URI로 요청한다.
디스패쳐 서블릿은 핸들러 매핑들을 실행해서 해당 URI의 핸들러를 찾는다. @RequestMapping으로 등록된 핸들러들을 먼저 뒤지고, 스프링 빈 이름이 URI로 등록된 핸들러들을 뒤진다. 위의 상황에서는 @RequestMapping으로 매핑된 핸들러를 찾아낼 것이다.
디스패쳐 서블릿은 핸들러 어댑터 목록을 뒤져 찾아낸 핸들러를 처리할 어댑터를 찾는다. @RequestMapping의 핸들러를 처리할 어댑터를 먼저 찾고, Controller 인터페이스를 구현한 핸들러를 처리할 어댑터를 찾는다. 위의 상황에서는 @RequestMapping으로 설정된 핸들러를 처리할 어댑터를 찾아낼 것이다.
디스패쳐 서블릿이 어댑터의 handle 메서드를 호출한다.
어댑터는 핸들러를 호출하고 수행 결과를 담은 모델과 뷰 상대경로를 담은 ModelAndView 객체를 받아온다. 핸들러가 상대경로만 반환할 경우 어댑터가 모델을 생성하여 핸들러가 사용하도록 파라미터로 넘겨준다.
어댑터가 디스패쳐 서블릿에게 ModelAndView 객체를 반환한다.
디스패쳐 서블릿이 뷰 리졸버에게 상대경로를 넘겨준다.
뷰 리졸버는 View 객체에 절대경로를 담아 반환한다.
View 객체에게 Model을 넘겨주며 메서드를 수행하면 템플릿 엔진에게 모델을 넘겨주며 포워드된다.
템플릿 엔진이 모델을 활용해 동적 HTML을 생성하고 브라우저에 출력한다.
1. URI와 매핑된 핸들러를 핸들러 매핑을 실행해 찾음
2. 핸들러를 처리할 어댑터를 핸들러 어댑터 목록에서 찾음.
3. 핸들러 어댑터가 핸들러를 실행하여 ModelAndView 받음.
4. 그 중에 상대 경로를 뷰 리졸버에게 줘서 절대 경로 담은 View 객체 받음.
5. View 객체가 내장 메서드로 모델 담아 포워드 수행.
디스패쳐 서블릿은 공통 처리를 위해.
뷰 리졸버는 절대 경로로 바꿔주므로 뷰 경로의 중복 방지를 위해.
뷰 클래스는 뷰로 포워드 하는 부분의 중복 방지를 위해.
위 기능들은 도입한 이유가 확실하다. 그런데 어댑터 패턴은? 어댑터 패턴은 다양한 방식의 처리를 위해 각 방식의 처리 어댑터를 별도로 생성하여 유연성을 보장한다. 스프링에서 대체 어떤 유연성이 필요하기에 어댑터 패턴을 도입했을까?
@Component("/spring/ex1")
public class OldController implements Controller {
@Override
public ModelAndView beanNameMappingController(HttpServletRequest request,
HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return null;
}
}
스프링 빈 이름으로 URL을 등록하여 컨트롤러와 매핑을 수행한다.
또한 Controller 인터페이스를 구현하는 방식으로 컨트롤러임을 알려준다.
@Controller
public class ExampleClass1 {
@RequestMapping("/spring/ex1")
public String requestMappingController() {
return "/view1";
}
}
@RequestMapping 으로 URL과 컨트롤러를 매핑한다.
@RequestMapping이 붙은 것은 컨트롤러이다.
현재 대부분 @RequestMapping 을 사용하지만, URL을 빈이름으로 하여 매핑하고 Controller 인터페이스를 구현하는 방식의 구형 컨트롤러도 존재할 것이다.
스프링은 두 경우를 모두 처리하기 위해 어댑터 패턴을 도입했다. 하지만 더 자주 사용하는 @RequestMapping 방식이 매핑과 어댑터 탐색 시 모두 우선순위를 갖는다.
URL 로 요청이 들어오면 디스패쳐 서블릿은 해당 URL과 매핑된 컨트롤러를 찾는다. 먼저 RequestMappingHandlerMapping을 실행하여 @RequestParam 방식으로 매핑된 컨트롤러를 찾고, 그 후 BeanNameUrlHandlerMapping을 실행해 스프링 빈 이름으로 매핑한 컨트롤러를 찾는다.
위의 결과 핸들러(컨트롤러)를 찾았을 것이다. 이제 해당 핸들러를 처리(실행)할 수 있는 어댑터를 찾아야하므로 어댑터 리스트를 돌면서 적합한 어댑터를 찾는다. 이때도 애노테이션 기반 처리 어댑터가 우선순위를 갖는다.
스프링은 다양한 매핑 방식의 핸들러, 다양한 핸들러의 구현방식을 동시에 처리할 수 있게 하기 위해 어댑터 패턴을 반영했다. 현재는 매핑과 핸들러 모두 애노테이션 방식이 주류로 사용되고 있다. 하지만 다형성으로 설계하였기에 새로 등장한 방식이 주류가 되어도 확장이 매우 용이하다.
본 글은 김영한님의 "스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술" 강의내용 및 이해한 내용을 정리한 것입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard