ServletModelAttributeMethodProcessor 와 @PathVariable
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 (이 부분이 이 글의 두 번째 코드)