@RequestMapping 이 선언되면 어떻게 Api를 호출할 수 있는 메소드가 될까

tokkaiiii·2025년 4월 27일

spring-mvc

목록 보기
5/27

(1) 서블릿 호출

서블릿 호출은 DispatcherServlet 클래스를 찾자.
그리고 doDispatch 메소드가 실행된다.
doDispatch 메소드는 파라미터로 HttpServletRequest와 HttpServletResponse 타입을 받는다.
즉, 요청과 응답처리를 하겠다는 말이다.

(2) Handler를 찾다

웹 어플리케이션 서버를 구축하면 우리는 클라이언트와 소통을 할 수 있는 인터페이스를 구축한다. 인터페이스는 클라이언트가 우리 서비스에 접속하도록 연결하는 단자라고 보면 된다.
자, 클라이언트는 URL로 요청을 하면 @Controller가 선언된 클래스에 있는 메소드를 실행하여 응답을 보내준다.
여기서 요청을 받는 메소드는 Handler다. 그리고 이 Handler를 찾는 것은 DispatcherServlet 클래스의 doDispatch 메소드에서 일어난다.

(3) RequestMappingHandlerMapping 으로 핸들러를 찾아보자

RequestMappingHandlerMapping 클래스는 RequestMappingInfoHandlerMapping 추상클래스를 상속한다.
이미지는 그 상속 관계를 설명하고 있다.
@Controller 가 선언된 방식은 RequestMappingHandlerMapping 클래스로 Handler를 저장하고 가져올 수 있다. 다른 매핑클래스도 존재하지만 언급한 HanlderMapping 클래스를 주로 사용한다.
어플리케이션을 실행해보자.
RequestMappingHandlerMapping 클래스에서 afterProperitesSet 메소드가 실행된다. 이 메소드는 스프링에서 bean으로 등록됐으면 자동으로 호출되는 메소드다.

// RequestMappingHandlerMapping.class
 public void afterPropertiesSet() {
    this.config = new RequestMappingInfo.BuilderConfiguration();
    this.config.setTrailingSlashMatch(this.useTrailingSlashMatch());
    this.config.setContentNegotiationManager(this.getContentNegotiationManager());
    if (this.getPatternParser() != null && this.defaultPatternParser && (this.useSuffixPatternMatch || this.useRegisteredSuffixPatternMatch)) {
      this.setPatternParser((PathPatternParser)null);
    }

    if (this.getPatternParser() != null) {
      this.config.setPatternParser(this.getPatternParser());
      Assert.isTrue(!this.useSuffixPatternMatch && !this.useRegisteredSuffixPatternMatch, "Suffix pattern matching not supported with PathPatternParser.");
    } else {
      this.config.setSuffixPatternMatch(this.useSuffixPatternMatch());
      this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch());
      this.config.setPathMatcher(this.getPathMatcher());
    }

    super.afterPropertiesSet();
  }

메소드 마지막에서 super 즉, 부모의 afterPropertiesSet() 메소드를 호출한다.

// AbstractHandlerMapping.class
public void afterPropertiesSet() {
    this.initHandlerMethods();
  }

  protected void initHandlerMethods() {
    for(String beanName : this.getCandidateBeanNames()) {
      if (!beanName.startsWith("scopedTarget.")) {
        this.processCandidateBean(beanName);
      }
    }

    this.handlerMethodsInitialized(this.getHandlerMethods());
  }
  
   protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;

    try {
      beanType = this.obtainApplicationContext().getType(beanName);
    } catch (Throwable ex) {
      if (this.logger.isTraceEnabled()) {
        this.logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
      }
    }

    if (beanType != null && this.isHandler(beanType)) {
      this.detectHandlerMethods(beanName);
    }

  }

  protected void detectHandlerMethods(Object handler) {
    Class var10000;
    if (handler instanceof String beanName) {
      var10000 = this.obtainApplicationContext().getType(beanName);
    } else {
      var10000 = handler.getClass();
    }

    Class<?> handlerType = var10000;
    if (handlerType != null) {
      Class<?> userType = ClassUtils.getUserClass(handlerType);
      Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (method) -> {
        try {
          return this.getMappingForMethod(method, userType);
        } catch (Throwable ex) {
          throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex);
        }
      });
      if (this.logger.isTraceEnabled()) {
        this.logger.trace(this.formatMappings(userType, methods));
      } else if (this.mappingsLogger.isDebugEnabled()) {
        this.mappingsLogger.debug(this.formatMappings(userType, methods));
      }

      methods.forEach((method, mapping) -> {
        Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
        this.registerHandlerMethod(handler, invocableMethod, mapping);
      });
    }

  }

(1) Handler 가 존재하는 클래스의 조건

메소드 흐름 속에서 if 조건으로 isHanlder 메소드로 Handler인지 검증을 한다. 이 메소드는 각 구체적인 클래서에 정의되어 있고 RequestMappingHandlerMapping 클래스가 동착하므로 해당 클래스의 메소드를 참고해야 한다.

// RequestMappingHandlerMapping.class
 protected boolean isHandler(Class<?> beanType) {
    return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
  }

bean을 순회하면서 @Controller 가 붙은 bean 만을 추출한다.

(2) Handler 를 찾았으면 메소드를 추출하자

// AbstractHandlerMethodMapping.class
protected void detectHandlerMethods(Object handler) {
    Class var10000;
    if (handler instanceof String beanName) {
      var10000 = this.obtainApplicationContext().getType(beanName);
    } else {
      var10000 = handler.getClass();
    }

    Class<?> handlerType = var10000;
    if (handlerType != null) {
      Class<?> userType = ClassUtils.getUserClass(handlerType);
      Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (method) -> {
        try {
          return this.getMappingForMethod(method, userType);
        } catch (Throwable ex) {
          throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex);
        }
      });
      if (this.logger.isTraceEnabled()) {
        this.logger.trace(this.formatMappings(userType, methods));
      } else if (this.mappingsLogger.isDebugEnabled()) {
        this.mappingsLogger.debug(this.formatMappings(userType, methods));
      }

      methods.forEach((method, mapping) -> {
        Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
        this.registerHandlerMethod(handler, invocableMethod, mapping);
      });
    }

  }

이 과정에서 RequestMappingInfo 객체를 생성한다.

// RequestMappingHandlerMapping.class
 @Nullable
  private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    RequestMappingInfo requestMappingInfo = null;
    RequestCondition var10000;
    if (element instanceof Class<?> clazz) {
      var10000 = this.getCustomTypeCondition(clazz);
    } else {
      var10000 = this.getCustomMethodCondition((Method)element);
    }

    RequestCondition<?> customCondition = var10000;
    List<AnnotationDescriptor> descriptors = getAnnotationDescriptors(element);
    List<AnnotationDescriptor> requestMappings = descriptors.stream().filter((desc) -> desc.annotation instanceof RequestMapping).toList();
    if (!requestMappings.isEmpty()) {
      if (requestMappings.size() > 1 && this.logger.isWarnEnabled()) {
        this.logger.warn("Multiple @RequestMapping annotations found on %s, but only the first will be used: %s".formatted(element, requestMappings));
      }

      requestMappingInfo = this.createRequestMappingInfo((RequestMapping)((AnnotationDescriptor)requestMappings.get(0)).annotation, customCondition);
    }

    List<AnnotationDescriptor> httpExchanges = descriptors.stream().filter((desc) -> desc.annotation instanceof HttpExchange).toList();
    if (!httpExchanges.isEmpty()) {
      Assert.state(requestMappingInfo == null, () -> "%s is annotated with @RequestMapping and @HttpExchange annotations, but only one is allowed: %s".formatted(element, Stream.of(requestMappings, httpExchanges).flatMap(Collection::stream).toList()));
      Assert.state(httpExchanges.size() == 1, () -> "Multiple @HttpExchange annotations found on %s, but only one is allowed: %s".formatted(element, httpExchanges));
      requestMappingInfo = this.createRequestMappingInfo((HttpExchange)((AnnotationDescriptor)httpExchanges.get(0)).annotation, customCondition);
    }

    return requestMappingInfo;
  }

ReuqestMapping 어노테이션을 가지고 있는지 확인 후 빌더 패턴으로 Request 정보를 저장한다.

protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
    RequestMappingInfo.Builder builder = RequestMappingInfo
    	.paths(this.resolveEmbeddedValuesInPatterns(requestMapping.path()))
        .methods(requestMapping.method())
        .params(requestMapping.params())
        .headers(requestMapping.headers())
        .consumes(requestMapping.consumes())
        .produces(requestMapping.produces())
        .mappingName(requestMapping.name());
    if (customCondition != null) {
      builder.customCondition(customCondition);
    }

    return builder.options(this.config).build();
  }

그리고 selectMethods 메소드 호출 결과는 Map을 반환하는데
Key 엔 Method를 Value엔 RequestMappingInfo를 담아서 반환한다.
이 Map을 다시 register로 등록을 하는데 AbstractHandlerMathodMapping 클래스 내부에 있는 MappingRegistry 클래스가 가진 아래의 Map에 RequestMappingInfo를 Key로 넣고 MappingRegistry가 가진 내부 클래스인 MappingRegistration 클래스에 HandlerMethod 를 담아 Value에 넣어서 저장한다.

  private final Map<T, MappingRegistration<T>> registry = new HashMap();
// AbstractHandlerMethodMapping.class
public void register(T mapping, Object handler, Method method) {
     this.readWriteLock.writeLock().lock();

     try {
       HandlerMethod handlerMethod = AbstractHandlerMethodMapping.this.createHandlerMethod(handler, method);
       this.validateMethodMapping(handlerMethod, mapping);
       handlerMethod = handlerMethod.createWithValidateFlags();
       Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);

       for(String path : directPaths) {
         this.pathLookup.add(path, mapping);
       }

       String name = null;
       if (AbstractHandlerMethodMapping.this.getNamingStrategy() != null) {
         name = AbstractHandlerMethodMapping.this.getNamingStrategy().getName(handlerMethod, mapping);
         this.addMappingName(name, handlerMethod);
       }

       CorsConfiguration corsConfig = AbstractHandlerMethodMapping.this.initCorsConfiguration(handler, method, mapping);
       if (corsConfig != null) {
         corsConfig.validateAllowCredentials();
         corsConfig.validateAllowPrivateNetwork();
         this.corsLookup.put(handlerMethod, corsConfig);
       }

       this.registry.put(mapping, new MappingRegistration(mapping, handlerMethod, directPaths, name, corsConfig != null));
     } finally {
       this.readWriteLock.writeLock().unlock();
     }

   }

자, 여기 까지가 스프링이 @Controller에 선언된 Handler를 저장하는 과정이다.

profile
풀스택 자바 개발자입니다

0개의 댓글