Spring

Method Arguments 들의 동작 방법

자바생 2023. 4. 25. 11:11
728x90

 

글을 쓰게 된 이유

 

 

스프링 미션을 하면서 HTTP GET 요청 시, 파라미터(쿼리 스트링)를 객체에 바인딩해주는 어노테이션이 어떤 어노테이션인지 알아보기 위해 글을 작성하게 됐습니다.

 

 

 

 

ProductDao 에서는 @JdbcTest를,

ProductQueryService , ProductCommandService 에서는 @SpringBootTest를 통해서 통합 테스트를 하고 있습니다.

 

 

 

 

 

먼저 코드를 보겠습니다.

public class GameInfoRequest {

    @Size(min = 2)
    private String names;

    @Positive
    private int count;

    public GameInfoRequest() {
    }

    public String getNames() {
        return names;
    }

    public int getCount() {
        return count;
    }

    public void setNames(final String names) {
        this.names = names;
    }

    public void setCount(final int count) {
        this.count = count;
    }
}
@PostMapping("/plays")
public RaceResultResponse registerRaceResult(@Validated @RequestBody final GameInfoRequest gameInfoRequest) {
    return raceResultService.createRaceResult(gameInfoRequest);
}

@GetMapping("/test")
public void test(final GameInfoRequest request) {
    System.out.println("request = " + request.getNames());
    System.out.println("request.getCount() = " + request.getCount());
}

 

 

registerRaceResult 의 파라미터는 @RequestBody로 받기 때문에 내부적으로 jackson을 통해 역직렬화가 됩니다.

 

 

 

 

GET 요청 시에는 Body를 사용하지 않고 쿼리 스트링으로 데이터를 전달하게 됩니다.

 

 

 

 

test 메서드와 같이 어떤 어노테이션도 붙지 않은 상태에서는 데이터를 어떻게 바인딩하는지 알아보겠습니다.

 

 

 

 

디버깅

 

 

처음에 어디에 breakpoint를 찍어야할지 감이 오질 않았습니다.

 

 

 

공식 문서를 보다가 Handler의 종류에는 Method Argument, Return values 등이 있다고 했습니다.

 

 

 

그래서 MethodArgument 로 시작하는 클래스 중 RequestParamMethodArgumentResolver의 supportsParameter에 breakpoint를 걸었습니다.

 

 

 

supportsParameter 메서드는 무엇인가요?

 

 

 

Method Argument의 최상위 인터페이스는 HandlerMethodArgumentResolver입니다.

 

 

 

해당 인터페이스에는 추상 메서드로 supportsParameter, resolveArgument를 가지고 있습니다.

즉, supportsParameter는 전달된 메서드 매개변수가 해당 resolver에서 지원되는지 확인하는 기능을 합니다.

 

 

 

 

 

그래서 위 코드에서는 RequestParam, RequestPart 어노테이션을 가지고 있지 않기 때문에 supportsParameter에서는 false를 반환합니다.

 

그리고 resolverArgument는 supportsParameter 메서드에서 true를 반환하면 데이터를 바인딩하고 나서 바인딩 된 값을 반환합니다.

 

 

 

HandlerMethodArgumentResolverComposite

 

 

 

 

그리고 나서 HandlerMethodArgumentResolverComposite 로 이동합니다.

 

 

해당 클래스의 역할은 공식문서에서 아래와 같이 나옵니다.

 

 

Resolves method parameters by delegating to a list of registered HandlerMethodArgumentResolvers. Previously resolved method parameters are cached for faster lookups

 

 

 

결국 등록된 MethodArgumentResolve들을 찾아주는데, 캐싱을 통해 최적화한 클래스입니다.

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;
}

 

 

 

해당 메서드를 통해서 등록된 HandlerMethodArgumentResolver 중 MethodParameter 를 지원할 수 있는 resolver를 찾습니다.

 

 

 

argumentResolver가 캐싱되지 않았다면 저장된 모든 argumentResolver들을 순회하며 찾습니다.

 

 

 

사진은 argumentResolver들의 종류를이고, 총 27개의 resolver들이 있습니다.

 

 

모든 xxxMethodArgumentResolver들은 최상위 인터페이스인 HandlerMethodArgumentResolver를 반환하고 있습니다.

 

 

 

 

 

 

 

지금 코드에서는 마지막 26번쨰인 ServletModelAttributeMethodProcessor 가 resolver로 반환됩니다.

즉, 위에서 궁금했던 생략된 어노테이션에 대한 답이 나왔습니다. 바로 @ModelAttribute 입니다.

 

 

 

@GetMapping("/test")
public void test((@ModelAttribute) final GameInfoRequest request) {
    System.out.println("request = " + request.getNames());
    System.out.println("request.getCount() = " + request.getCount());
}

 

 

 

 

그런데 제가 궁금했던 점은 "누가" 데이터를 객체에 바인딩 해주는지 알아보기 위함이기 때문에 계속해서 디버깅해보겠습니다.

 

 

데이터 바인딩은 어디서 해주는 걸까?

 

 

 

위에서 resolver를 찾으면 ModelAttributeMethodProcessor 에 들어갑니다.

 

 

Resolve @ModelAttribute annotated method arguments and handle return values from @ModelAttribute annotated methods. …

 

 

ModelAttributeMethodProcessor@ModelAttribute 어노테이션 반환 값을 처리하는 곳입니다.

ModelAttributeMethodProcessor#resolverArgument 메서드에서 바인딩 해줍니다.

 

 

 

 

 

 

 

 

bindRequestParameters는 ModelAttributeMethodProcessor에 구현되어 있는 메서드이지만, 지금은 ModelAttributeMethodProcessor 를 상속받고 있는 ServletModelAttributeMethodProcessor 가 오버라이딩 한 메서드를 호출합니다.

 

 

체스 미션 중 상속 관계에서 부모의 구현 메서드를 오버라이딩 하는 것은 위험하다는 리뷰를 보았는데, 스프링에서 이렇게 구현하고 있으니 무조건 객체지향적이게 작성할 수 없다는 말이 떠올랐습니다.

 

 

 

 

결국 실제로 데이터를 바인딩해주는 곳은 ServletRequestDataBinder 입니다.

 

 

 

 

코드를 보니 들어오는 값들은 PropertyValue에 저장되어 있고, 이를 객체에 바인딩해줍니다.

 

 

 

바인딩 된 객체를 ModelAttributeMethodProcessor로 전달합니다.

즉, 다시 바인딩 된 객체를 받는 것이지요.

 

 

 

 

아래 코드는 ModelAttributeMethodProcessor 중 일부인데, mavContainer는 ModelAndViewContainer로써 model 에 값을 넣어주는 것으로 생각하면 됩니다.

 

 

mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);

return attribute;

 

 

 

그러고 나서 attribute를 반환하는데, 이때 attribute는 바인딩 된 GameInfoRequest입니다.

 

 

반환된 GameInfoRequest 들은 중간에 많은 클래스들이 있지만 간략하게 아래의 클래스들을 통해서 DispatcherServlet으로 반환됩니다.

 

 

 

ModelAttributeMethodProcessor → HandlerMethodArgumentResolverComposite → RequestMappingHandlerAdapter(ModelAndView 반환) → AbstractHandlerMethodAdapter → DispatcherServlet

 

 

 

결론

 

 

 

스프링 공식문서에서 말하는 Method Argument는 대표적으로 HttpMethod, @PathVariable, @RequestParam, @RequestHeader, @RequestBody, @ModelAttribute 등이 있습니다.

 

 

 

이러한 Method Argument들을 처리해 주는 곳이 HandlerMethodArgumentResolver 입니다.

또한, 앞에서 언급했듯이 handler method 는 Method Argument 뿐만 아니라 Return Values도 존재합니다.

대표적으로 @ResponseBody, @ModelAttribute, Map, String 등이 있습니다.

 

 

 

이러한 Return Values 들을 처리해 주는 곳이 HandlerMethodReturnValueHandler 입니다.

 

 

 

아무튼 한 요청을 처리하기 위해 많은 클래스들을 거치기 때문에 디버깅을 하며 depth 하나하나 들어갈 때마다 모험하는 느낌을 받았고, 많은 것들을 알 수 있었습니다.

 

 

 

 

처음에 고민했던 Controller에서 Method Arguments, Return Values 종류의 어노테이션들이 어떻게 동작하는지 알게 되어서 좋은 경험이었습니다.

 

 

 

중간에 xxxProcessor, xxxArgumentResolver 라는 클래스들이 생겨서 무슨 뜻인지 궁금했는데, 점차 스프링을 익혀가면서 배워보려 합니다.

728x90