[Spring MVC] Request 처리 과정 [2] - HandlerMapping
Spring MVC Request 처리의 과정 2편 HandlerMapping
지난 포스트에서 사용자 Request를 처리하는 Front Controller인 DispatcherServlet 의 동작 순서를 간략하게 알아보았다. 이제 DispatcherServlet에서 사용하는 HandlerMapping과 HandlerAdapter에 대해 알아본다.
HandlerMapping
HandlerMapping은 이전 포스트에서 언급했듯 HandlerExecutionChain 를 반환하는 메소드 getHandler 를 제공하는 인터페이스이다.
살펴보기 앞서 RequestMappingHandlerMapping의 계층 구조를 살펴 볼 필요가 있다.
AbstractHandlerMapping이 HandlerMapping 인터페이스를 최초 구현한 추상 클래스이며
MatchableHandlerMapping 은 아래 두 메소드를 구현해야하는 인터페이스이다.
public interface MatchableHandlerMapping extends HandlerMapping {
@Nullable
default PathPatternParser getPatternParser() {
return null;
}
@Nullable
RequestMatchResult match(HttpServletRequest request, String pattern);
}
AbstractHandlerMapping은 템플릿 메서드 패턴이 적용된 추상 클래스로 getHandlerInternal 이라는 메서드를 상속받은 클래스에서 구현해 HandlerExecutionChain을 반환해야한다. 이 추상 클래스에서는 Interceptor 리스트, URL 파싱을 위한 PathMatcher, PathPatternParser CORS 처리를 위한 CorsProcessor 등을 Field로 가지고 있다.
이 추상 클래스는 getHandler 메소드를 구현하고 있다.
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
...
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String handlerName) {
handler = obtainApplicationContext().getBean(handlerName);
}
// Ensure presence of cached lookupPath for interceptors and others
if (!ServletRequestPathUtils.hasCachedPath(request)) {
initLookupPath(request);
}
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (logger.isTraceEnabled()) {
logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) {
logger.debug("Mapped to " + executionChain.getHandler());
}
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = getCorsConfiguration(handler, request);
if (getCorsConfigurationSource() != null) {
CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
config = (globalConfig != null ? globalConfig.combine(config) : config);
}
if (config != null) {
config.validateAllowCredentials();
}
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
@Nullable
protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
...
}
이런 저런 처리 과정이 많아 붙어 있지만 핵심 내용은 getHandlerInternal을 첫 줄에서 호출하여 HandlerExecutionChain을 가져온다는 점이고, 이는 상속받은 클래스에서 구현해야하는 추상 메소드이다.
이를 상속받은 [AbstractHandlerMethodMapping][AbstractHandlerMapping]을 살펴보자
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
@Nullable
private HandlerMethodMappingNamingStrategy<T> namingStrategy;
private final MappingRegistry mappingRegistry = new MappingRegistry();
...
@Override
@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
}
여기에 구현되어있는 getHandlerIntenal 구현은 request로부터 lookupPath를 찾아오는데 initLookupPath(request) 의 동작과정은 아래와 같다.
- ServletRequestParseUtils 를 이용하여 RequestPath 객체를 얻는다. RequestPath 는 contextPath와 pathWithInApplication 두 메소드를 제공하는 인터페이스로 구현체는 스프링 어플리케이션의 Context Path와 해당 Context Path 이하 Full Path을 제공하는 기능을 한다.
- RequestPath 의 pathWithInApplication 을 이용하여 실제 요청을 처리할 전체 경로를 얻는다.
이후 mappingRegistry 에 락을 걸고 HandlerMethod를 찾는 과정을 수행한다. HandlerMethod가 우리가 @Controller, @RestController 등을 사용하여 등록한 컨트롤러에 구현한 실제 메소드이다.
여기에 AbstractHandlerMethodMapping 클래스의 추상 메소드들이 존재한다.
protected abstract boolean isHandler(Class<?> beanType);
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
protected abstract T getMatchingMapping(T mapping, HttpServletRequest request);
protected abstract Comparator<T> getMappingComparator(HttpServletRequest request);
제네릭이 적용된 형태라서 아직은 이게 무슨 용도인지 주석만 봐서는 파악이 잘 안된다. 우선 isHandler는 아래에서 호출된다.
...
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
...
즉 beanName을 스트링으로 받아와 ApplicationContext에서 Bean을 검색하여 해당 Bean의 Class 메타정보를 획득한다. 그 후 해당 bean이 Handler에 해당하면 detectHandlerMethods 를 수행한다.
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String beanName ?
obtainApplicationContext().getType(beanName) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
else if (mappingsLogger.isDebugEnabled()) {
mappingsLogger.debug(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
여기서 handlerType에 대해 methods를 구성하는데 Map<Method, T> 이다. 여기서 추상 메소드 getMappingForMethod 를 통해 Method에 매칭될 T를 생성하고 있다. 그 후 HandlerMethod를 등록하는 과정을 거친다.
HandlerMethod는 아래와 같다.
public class HandlerMethod {
private final Object bean;
@Nullable
private final BeanFactory beanFactory;
@Nullable
private final MessageSource messageSource;
private final Class<?> beanType;
private final Method method;
private final Method bridgedMethod;
private final MethodParameter[] parameters;
@Nullable
private HttpStatusCode responseStatus;
@Nullable
private String responseStatusReason;
@Nullable
private HandlerMethod resolvedFromHandlerMethod;
@Nullable
private volatile List<Annotation[][]> interfaceParameterAnnotations;
private final String description;
}
실제 Handler의 인스턴스인 bean(bean을 생성할 때 사용할 Factory) 가 정의되어 있고, 해당 bean의 Class Meta정보인 beanType, 실제 핸들러로 수행하게될 Method, method의 파라미터로 전달할 parameters 등이 있는것을 확인할 수 있다.
// AbstractHandlerMethodMapping의 Nested Class
static class MappingRegistration<T> {
private final T mapping;
private final HandlerMethod handlerMethod;
private final Set<String> directPaths;
@Nullable
private final String mappingName;
private final boolean corsConfig;
}
위 형태이다. 그리고 MappingRegistry는 아래와 같다.
// AbstractHandlerMethodMapping의 Nested Class
class MappingRegistry {
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new
ConcurrentHashMap<>();
...
}
여기까지 보자면 T 는 Request 정보를 통해 만들어진 어떤 클래스 일 것이고 위 추상 HandlerMapping에서는 이 정보를 가지고 Request를 처리할 수 있는 HandlerMethod를 저장하고 관리하는 기능을 구현한 것으로 볼 수 있다.
그럼 다음 추상 클래스인 RequestMappingInfoHandlerMapping 를 살펴보자
public abstract class RequestMappingInfoHandlerMapping extends
AbstractHandlerMethodMapping<RequestMappingInfo> {
...
}
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
...
@Nullable
private final PathPatternsRequestCondition pathPatternsCondition;
@Nullable
private final PatternsRequestCondition patternsCondition;
private final RequestMethodsRequestCondition methodsCondition;
private final ParamsRequestCondition paramsCondition;
private final HeadersRequestCondition headersCondition;
private final ConsumesRequestCondition consumesCondition;
private final ProducesRequestCondition producesCondition;
private final RequestConditionHolder customConditionHolder;
private final int hashCode;
private final BuilderConfiguration options
}
RequestMappingInfoHandlerMapping에서는 RequestMappingInfo 라는 클래스르 이용하여 기능을 구현하고 있다. RequestMappingInfo는 Controller 에서 어떤 요청을 받아 어떤 응답을 할 것인지 정의할 수 있는데 그 정보들을 한데 모은 클래스로 hashCode를 별도로 정의하여 구분할 수 있도록 구현되어 있다.
그럼 이 클래스에서 구현된 AbstractHandlerMethodMapping의 추상 메소드는 두가지이다.
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
return info.getMatchingCondition(request);
}
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
return (info1, info2) -> info1.compareTo(info2, request);
}
RequestMatchingInfo의 MatchingCondition을 가져와 반환하고 있고, 이를 기반으로 AbstractHandlerMethodMapping 에서는 매칭되는 handler 정보들을 가져오고 이 중 가장 잘 맞는 HandlerMethod를 반환한게 된다.
다음은 최종 구현체인 RequestMappingHandlerMapping을 살펴보자
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements MatchableHandlerMapping, EmbeddedValueResolverAware {
@Override
protected boolean isHandler(Class<?> beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
}
isHandler가 여기서 구현되어 있다. 해당 클래스에 @Controller 어노테이션이 달려있다면 참을 반환한다. 그리고 getMappingForMethod는 createRequestMappingInfo 를 이용하여 @RequestMapping 어노테이션에 입력된 파라미터 정보들을 가져와 RequestMappingInfo를 생성하여 반환한다.
자 엄청나게 클래스들 간을 넘나들며 인터페이스 및 추상 메소드 구현을 살펴 보았으니 동작을 순서대로 정리하여 HandlerMapping의 동작과정을 정리해보자.
- DispatcherServlet의 initHandlerMappings 메소드에서 ApplicationContext에서 이용 가능한 모든 HandlerMapping 구현체들을 받아온다.
- DispatcherServlet은 요청을 가로채고 이 요청을 처리할 1에서 등록한 HandlerMapping 들의 getHandler 메소드를 호출한다.
- RequestMappingHandlerMapping의 getHandler(AbstractHandlerMapping에 정의) 메소드가 호출된다.
- RequestMappingHandlerMapping의 getHandlerInternal(AbstractHandlerMethodMapping에 정의)이 실행된다.
- mappingRegistry에서 요청 경로에 맞는 RequestMappingInfo를 탐색하여 getMatchingMappings(추상 메소드)가 실행되며 Match 정보가 추가된다.
- getMappingComparator(추상 메소드) 를 이용하여 가장 잘 맞는 Match를 선정한다.
- Best Match 내부 registration 정보에서 HandlerMethod를 반환한다.
- getHandlerExecutionChain 메소드가 호출되어 HandlerExecutionChain이 반환된다.
- getHandler 메소드가 종료되며 HandlerExecutionChain이 반환된다.
여기까지가 Request가 들어온 직후 HandlerExecutionChain 반환까지의 과정이다.
AbstractHandlerMethodMapping을 보다 Controller 어노테이션이 달려있는 컨트롤러 클래스가 언제 등록되는 것인가? 에 대한 의문이 생긴다. 그 과정은 InitializingBean 인터페이스 구현부인 afterPropertiesSet 메소드에 있다.
Spring 에 의해 Bean 초기화 과정(생성자를 통한 생성)이 끝나고 난 뒤 afterPropertiesSet 메소드가 호출된다. AbstractHandlerMethodMapping에서는 아래의 순서로 HandlerMethod가 등록이 된다.
- afterPropertiesSet 메소드에서 initHandlerMethods 메소드를 호출한다.
- initHandlerMethods 내부에서 getCandidateBeanNames 메소드를 호출하여 Object.class 타입의 모든 Bean의 이름을 반환 받는다.
- initHandlerMethods 내부에서 "scopedBeanTarget." 으로 시작하지 않는 모든 빈들을 대상으로 processCandidatesBean을 호출한다.
- 해당 beanName을 가진 Bean의 타입이 Handler인지 체크하고 Handler라면(즉 @Controller 어노테이션이 붙은 클래스) detectHandlerMethods 메소드를 호출한다.
- detectHandlerMethods 내부에서 RequestMappingInfo List를 생성하고 실행 가능한 메소드들을 registerHandlerMethod를 통해 등록한다.
- registerHandlerMethod 는 결국 mappingRegistry에 RequestMappingInfo로 MappingRegistration<RequestMappingInfo> 을 등록하고, pathName으로 RequestMapping을 검색할 수 있도록 등록하는 과정을 거친다.
위 과정을 통해 Handler에서 처리할 수 있는 HandlerMethod 들이 등록되고 HttpServletRequest가 들어왔을 때, DispatcherServlet에서 RequestMappingHandlerMapping이 mappingRegistry에서 해당 요청을 처리할 HandlerMethod를 반환받아 HandlerExecutionChain 을 형성한다.
다음 포스트에서는 HandlerAdapter에 대해 알아본다.