이전 포스팅에서는 HTTP 메시지와 Servlet의 등장,
그리고 Front Controller 패턴까지 알아봤습니다.
이번 포스팅에서는 스프링의 Front Controller인
DispatcherServlet에 handler들이 어떻게 등록되는지 살펴보겠습니다.
요약
- 최초의 스프링은 핸들러 인터페이스를 구현한 클래스를 스프링 빈으로 등록하여 핸들러로 등록했습니다.
- @Component("/urlPattern") 식으로 스프링 빈을 등록하는 것이죠.
- 이들을 BeanNameUrlHandlerMapping 이 탐지해서 등록해뒀다가 탐색 시 사용됐습니다.
- 요즘에는 RequestMapping 애너테이션 기반 핸들러 등록 방법이 많이 사용됩니다.
- RequestMappingHandlerMapping은 @Controller 또는 @RequestMapping이 있는 클래스의
핸들러들을 모두 조회해서 일괄적으로 등록을 합니다. - 그로 인해 RequestMapping애너테이션 기반으로 핸들러를 등록한다면,
하나의 클래스에 여러 핸들러를 명시할 수 있게 됩니다.
스프링도 처음엔 지금처럼 나이스하진 않았다!
import org.springframework.web.servlet.mvc.Controller;
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
throws Exception {
return new ModelAndView("new-form");
}
}
스프링의 초기 형태 모습입니다.
Front Controller 패턴이 적용되어 DispatcherServlet 하나만 서블릿 객체로 등록되는 점은 나이스하지만,
지금처럼 하나의 클래스에 여러 엔드포인트를 작성하는 모습은 아닙니다.
여기서 구현된 Controller 라는 인터페이스는 우리가 현재 사용하는 @Controller 와는 다릅니다.
해당 인터페이스를 구현하면 ModelAndView를 반환하는 handleRequest 메서드를 구현해야 합니다.
한 가지 주목할만한 점은, 이 컨트롤러를 DispatchServlet이 어떻게 인식하느냐 입니다.
우리에게 익숙한 @Component 애너테이션을 보시면,
매개변수로 전달하는 Bean Name에 해당하는 문자열이 urlPattern 과 유사한 형태임을 알 수 있습니다.
그렇다면 DispatcherServlet은 빈 컨테이너에 등록된 빈들 중,
요청하는 url 과 동일한 빈 이름을 가진 핸들러를 탐색하는 걸까요?
BeanNameUrlHandlerMapping
WebMvcConfigurationSupport 클래스에서 @Bean 애너테이션을 사용하며,
BeanNameUrlHandlerMapping 이라는 객체를 생성해서 스프링 빈에 등록합니다.
요청 URL을 이용해 핸들러를 찾는 역할을 하는 HandlerMapping 들 중 두번째 우선순위로 등록됩니다.
Implementation of the HandlerMapping interface
that maps from URLs to beans with names that start with a slash ("/") ...
This is the default implementation used by the DispatcherServlet,
along with RequestMappingHandlerMapping. ...
The mapping is from URL to bean name.
출처 : 스프링 문서 BeanNameUrlHandlerMapping
두번째 우선순위로 등록된 BeanNameHandlerMapping은
요청이 왔을 때 첫번째 우선순위에 해당하는 HandlerMapping에 의해 핸들러를 찾지 못했을 때 사용됩니다.
주석에서 알 수 있듯이 Spring에 의해 자동으로 등록되는 기본 구현체이고,
빈 이름이 슬래쉬("/") 로 시작할 경우, 핸들러로 인식하고 등록해뒀다가
이후 요청이 왔을 때 URL과 빈 이름을 비교하는 탐색시 사용하는 것으로 보입니다.
BeanNameHandlerMapping은 AbstractDetectingUrlHandlerMapping 을 상속하는데요,
부모 클래스 내부를 살펴보면 스프링의 ApplicationContext를 모두 불러와서
등록된 스프링 빈들 중 핸들러로 등록할 빈들을 탐색하는 코드를 살펴볼 수 있습니다.
SimpleControllerHandlerAdapter
Controller 인터페이스를 구현하고, @Component 애너테이션을 이용해서 스프링 빈으로 등록하되,
스프링 빈 이름을 URL 패턴에 맞춰 슬래쉬("/")로 시작하게 작성하면
BeanNameUrlHandlerMapping이 이를 탐지해서 등록해뒀다가 DispatcherServlet에 의해 호출됐을 때
핸들러를 찾아준다는 내용을 확인했습니다.
핸들러를 찾았다면, DispatcherServlet은 이 핸들러를 호출해줄 핸들러 어댑터를 찾아야 합니다.
즉, 핸들러 어댑터 또한 DispatcherServlet에 등록되어 있어야 하는 거죠.
참고로 DispatcherServlet 에는 다음과 같은 필드들이 있고, 이들이 핸들러와 어댑터를 찾는데 사용됩니다.
/** List of HandlerMappings used by this servlet. */
@Nullable
private List<HandlerMapping> handlerMappings;
/** List of HandlerAdapters used by this servlet. */
@Nullable
private List<HandlerAdapter> handlerAdapters;
public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
}
Controller를 구현한 핸들러들을 담당하는 어댑터는 SimpleControllerHandlerAdapter 입니다.
이전 포스팅에서 보았던 어댑터의 구조와 완전히 동일합니다.
supports 메서드를 재정의 할 때, Controller 타입이라면 true를 반환하는 거죠.
여기서 Controller도 @Controller와 다릅니다 :)
이전 포스팅의 V3 에서 살펴봤듯, 핸들러에서 ModelAndView를 직접 반환하는 형태이기 때문에,
핸들러 어댑터에서는 핸들러가 반환하는 값을 그대로 반환하는 모습을 보입니다.
HttpRequestHandler와 HttpRequestHandlerAdapter
@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");
}
}
public class HttpRequestHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof HttpRequestHandler);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}
}
Controller 인터페이스를 구현하지 않고, HttpRequestHandler 인터페이스를 구현하는 방법도 있는데요,
이를 구현한 핸들러는 HttpRequestHandlerAdapter가 지원해줍니다.
@Component 애너테이션과 빈 이름을 통한 탐지 등의 프로세스는 모두 동일한데요,
Controller 인터페이스와 구분된 이유는 리턴타입의 차이 때문입니다.
이전 포스팅에서 V3 와 V4가 ModelView를 리턴하느냐, String을 리턴하느냐에 따라 차이가 있었고,
V5에서는 서로 다른 V3, V4 인터페이스를 모두 사용 가능하도록 확장해보았었는데요,
스프링에 이 원리가 그대로 적용되어 있는 것입니다.
사용성을 위해 리턴 타입이 서로 다른 인터페이스를 준비해두고,
해당 인터페이스를 처리할 어댑터를 준비해두고, 개발자가 어떤 인터페이스를 선택해서 무엇을 반환하던
핸들러 어댑터에서는 일관적으로 ModelAndView를 반환할 수 있게 해주는 겁니다.
물론 HttpRequestHandlerAdapter 에서는 HandlerAdapter 인터페이스를 구현했기 때문에
리턴타입은 ModelAndView이지만, 실제 리턴은 null을 합니다.
ModelAndView가 null로 리턴되었을 때의 분기처리는 DispatcherServlet에서 진행하겠군요!
@Controller와 @RequestMapping
와 드디어! ㅋㅋ HTTP 메시지와 서블릿으로 시작해서 드디어 여기까지 왔습니다.
지금부터 앞서 알아본 Controller, HttpRequestHandler 인터페이스 구현 방식에서 더 개선된
현재 사용되는 모습인 RequestMapping 애너테이션 기반 핸들러 등록 방식을 알아보겠습니다.
스프링 애플리케이션 기동 시점에 initHandlerMethods 가 호출되면
같은 클래스의 processCandidateBean가 호출되고,
ApplicationContext에서 빈 이름으로 빈을 찾아와서 해당 빈의 타입을 찾아옵니다.
이 빈의 타입에 애너테이션 정보 중 Controller 또는 RequestMapping이 있는지 확인하는데,
이 확인하는 isHandler 메서드는 RequestMappingHandlerMapping에서 수행합니다.
간단히 생각한다면, 애플리케이션 기동 시점에 스프링 빈으로 등록되는 것들 중,
@Controller, @RequestMapping 애너테이션이 있는 빈들을 RequestMappingHandlerMapping이
자신의 핸들러 목록으로 등록한다고 생각할 수 있겠습니다.
아시다시피 @RequestMapping 애너테이션은
서블릿에서 제공하던 urlPattern 이외에도 상당히 많은 옵션을 통해 요청을 구분해서 처리할 수 있습니다.
이러한 내용은 RequestMappingHandlerMapping에서 단순히 URL로만 구분하지 않기 때문인데요,
RequestMappingInfo 라는 객체를 만들어서 여기에 urlPattern, method 등의 여러 정보를 담고,
이 정보들을 통해서 검증하고 매칭해줍니다.
완성된 RequestMappingInfo는 registerHandlerMethod 에 의해 MappingRegistry에 등록됩니다.
이처럼 애너테이션 기반으로 작성하는 것이 인터페이스를 등록하는 방식 보다 훨씬 간편하기 때문에,
DispatcherServlet 에서 요청을 처리할 핸들러를 탐색할 때,
RequestMappingHandlerMapping이 첫번째 우선순위로 탐색됩니다.
보다 간편한 애너테이션
@GetMapping("/new-form")
public String newForm() {
return "new-form";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
// ...
}
앞서 RequestMappingHandlerMapping 에서 isHandler 라는 메서드 내부에서
Controller 혹은 RequestMapping 애너테이션이 있는지 검증하는 모습을 살펴봤습니다.
요새는 다들 GetMapping, PostMapping 과 같은 애너테이션을 많이 사용하실 텐데요,
해당 애너테이션들 내부에서는 RequestMapping 애너테이션에 속성값으로 메서드를 지정해둔 것으로
RequestMapping 애너테이션을 사용하는 것과 동일합니다.
그로 인해 RequestMappingHandlerMapping 에서 isHandler 메서드를 호출해도 정상 탐지가 되겠죠!
김영한님 강의에서 Primary가 아닌 서브 DB가 필요할 때 @Qualifier("빈 이름") 을 사용하게 되는데,
이를 하나의 커스텀 애너테이션으로 만들어 사용하는 것을 배운 바 있습니다.
단순 문자열을 다루게 되면 분명 실수의 여지가 있기 때문에, 이를 방지하고 IDE의 도움을 받는 것인데요,
@SomeSubDB 애너테이션을 만들고 내부에 @Qualifier("빈 이름")을 담아두는 것이죠.
해당 컨셉과 유사하게 느껴져서 여담으로 적어봅니다.
스프링이 하나의 클래스에 여러 핸들러를 등록할 수 있는 이유
이부분은 정확히 확인된 내용은 아니고 스스로 학습하며 추론한 내용입니다.
인터페이스 구현과 빈 등록 기반으로 HandlerMapping을 할 경우,
해당 빈에서 재정의한 메서드를 호출해야 하고, 하나의 클래스에 동일한 메서드를 여러 종류로 재정의할 수 없기 때문에
하나의 클래스가 하나의 핸들러가 될 수밖에 없었습니다.
이러한 방식을 애너테이션 기반 메타정보를 활용한 HandlerMapping으로 변경할 경우,
하나의 클래스에 특정 애너테이션이 있을 경우, 그 클래스의 public 메서드 중
특정 애너테이션이 있는 메서드들의 정보를 가져와서 이들을 핸들러로 등록하는 것이 가능합니다.
그렇기에 애너테이션 기반으로 핸들러를 등록할 경우, 하나의 클래스에 여러 핸들러를 등록 가능한 것 같습니다.
실제로 하나의 컨트롤러 클래스에 선언된 여러 개의 핸들러들이 등록되는 곳을 디버그로 확인해봤습니다.
ApplicationContext 에서 빈들을 생성한 이후에 RequestMappingHandlerMapping의 afterPropertiesSet이 호출되고,
그 뒤엔 initHandlerMethods -> processCandidateBean -> detectHandlerMethods 순으로 호출되는데요,
그 중 detectHandlerMethods에서는 핸들러 후보가 되는 클래스를 탐색한 후,
해당 클래스 내부에 있는 핸들러에 해당하는 메서드들을 모두 찾아옵니다.
그리고 이들을 forEach로 순회하며 registerHandlerMethod 를 이용해 등록합니다.
학습 출처
우테코 4기 크루 칙촉과의 대화
'Java & Spring' 카테고리의 다른 글
📦 DTO는 택배상자 (Bean Validation 검증은 누가 하나?) (3) | 2022.07.25 |
---|---|
의존이 복잡하게 얽힌 Bean들은 어떻게 생성될까? (4) | 2022.07.05 |
🖋 Servlet부터 DispatcherServlet까지 (Front Controller 패턴) (0) | 2022.06.20 |
🤔 객체지향 생활체조 돌아보기 (우테코 레벨2를 마치며) (0) | 2022.06.17 |
오라클 클라우드 Autonomous Database DB접속 로컬 + 서버 (0) | 2021.10.18 |