코코코딩공부/Spring

[Spring] Argument Resolver 내부 구경 하기

Ocean_ 2023. 4. 30. 13:00

사용자가 컨트롤러의 메서드 인자값으로 임의의 값을 전달하려할 때 사용된다.

 

Argument Resolver를 Controller 단에서 사용하면 중복 코드(HttpSession에서 세션 로드, HttpServletRequest에서 요청 url 및 ip 정보 로드 등)를 깔끔하게 처리할 수 있다.


RequestMapping에 대한 매칭 (RequestMappingHandlerAdapter가 수행) 이후

Interceptor 처리되고 그 후에

Argument Resolver가 파라메터를 처리한다.


언제 사용하면 좋을까?

내가 생각한 몇가지 예시는 다음과 같다.

 

1. 프로덕션 코드에서 코드 구조를 드러내기 싫을 때

Ex) 암호화 -> 복호화 하는 숨기고싶은 구조가 있을 때

 

2. 파라메터로 받는 인자를 다르게 받고 싶을 때

Ex) 유저 토큰을 받는데 토큰 값을 분해해서 하기 보다는 토큰 안에 있는 값을 처음부터 받아서 처리하고 싶을 때


쓰면 왜 좋을까?

위에서 말한 예시들의 장점이다.

컨트롤러에서 파라미터 가공추가수정할 수 있지만 이방식은 중복코드를 양산한다. 이를 줄이고자 할 때 사용하면 좋을 것 같다.

 


내부 작동

Spring은 요청을 컨트롤러의 메서드를 wrapping한 InvocableHandlerMethod invokeForRequest 메서드로 처리한다.

@Nullable
	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
		if (logger.isTraceEnabled()) {
			logger.trace("Arguments: " + Arrays.toString(args));
		}
		return doInvoke(args);
	}

getMethodArgumentValues를 보면 

 

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
    Object... providedArgs) throws Exception {
    ...
    for (int i = 0; i < parameters.length; i++) {
    ...
    if (!this.resolvers.supportsParameter(parameter)) {
        throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
    }
    try {
        args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
    }
}

여기서는 supportsParameter를 통해 이 타입,파라미터가 처리되어질 수 있는지 확인한다. 

그리고 밑에 try문을 보면 알 수 있듯이 여러개의 파라미터가 있다면 각각의 파라미터에 대해서 하나하나씩 resolveArgument를 통해 인자를 처리해서 저장한다.

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    }
    return result;
}

위의 반복문에서 argumentResolver를 순회하면서 찾는데 이는 어디에 등록되있는걸까 ?

requestMappingHandler Adapter이다. 여기서 getDefaultInitBinderArgumentResolvers를 통해 HandlerMethodArgumentResolverComposite().addResolvers(resolvers);를 사용하여 값을 넣어주는 것을 확인할 수 있다.

if (this.initBinderArgumentResolvers == null) {
    List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
    this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);

		// Annotation-based argument resolution
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
		resolvers.add(new RequestParamMapMethodArgumentResolver());
		resolvers.add(new PathVariableMethodArgumentResolver());
		resolvers.add(new PathVariableMapMethodArgumentResolver());
		resolvers.add(new MatrixVariableMethodArgumentResolver());
		resolvers.add(new MatrixVariableMapMethodArgumentResolver());
		resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
		resolvers.add(new SessionAttributeMethodArgumentResolver());
		resolvers.add(new RequestAttributeMethodArgumentResolver());

		// Type-based argument resolution
		resolvers.add(new ServletRequestMethodArgumentResolver());
		resolvers.add(new ServletResponseMethodArgumentResolver());

		// Custom arguments
		if (getCustomArgumentResolvers() != null) {
			resolvers.addAll(getCustomArgumentResolvers());
		}

		// Catch-all
		resolvers.add(new PrincipalMethodArgumentResolver());
		resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));

		return resolvers;
	}

엄청나게 많은 argument resolver들이 있는데 이중에 type에 맞는 리솔버를 사용한다.!

 

그중에 

PathVariableMapMethodArgumentResolver

만 한번 보려고 한다.

@Override
public boolean supportsParameter(MethodParameter parameter) {
    PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
    return (ann != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
            !StringUtils.hasText(ann.value()));
}

Pathvariable인지 확인하여 처리 할 수 있는지 확인해 주는 함수이다 !

그리고 이런 방식을 통해 처리가 된다! 여기서 처리되는 것만 알고 있다 ..

@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    @SuppressWarnings("unchecked")
    Map<String, String> uriTemplateVars =
            (Map<String, String>) webRequest.getAttribute(
                    HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);

    if (!CollectionUtils.isEmpty(uriTemplateVars)) {
        return new LinkedHashMap<>(uriTemplateVars);
    }
    else {
        return Collections.emptyMap();
    }
}

잘 뜯어봤는지 모르겠다.. 생각보다 안에 까지 보는게 재밌다