관리 메뉴

생각해보기

스프링 request와 경로 매핑 본문

Spring

스프링 request와 경로 매핑

정한_s 2023. 7. 7. 18:41

스프링에서는 요청에 매칭되는 다양한 매개변수 들이 있습니다. 대표적으로 다음과 같은 매개변수가 존재합니다.

  1. @RequestParam: 클라이언트로부터 전송된 파라미터 값을 Handler(Method)의 파라미터로 사용할 수 있게 매핑해주는 어노테이션입니다. 예를 들어, GET 방식의 요청에 ?name=example이라는 쿼리 문자열이 있다면, @RequestParam("name") String name으로 해당 값을 받을 수 있습니다. 필수 여부를 설정할 수 있으며, 기본값은 true입니다. 필수가 아닌 경우 @RequestParam(required = false)를 사용할 수 있습니다.
  2. @RequestBody: 클라이언트로부터 전송된 JSON 혹은 XML 등의 데이터를 Java Object로 변환해서 사용할 수 있게 하는 어노테이션입니다. 주로 POST, PUT 요청시 사용되며, 요청 본문을 HTTP 메시지 컨버터를 통해 해당 타입의 객체로 변환하고 Handler의 파라미터로 전달합니다.
  3. @PathVariable: URL의 경로에서 특정 값을 추출하여 Handler(Method)의 파라미터로 전달할 수 있게 하는 어노테이션입니다. 예를 들어, "/users/{id}"라는 URL이 있을 때, @PathVariable("id") int id처럼 사용해서 해당 값을 받을 수 있습니다.
  4. @ModelAttribute: 요청 파라미터를 객체로 매핑하여 사용할 수 있게 하는 어노테이션입니다. 예를 들어, 사용자로부터 받은 여러 요청 파라미터를 특정 객체에 바로 매핑해서 사용할 수 있습니다. 해당 어노테이션은 생략할 수 있습니다.

그렇다면 해당 request에 따라 어떤 클래스의 어떤 메소드에 매핑되는지, 또한 스프링은 request에 맞는 정보를 어떻게 관리하는 지 궁금해졌습니다. 

DispatcherServlet.doDispatch() 핵심 로직 분석

메서드 이름대로 해당 메서드에서 적절한 컨트롤러를 찾아 매핑해주고 뷰까지 찾아 View를 반환해 렌더링까지 해주는 핵심 메서드입니다.

protected void doDispatch(HttpServletRequest request, 
                              HttpServletResponse response) throws Exception {

    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;

    // 1. 핸들러 조회
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    //2.핸들러 어댑터 조회-핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    /**
     * 3. 핸들러 어댑터 실행 
     * -> 4. 핸들러 어댑터를 통해 핸들러 실행 
     * -> 5. ModelAndView 반환 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
     */
		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

private void processDispatchResult(HttpServletRequest request,
                                   HttpServletResponse response, 
                                   HandlerExecutionChain mappedHandler, 
                                   ModelAndView mv, Exception exception) throws Exception {
    // 뷰 렌더링 호출
    render(mv, request, response);
}

protected void render(ModelAndView mv, HttpServletRequest request,
                      HttpServletResponse response) throws Exception {
    View view;
    String viewName = mv.getViewName(); //6. 뷰 리졸버를 통해서 뷰 찾기,7.View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
    // 8. 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
}

이 중 요청에 맞는 특정 핸들러를 어떻게 찾는 지, getHandler와 관련 로직을 집중해서 보고자 합니다.

getHandler

요청을 우선 처리하기 위해서 어떤 핸들러을 사용하는지 정해야 합니다. DispatcherServlet에서 getHandler 함수를 통해 우선 순위 대로 결정합니다.

	// DispatcherServlet.java 일부 
	/**
	 * Return the HandlerExecutionChain for this request.
	 * <p>Tries all handler mappings in order.
	 * @param request current HTTP request
	 * @return the HandlerExecutionChain, or {@code null} if no handler could be found
	 */
	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

우선 순위는 다음과 같습니다 (3.1 기준)

  1. RequestMappingHandlerMapping @RequestMapping 어노테이션이 붙은 메소드를 찾아 요청을 처리합니다. HTTP 요청 정보를 기반으로 @RequestMapping 어노테이션과 매칭하는 가장 적합한 메소드를 찾습니다.
  2. WelcomePageHandlerMapping 스프링 부트에서 사용되며, 애플리케이션의 루트 경로에 대한 요청을 처리하고 정적 콘텐츠나 'index.html' 같은 웰컴 페이지로 리다이렉트하는 역할을 합니다.
  3. BeanNameUrlHandlerMapping Bean의 이름을 URL 경로에 직접 맵핑합니다. 이 핸들러 맵핑을 사용하면, 빈의 이름이 '/myBean'인 경우, 이 빈은 '/myBean' URL에 매핑됩니다.
  4. RouterFunctionMapping 스프링 웹플럭스(Spring 5부터 도입된 반응형 웹 프레임워크)에서 사용됩니다. RouterFunction 인터페이스를 구현한 라우팅 규칙을 처리합니다.
  5. WelcomePageNotAcceptableHandlerMapping 스프링 부트에서 사용되며, WelcomePageHandlerMapping과 유사하게 웰컴 페이지로의 요청을 처리합니다. 주로 요청 헤더의 'Accept' 필드가 HTML을 허용하지 않는 경우에 사용됩니다
  6. SimpleUrlHandlerMapping URL 패턴을 특정 핸들러 빈에 명시적으로 맵핑합니다. 프로퍼티 설정 파일이나 XML 파일을 통해 URL 패턴과 핸들러 빈 간의 맵핑을 설정할 수 있습니다.

그 중에 가장 많이 사용하는 RequestMappingHandlerMapping을 살펴보겠습니다.

	// AbstarctHandlerMethodMapping.java	

	/**
	 * Look up a handler method for the given request.
	 */
	@Override
	@Nullable
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		String lookupPath = initLookupPath(request);
		this.mappingRegistry.acquireReadLock();
		try {
	    // 최적의 매칭된 HandlerMethod를 찾습니다 
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

/**
	 * Look up the best-matching handler method for the current request.
	 * If multiple matches are found, the best match is selected.
	 * @param lookupPath mapping lookup path within the current servlet mapping
	 * @param request the current request
	 * @return the best-matching handler method, or {@code null} if no match
	 * @see #handleMatch(Object, String, HttpServletRequest)
	 * @see #handleNoMatch(Set, String, HttpServletRequest)
	 */
	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		// mapping registry 와 바로 매칭되는 path를 찾습니다. path 고정인것 
		if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
		// mapping registry 와 바로 매칭되는 path를 찾습니다. path 유동인것
		if (matches.isEmpty()) {
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		}
		// 최적의 매칭되는 path에 대한 HandlerMethod를 선택합니다.
		if (!matches.isEmpty()) {
		 ...
		}
		else {
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		}

mappingRegistry에 다양한 핸들러 매핑 정보가 들어가 있고, 해당 mappingRegistry에 있는 정보를 가지고 적절한 핸들러 매핑을 반환합니다.

예시

GET /params/1 요청 시 mappingRegistry 일부 정보

 

그렇다면 해당 mappingRegistry는 언제 만들어질까?

처음 어플리케이션 실행시에 만들어집니다.

// AbstractHandlerMethodMapping.java
	/**
	 * Look for handler methods in the specified handler bean.
	 * @param handler either a bean name or an actual handler instance
	 * @see #getMappingForMethod
	 */
	protected void detectHandlerMethods(Object handler) {
	  // HandlerMapping에 관련된 클래스를 블러온다
		Class<?> handlerType = (handler instanceof String beanName ?
				obtainApplicationContext().getType(beanName) : handler.getClass());
	.....
			// method를 등록한다
			methods.forEach((method, mapping) -> {
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				registerHandlerMethod(handler, invocableMethod, mapping);
			});

	 /**
	 * Register a handler method and its unique mapping. Invoked at startup for
	 * each detected handler method.
	 * @param handler the bean name of the handler or the handler instance
	 * @param method the method to register
	 * @param mapping the mapping conditions associated with the handler method
	 * @throws IllegalStateException if another method was already registered
	 * under the same mapping
	 */
	protected void registerHandlerMethod(Object handler, Method method, T mapping) {
		this.mappingRegistry.register(mapping, handler, method);
	}

결론

  • 요청을 우선 처리하기 위해서 어떤 핸들러을 사용하는지 정해야 하며, 우선순위에 따라 적절한 핸들러가 정해진다.
  • RequestMappingHandlerMapping은 mappingRegistry의 정보에 따라 가장 잘 맞는 핸들러 매핑을 선택한다
  • mappingRegistry의 정보는 어플리케이션 시작시에 등록되며, RequestMappingHandlerMapping의 경우 핸들러에 연관된 클래스의 메소드를 기반으로 mappingRegistry를 등록한다

'Spring' 카테고리의 다른 글

Autowire 동작 방식 알아보기  (0) 2023.08.05
Transactional 어노테이션에 대해 알아보자  (0) 2023.07.21
springjpa 정리 -2-  (0) 2022.01.07
spring jpa 정리 -1-  (0) 2022.01.06
spring security & oauth2.0 & jwt 간단 정리  (0) 2022.01.04
Comments