우아한테크코스 레벨2 장바구니 미션에서 HandlerMethodArgumentResolver
를 처음으로 사용해보게 되어 정리해보았다.
중복 코드 제거와 컨트롤러 내의 역할 분리를 위해 사용해보았는데, 동작 원리에 대해 아직까지는 정확한 이해는 하지 못한 것 같다.
그래서 우선 공부한 내용에 대해 정리해보고 이후에 보완하고자 한다.
주로 사용법에 대해 작성해보았고, 동작 순서나 동작 원리에 대해 100% 는 이해하진 못 했기 때문에 이에 대해서는 간단하게 정리해보았다.
request
에서 메소드의parameters
값으로 변환 혹은 바인딩 하는resolver
이며,Spring framework
에서 제공하는 인터페이스이다.
HandlerMethodArgumentResolver
는 아래와 같은 2가지 메소드를 제공한다.
supportsParameter()
: parameter가 해당 resolver를 지원하는 여부 확인
resolveArgument()
: Method parameter를 argument value로 변환, 바인딩 하는 역할
Argument Resolver는 Interceptor 가 처리된 후 처리된다.
Client 가 요청한다(Request).
Dispatcher Servlet에서 Request를 처리한다.
요청을 분석하여 Request에 대한 Hadler Mapping을 한다.
RequestMappingHandlerAdapter(핸들러 매핑에 맞는 어댑터 결정).
Interceptor 처리
Argument Resolver 처리 (등록한 리졸버에 대응되는 파라미터 바인딩)
Message Converter 처리
Controller Method invoke
아래의 방법을 이용하여 Custom Resolver를 사용할 수 있다.
선택적으로 구현한 Custom Resolver에 애너테이션을 사용하여 명시적으로 그 범위를 제한할 수 있다.
예시 코드는 장바구니 미션에 적용한 코드이다.
애너테이션을 통해 제한적으로 Resolver를 적용할 수 있다.
어떤 객체를 바인딩하고 있다는 것을 명시적으로 드러내기 위해 어노테이션을 사용할 수 있다.
반드시 사용해야하는 것은 아니며, 선택적으로 사용할 수 있다(생략 가능하다).
package cart.auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticationPrincipal {
}
HandlerMethodArgumentResolver
인터페이스 구현한 클래스 생성한다.
package cart.auth;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
// ...
}
supportsParameter()
, resolveArgument()
구현
supportsParameter()
,resolveArgument()
오버라이딩한다.
AuthenticationPrincipalArgumentResolver
supportsParameter()
: 현재 파라미터를 리졸버가 지원하는지에 대한 여부에 따라 true false 값을 반환한다.
아래 코드에서는 해당 파라미터가 @AuthenticationPrincipal
애너테이션을 포함하는지 여부에 따라 boolean 값을 리턴하고 있다.
resolveArgument()
: 실제로 바인딩할 객체를 리턴한다.
아래 코드에서는 request 내 Authorization
헤더에서 추출한 AuthInfo
를 리턴하고 있다.
(바인딩: 프로그램에 사용된 구성 요소의 실제 값 또는 프로퍼티를 결정짓는 행위를 의미)
package cart.auth;
import cart.dto.auth.AuthInfo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthorizationExtractor<AuthInfo> extractor = new BasicAuthorizationExtractor();
// 1. supportsParameter() 구현
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthenticationPrincipal.class);
}
// 2. resolveArgument() 구현
@Override
public AuthInfo resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
return extractor.extract(request);
}
}
WebMvcConfigurer
를 구현한 클래스에addArgumentResolver()
메소드로 구현한 resolver 추가한다.
아래와 같이 적용이 필요한 파라미터에@AuthenticationPrincipal
를 붙여 사용할 수 있다.
package cart.config;
import cart.auth.AuthenticationPrincipalArgumentResolver;
import cart.auth.LoginInterceptor;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CartConfiguration implements WebMvcConfigurer {
// ... viewControllers, Interceptor 관련 코드 ...
@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticationPrincipalArgumentResolver());
}
}
package cart.controller;
import cart.auth.AuthenticationPrincipal;
import cart.dto.CartItemDto;
import cart.dto.auth.AuthInfo;
import cart.service.CartService;
import java.net.URI;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/cart")
@RequiredArgsConstructor
public class CartApiController {
private final CartService cartService;
// ...
@DeleteMapping("/{cartId}")
public ResponseEntity<Void> delete(@PathVariable long cartId, @AuthenticationPrincipal AuthInfo authInfo) {
cartService.delete(authInfo.getEmail(), cartId);
return ResponseEntity
.ok()
.build();
// ...
}
InvocableHandlerMethod
:Controller
의 메서드를wrapping
Spring은 InvocableHandlerMethod
의 invokeForRequest()
로 Request
를 처리한다.
invokeForRequest()
내에서 호출되는 getMethodArgumentValues()
내의 resolvers
는 HandlerMethodArgumentResolverComposite
객체이다.
HandlerMethodArgumentResolverComposite
를 살펴보자.
HandlerMethodArgumentResolverComposite
는HandlerMethodArgumentResolver
를 상속받은 객체이다.
HandlerMethodArgumentResolverComposite
은 다른 모든 HandlerMethodArgumentResolver
를 상속받은 객체를 가지고 루프를 돌며 지원하는 파라미터인지 확인을 하고 결과를 리턴한다.
이 때 지원하는 파라미터인지 확인 하는 역할을 담당하는 것이 위에서 다룬 supportsParameter()
이다.
파라미터로 받는 값이 많고, 중복이 발생할 때 공통적인 처리를 위해
HandlerMethodArgumentResolver
를 사용하면 좋을 것 같다.
Interface HandlerMethodArgumentResolver
Spring HandlerMethodArgumentResolver의 사용법과 동작원리