ArgumentResolver가 필요한 상황
서비스를 운영하다보면 다양한 종류의 데이터를 받는다. 즉, 컨트롤러의 파라미터로 다양한 데이터가 온다는 것이다.
만약 사용자로부터 회원 id 요청이 들어왔지만 서버에서 필요한 정보는 회원이라면 어떻게 할까?
회원 id로 부터 회원정보를 조회해 회원 객체를 만들어주는 일련의 과정이 필요할 것이다. 이 때 사용되는것이 ArgumentResolver이다.
Spring에서는 파라미터를 공통으로 처리할 수 있도록 구현된 인터페이스가 있는데, 그것을 바로 ArgumentResolver라고 부른다.
즉, 어떠한 요청이 컨트롤러에 들어왔을 때, 요청에 들어온 값으로부터 원하는 객체를 만들어내는 일을 ArgumentResolver이 간접적으로 해줄 수 있다.
Spring ArgumentResolver는 HandlerMethodArgumentResolver를 구현하여 애플리케이션에 맞는 새로운 Resolver를 만들고, 애플리케이션 실행 시 Resolver 리스트에 추가함으로써 적용할 수 있다.
Spring MVC는 다음과 같은 흐름으로 요청 처리가 이루어진다.
- 사용자가 웹 브라우저를 통해 요청하면 DispatcherServlet이 이를 받는다.
- DistpatcherServlet은 해당 요청에 맞는 URI를 HandlerMapping에서 검색한다.
- 이 때, RequestMapping으로 구현한 API를 찾게 되는데, 이들은 RequestMappingHandlerAdapter가 모두 가지고 있다.
- 원하는 Mapping을 찾은 경우, 첫 번째로 Interceptor를 처리한다.
- Argument Resolver를 처리한다.
- Message Converter 처리한다.
- Controller Method invoke이 일어난다.
즉, ArgumentResolver는 Interceptor 요청 뒤에 이루어진다.
ArgumentResolver 적용법
ArgumentResolver는 HandlerMethodArgumentResolver 인터페이스를 구현해야 한다.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
ArgumentResolver가 실행되길 원하는 파라미터의 앞에 특정 어노테이션을 생성해 붙인다.
supportsParameter는 요청받은 메서드의 인자에 원하는 어노테이션이 붙어있는지 확인하고 원하는 어노테이션을 포함하고 있으면 true를 반환한다.
resolveArgument는 supportsParameter에서 true를 받은 경우, 즉, 특정 어노테이션이 붙어있는 어느 메서드가 있는 경우 parameter가 원하는 형태로 정보를 바인딩하여 반환하는 메서드이다.
다음과 같이 사용자의 주문에 대한 정보가 필요할 때, 헤더에 담긴 토큰으로 부터 사용자를 식별하고 싶다고 생각을해보자.
@GetMapping("/{orderId}")
public ResponseEntity<OrdersResponse> findOrder(@AuthenticationPrincipal final LoginCustomer loginCustomer,
@PathVariable final Long orderId) {
final OrdersResponse ordersResponse = orderService.findOrderById(loginCustomer.getUsername(), orderId);
return ResponseEntity.ok(ordersResponse);
}
헤더의 토큰엔 회원의 userId만 있다. 그러나 컨트롤러의 findOrder 메서드에서 호출하는 orderService는 회원의 userName을 필요로하고 있다. 이 때 회원의 userId로부터 회원 객체를 만들고 회원의 name을 가져오는 로직을 컨트롤러에 넣으면 어떻게 될까?
또한 findOrder메서드 뿐만 아니라 다른 메서드에서 사용된다면 어떻게 될까?
이러한 과정을 줄여주기 위해 ArgumentResolver를 사용한다.
다음과 같이 AuthenticationPrincipal 어노테이션을 만들고
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticationPrincipal {
}
HandlerMethodArgumentResolver를 구현한 AuthenticationPrincipalArgumentResolver 클래스를 생성한다.
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
private final AuthService authService;
public AuthenticationPrincipalArgumentResolver(AuthService authService) {
this.authService = authService;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthenticationPrincipal.class);
}
@Override
public LoginCustomer resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String token = AuthorizationExtractor.extract(nativeRequest);
return authService.findCustomerByToken(token);
}
}
해당 클래스의 supportsParameter는 요청받은 메서드의 인자에 원하는 어노테이션이 붙어있는지 확인하고 원하는 어노테이션을 포함하고 있으면 true를 반환한다.
resolveArgument는 supportsParameter에서 true를 받은경우, parameter가 원하는 형태로 정보를 바인딩하여 반환하는 메서드이다.
해당 메서드는 헤더의 토큰으로부터 LoginCustomer라는 객체를 반환한다.
Configuration을 이용해 ArgumentResolver를 등록할 수 있다.
@Configuratio
public class WebConfig implements WebMvcConfigurer {
private final AuthService authService;
private final JwtTokenProvider jwtTokenProvider;
public WebConfig(AuthService authService, JwtTokenProvider jwtTokenProvider) {
this.authService = authService;
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(createAuthenticationPrincipalArgumentResolver());
}
@Bean
public AuthenticationPrincipalArgumentResolver createAuthenticationPrincipalArgumentResolver() {
return new AuthenticationPrincipalArgumentResolver(authService);
}
}
reference
https://tecoble.techcourse.co.kr/post/2021-05-24-spring-interceptor/
'Spring' 카테고리의 다른 글
[Spring] Spring Framework vs Spring Boot (0) | 2023.01.03 |
---|---|
[Spring] DI 의존성 주입 방식과 생성자 주입을 사용해야 하는 이유 (0) | 2022.11.22 |
[Spring] @Transactional의 트랜잭션 전파레벨 (0) | 2022.11.17 |
[Spring] 필터와 인터셉터 (Filter vs Interceptor) (0) | 2022.11.16 |
[Spring] SpringBootTest 테스트 격리 방법 (0) | 2022.11.15 |