Web
수정·

[Spring MVC] Request 처리 과정 [2] - HandlerMapping

by Dohoon Kim · 23년 11월 03일 20:11:16

Spring MVC Request 처리의 과정 2편 HandlerMapping

지난 포스트에서 사용자 Request를 처리하는 Front Controller인 DispatcherServlet 의 동작 순서를 간략하게 알아보았다. 이제 DispatcherServlet에서 사용하는 HandlerMapping과 HandlerAdapter에 대해 알아본다.

HandlerMapping

HandlerMapping은 이전 포스트에서 언급했듯 HandlerExecutionChain 를 반환하는 메소드 getHandler 를 제공하는 인터페이스이다.

살펴보기 앞서 RequestMappingHandlerMapping의 계층 구조를 살펴 볼 필요가 있다. imageURL 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) 의 동작과정은 아래와 같다.

  1. ServletRequestParseUtils 를 이용하여 RequestPath 객체를 얻는다. RequestPath 는 contextPath와 pathWithInApplication 두 메소드를 제공하는 인터페이스로 구현체는 스프링 어플리케이션의 Context Path와 해당 Context Path 이하 Full Path을 제공하는 기능을 한다.
  2. 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의 동작과정을 정리해보자.

  1. DispatcherServlet의 initHandlerMappings 메소드에서 ApplicationContext에서 이용 가능한 모든 HandlerMapping 구현체들을 받아온다.
  2. DispatcherServlet은 요청을 가로채고 이 요청을 처리할 1에서 등록한 HandlerMapping 들의 getHandler 메소드를 호출한다.
  3. RequestMappingHandlerMapping의 getHandler(AbstractHandlerMapping에 정의) 메소드가 호출된다.
  4. RequestMappingHandlerMapping의 getHandlerInternal(AbstractHandlerMethodMapping에 정의)이 실행된다.
  5. mappingRegistry에서 요청 경로에 맞는 RequestMappingInfo를 탐색하여 getMatchingMappings(추상 메소드)가 실행되며 Match 정보가 추가된다.
  6. getMappingComparator(추상 메소드) 를 이용하여 가장 잘 맞는 Match를 선정한다.
  7. Best Match 내부 registration 정보에서 HandlerMethod를 반환한다.
  8. getHandlerExecutionChain 메소드가 호출되어 HandlerExecutionChain이 반환된다.
  9. getHandler 메소드가 종료되며 HandlerExecutionChain이 반환된다.

여기까지가 Request가 들어온 직후 HandlerExecutionChain 반환까지의 과정이다.

AbstractHandlerMethodMapping을 보다 Controller 어노테이션이 달려있는 컨트롤러 클래스가 언제 등록되는 것인가? 에 대한 의문이 생긴다. 그 과정은 InitializingBean 인터페이스 구현부인 afterPropertiesSet 메소드에 있다.

Spring 에 의해 Bean 초기화 과정(생성자를 통한 생성)이 끝나고 난 뒤 afterPropertiesSet 메소드가 호출된다. AbstractHandlerMethodMapping에서는 아래의 순서로 HandlerMethod가 등록이 된다.

  1. afterPropertiesSet 메소드에서 initHandlerMethods 메소드를 호출한다.
  2. initHandlerMethods 내부에서 getCandidateBeanNames 메소드를 호출하여 Object.class 타입의 모든 Bean의 이름을 반환 받는다.
  3. initHandlerMethods 내부에서 "scopedBeanTarget." 으로 시작하지 않는 모든 빈들을 대상으로 processCandidatesBean을 호출한다.
  4. 해당 beanName을 가진 Bean의 타입이 Handler인지 체크하고 Handler라면(즉 @Controller 어노테이션이 붙은 클래스) detectHandlerMethods 메소드를 호출한다.
  5. detectHandlerMethods 내부에서 RequestMappingInfo List를 생성하고 실행 가능한 메소드들을 registerHandlerMethod를 통해 등록한다.
  6. registerHandlerMethod 는 결국 mappingRegistry에 RequestMappingInfo로 MappingRegistration<RequestMappingInfo> 을 등록하고, pathName으로 RequestMapping을 검색할 수 있도록 등록하는 과정을 거친다.

위 과정을 통해 Handler에서 처리할 수 있는 HandlerMethod 들이 등록되고 HttpServletRequest가 들어왔을 때, DispatcherServlet에서 RequestMappingHandlerMapping이 mappingRegistry에서 해당 요청을 처리할 HandlerMethod를 반환받아 HandlerExecutionChain 을 형성한다.

다음 포스트에서는 HandlerAdapter에 대해 알아본다.