Programing/Framework

ServletModelAttributeMethodProcessor 와 @PathVariable

나모찾기 2021. 8. 22. 11:06

ServletModelAttributeMethodProcessor 은 ModelAttributeMethodProcessor 상속받아 서블릿을 위한 위한 구현체이다.

가령 Spring MVC 에서 @PathVariable 을 이용해 URI 템플릿을 파라미터로 받아올 때 ServletModelAttributeMethodProcessor 가 사용된다.

사례

예로 아래와 같은 코드가 있을 때 requestNo 과 memberNo 에는 URI path의 템플릿 값이 들어오게 된다.

@RestController
public class VPNController {

    @GetMapping(value = "/vpn/{memberNo}/{requestNo}", produces = MediaType.APPLICATION_JSON_VALUE)
    public VpnResult getVpn(
            @PathVariable("requestNo") String requestNo,
            @PathVariable("memberNo") String memberNo,

 

만약 GET /vpn/namo/4567 처럼 요청이 왔다면 URI 템플릿이 /vpn/{memberNo}/{requestNo} 이므로,
requestNo 에는 4567가 memberNo 에는 namo 라는 값이 들어오게 된다.

 

위의 예에서 일부러 path 의 순서와 파라미터의 순서를 다르게 했음에도 값이 잘 들어온다.

그 이유는 템플릿의 이름을 기반으로 맵에서 값을 꺼내오기 때문이다.

 

ModelAttributeMethodProcessor 의 resolveConstructorArgument구현을 보면

먼저 부모 클래스인 ModelAttributeMethodProcessor 에게 물어보고 결과가 없을 경우에 fall-back을 하도록 구현이 되어 있다.

(spring-web-5.3.9 / spring boot 2.5.4 기준)

package org.springframework.web.servlet.mvc.method.annotation;

public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {
	// ..
	@Override
	@Nullable
	public Object resolveConstructorArgument(String paramName, Class<?> paramType, NativeWebRequest request)
			throws Exception {
		// 1. ModelAttributeMethodProcessor 에 먼저 요청
		Object value = super.resolveConstructorArgument(paramName, paramType, request);
		if (value != null) {
			return value;
		}
		// 2. fall-back
		ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
		if (servletRequest != null) {
			String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
			@SuppressWarnings("unchecked")
			Map<String, String> uriVars = (Map<String, String>) servletRequest.getAttribute(attr);
			return uriVars.get(paramName);
		}
		return null;
	}

실제 저장은 Map<string, string> 의 형태

fall-back 부분을 더 살펴보자.
템플릿 URI을 통해 매핑이 되면 스프링 MVC는 ServletRequest 에 attributes 에 Map<String, String> 형태의 파라미터 이름과 값을 저장해놓는다.

 

Tomcat의 서블릿 구현체 카탈리나(catalina)의 경우 attributes를 ConcurrentHashMap 로 구현을 해놓았다.

Servlet 스펙:

package javax.servlet;

public interface ServletRequest {

    public Object getAttribute(String name);
    public void setAttribute(String name, Object o);
    // ..

Catalina 구현체:

package org.apache.catalina.connector;

public class Request implements HttpServletRequest {
    // ..
    private final Map<String, Object> attributes = new ConcurrentHashMap<>();

    @Override
    public Object getAttribute(String name) {
        // Special attributes
        SpecialAttributeAdapter adapter = specialAttributes.get(name);
        if (adapter != null) {
            return adapter.get(this, name);
        }

        Object attr = attributes.get(name);
        if (attr != null) {
            return attr;
        }
        // ..
    }

    @Override
    public void setAttribute(String name, Object value) {
        // prameters validation...
        // Special attributes
        SpecialAttributeAdapter adapter = specialAttributes.get(name);
        if (adapter != null) {
            adapter.set(this, name, value);
            return;
        }

        // ..
        Object oldValue = attributes.put(name, value);
        // ..
    }

attributes 에 저장되는 URL 관련 값들

이 attribute의 키는 HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE 에 정의되어 있는 값으로 정해져있다.

package org.springframework.web.servlet;

public interface HandlerMapping {
	// ..
	String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";

HandlerMapping.class.getName() 는 org.springframework.web.servlet.HandlerMapping 이므로

org.springframework.web.servlet.HandlerMapping.uriTemplateVariables 가 된다.

 

그래서 위의 코드의 예를 들어보면 attributes 에는 아래와 같은 값들이 들어있다.

key (접두어는 생략) value value의 value
[prefix].bestMatchingPattern /vpn/{memberNo}/{requestNo}  
[prefix].pathWithinHandlerMapping /vpn/namo/4567  
[prefix].uriTemplateVariables LinkedHashMap memberNo -> namo
requestNo -> 4567

More Deep Dive

InvocableHandlerMethod#getMethodArgumentValues

→ HandlerMethodArgumentResolverComposite#resolveArgument

→ HandlerMethodArgumentResolver#resolveArgument

→ ModelAttributeMethodProcessor#resolveArgument → createAttribute

→ ServletModelAttributeMethodProcessor#createAttribute → super#createAttribute

= ModelAttributeMethodProcessor#createAttribute → constructAttribute → resolveConstructorArgument

= ServletModelAttributeMethodProcessor#resolveConstructorArgument (이 부분이 이 글의 두 번째 코드)

같이보기