Custom Handler Method ArgumentResolver 만들어보기

@Frank Oh · July 20, 2020 · 7 min read

1.HandlerMethodArgumentResolver란?
1.1 들어가면
2. Custom Argument Resolver 만들기
2.1 Argument Resolver 컨트롤러에 사용예제
2.2 Argument Resolver 생성하기
2.3 Argument Resolver 등록하기
2.4 Controller 실행하기
3. Argument Resolver 동작 방식
4. 마무리
5. 참고

1.HandlerMethodArgumentResolver란?

1.1 들어가면

HandlerMethodArgumentResolver에 대해서 알아보자. 아래와 같이 컨트롤러 메서드에 여러 인자 값(ex. @PathVariable)을 추가하여 자주 작업을 한다. 이런 인자는 HandlerMethodArgumentHandler에 의해서 처리가 된다.

필요에 따라서 컨트롤러 메서드에 여러 인자 값을 추가하는데 이런 인자는 HandlerMethodArgumentHandler에 의해서 처리가 된다.

@GetMapping
public ResponseEntity<?> getStudentList(
  @PathVariable(value = "version") Integer version,
  @RequestParam(value = "listSize", defaultValue = "10") Integer listSize) {
  ...생략...
}

HandlerMethodArgumentHandler는 어노테이션이나 타입에 따라서 실제 값을 컨트롤러에 넘겨주는 역할을 한다. 스프링에서도 기본적으로 여러 Argument Resolver가 구현되어 있다.

  • PathVariableMethodArgumentResolver

    • @PathVariable 어노테이션으로 선언된 인자를 처리하는 Argument Resolver이다
  • RequestParamMethodArgumentResolver

    • @RequestParam 어노테이션으로 선언된 인자의 실제 값을 지정해 준다
  • RequestHeaderMapMethodArgumentResolver

    • @RequestHeader 어노테이션으로 선언된 인자의 실제 값을 지정해 준다

HandlerMethodArgumentHandler을 사용하게 되면 중복 코드를 줄이고 공통 기능으로 빼서 사용할 수 있는 장점이 있다. 이제 Custom HandlerMethodArgumentResolver를 직접 구현해보도록 하자.

2. Custom Argument Resolver 만들기

2.1 Argument Resolver 컨트롤러에 사용예제

컨트롤러 메서드에서 @ClientIp 어노테이션이 추가된 인자를 넘겨주면 Client Ip 주소를 얻어 올 수 있는 Resolver를 만들어보자.

@RestController
public class IpController {
    @GetMapping("/test")
    public String getIpAddress(@ClientIp String clientIp) {
        return String.format("ip address : %s", clientIp);
    }
}

2.2 Argument Resolver 생성하기

Argument Resolver 인터페이스에는 2가지 메서드가 존재하고 supportsParameter()가 참인 경우에 resolveArgument() 메서드를 실행한다.

public interface HandlerMethodArgumentResolver {

	boolean supportsParameter(MethodParameter parameter);

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

}
메서드 설명
supportsParameter 현재 parameter를 resolver가 지원할지 true/false로 반환한다
resolveArgument 실제 바인딩할 객체를 반환한다
@Slf4j
@Component
public class ClientIpArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(ClientIp.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();

        String clientIp = request.getHeader("X-Forwarded-For");
        if (StringUtils.isEmpty(clientIp) || "unknown".equalsIgnoreCase(clientIp)) {
            clientIp = request.getRemoteAddr();
        }
        log.debug("[debug] clientIp : {}", clientIp);
        return clientIp;
    }
}
  • supportsParameter() 메서드에서는 인자 값에 ClientIp 어노테이션이 포함되어 있는 지 확인한다
  • resolveArgument() 메서드에서는 실제 client Ip 주소를 request에서 얻어 오는 로직이 있다

2.3 Argument Resolver 등록하기

이제 앞에서 생성한 Resolver를 addArgumentResolvers() 메서드에서 추가해주면 된다.

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final ClientIpArgumentResolver clientIpArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(clientIpArgumentResolver);
    }
}

2.4 Controller 실행하기

Unit Test로 확인해보자. 컨트롤러에서 Client Ip 주소를 잘 반환해주고 있다.

@Test
void getIpAddress() throws Exception {
  this.mockMvc.perform(get("/test"))
  .andDo(print())
  .andExpect(status().isOk())
  .andExpect(jsonPath("$", is("ip address : 127.0.0.1")));
}

3. Argument Resolver 동작 방식

Custom Argument Resolver를 구현해보았다. 이제 스프링안에 내부적으로 어떻게 Argument Resolver가 호출되는지 알아보자. Argument Resolver 동작 구조를 쉽게 보여주는 이미지(Carrey's 기술 블로그에서 참고)이다.

Dispatch-Seq

Request 처리시 Argument Resolver가 실행되는 순서는 크게 보면 아래와 같다. 실제 실행시 debug해서 확인하면 더 쉽게 이해할 수 있다.

  1. Client에서 Request 요청을 보낸다
  2. 요청은 Dispatcher Serlvet에서 처리가 된다
  3. 요청에 대한 HandlerMapping 처리

    1. (스프링 구동시) RequestMappingHandlerAdapter에서 필요한 Argument resolver를 등록한다 (#1.2.1)
    2. (요청시) RequestMappingHandlerAdapter.invokeHandlerMethod()에서 Argument resolver를 실행한다 (#1.2.2)

      1. DispatcherServlet.doDispatch() -> RequestMappingHandlerAdapter.handleInternal() -> invokeHandlerMethod()
  4. 컨트롤러 메서드 실행

1.2.1 스프링 기본 + Custom Argument Resolver은 어디서 등록이 되나?

RequestMappingHandlerAdapter 객체가 초기화(ex. 스프링 시작시) 될 때 afterPropertiesSet()에서 getDefaultArgumentResolvers() 메서드를 호출하여 기본 스프링과 Custom resolver를 등록한다.

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
		...생략...
      
		resolvers.add(new PathVariableMethodArgumentResolver());
		resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
		resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));

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

1.2.2 supportsParameter는 어디에서 호출되나?

HandlerMethodArgumentResolverComposite.getArgumentResolver() 메서드에서 supportsPameter() 실행해서 true를 반환하면 해당 Argument Resolver를 반환한다.

private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
				if (resolver.supportsParameter(parameter)) { //true이면 resolveArgument를 결과로 반환함
					result = resolver;
					this.argumentResolverCache.put(parameter, result);
					break;
				}
			}
		}
		return result;
}

getArgumentResolver() 메서드는 아래 여러 메서드에 의해서 호출된다.

  • DispatcherServlet.doDispatch() -> ...생략... -> invokeHandlerMethod() -> ...생략... -> InvocableHandlerMethod.getMethodArgumentValues() -> getArgumentResolver() 순으로 실행되는 것을 확인할 수 있다.

image 20200912154932896

4. 마무리

HandlerMethodArgumentResolver는 컨트롤러 메서드에서 인자 값에 대한 처리를 위해 사용된다. 이미 스프링에서 공통기능으로 많이 제공하고 있지만, 사용자용 메서드도 쉽게 작성하여 중복 로직을 많이 줄일 수 있어 용의하게 사용된다.

관련 소스는 github에 올려두어서 참고하시면 됩니다.

5. 참고

@Frank Oh
안녕하세요. 방문해주셔서 감사합니다. 컴퓨터 관련 스터디한 내용 기록하는 블로그입니다.