최근 학교 졸업 작품을 위해 프로젝트의 로그인 인증 방식을 JWT로 바꾸는 작업을 진행했다. 그러던 중 컨트롤러에서 중복되는 코드를 발견했는데, 해당 코드는 다음과 같다.
- jwtHandler의 getJwtUser는 클라이언트의 요청에서 받은 JWT를 파싱하고 그것을 JwtUser라는 객체로 반환해주는 역할을 한다.
- 즉, JWT 값을 가져와서 모델에 추가하는 과정이 중복되는 것이다.
- 다른 컨트롤러와 메서드에서 이와 같이 JWT 값이 필요하면 그 때마다 똑같은 코드를 반복해서 작성해야한다. 분명 개선이 필요한 부분이다.
- 예전에 이동욱 님이 쓴 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스'라는 책에서 컨트롤러에서 중복되는 코드를 파라미터 기반으로 변경했던 기억이 있다.
- 따라서 이 부분을 책에서 봤던대로, 메서드의 인자로 JWT 값을 바로 받을 수 있도록 변경할 예정이다.
🧶 HandlerMethodArgumentResolver??
Spring 의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있다. @RequestParam, @ModelAttribute, @PathVariable, @RequestBody 등의 어노테이션부터 시작해서, 요청, 응답 객체인 HttpServletRequest / Response , View 를 처리하는 Model 까지 컨트롤러 메서드의 파라미터를 정의하면 Spring이 알아서 원하는 객체를 할당해준다.
이렇게 파라미터를 유연하게 사용할 수 있는 이유가 바로 HandlerMethodArgumentResolver 덕분이다. (줄여서 ArgumentResolver 라고도 한다.)
- 실제로 Spring이 제공하는 HandlerMethodArgumentResolver 코드를 보면, 이를 구현하는 클래스가 다음과 같이 많다는 것을 확인할 수 있다.
클라이언트의 요청이 오면 HandlerMethodArgumentResolver (이하 ArgumentResolver)에 의해 컨트롤러의 파라미터 타입, 어노테이션 정보를 기반으로 파라미터에 객체를 할당하게 된다.
Spring은 이미 개발자들이 필요로하는 많은 객체들을 HandlerMethodArgumentResolver 를 구현하여 어떤 객체를 반환할지 정의 해놓았기 때문에 @RequestParam 과 같은 어노테이션들을 적절히 사용하기만 하면 되는 것이다.
물론 Spring은 개발자가 직접 HandlerMethodArgumentResolver 를 구현하여 사용할 수 있도록 추상화 되어있기 때문에 사용하는 법은 그리 어렵지 않다. 이제부터 그 방법에 대해 살펴보자.
🎯 컨트롤러에서 중복되는 코드를 파라미터 기반으로 변경하기
앞서 설명했던 것 처럼 HandlerMethodArgumentResolver 를 구현하면 원하는 객체를 컨트롤러 파라미터에 자동으로 할당할 수 있다.
그 전에 보다 명확한 코드를 위해 @LoginUser라는 어노테이션을 생성한다.
(어노테이션 없이 클래스 타입만 조건을 걸어 구현할 수도 있다. 하지만 어노테이션을 붙이면 코드가 조금 더 명확해진다.)
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 LoginUser {
}
- Target은 어노테이션을 적용할 위치를 선택한다.
- PARAMETER : 파라미터에 적용.
- Retention은 자바 컴파일러가 어노테이션을 다루는 방법을 기술하며, 어느 시점까지 영향을 미치는지 결정한다.
- RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조 가능.
이후 HandlerMethodArgumentResolver 를 구현하는 객체를 생성하여 메서드를 구현해주면 된다!
HandlerMethodArgumentResolver 에는 다음과 같이 두가지 메서드가 존재한다.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
- supportsParameter : 현재 파라미터를 ArgumentResolver가 지원할지 체크
(return값이 true라면 resolveArgument 실행 ) - resolveArgument : 파라미터에 바인딩할 객체를 반환한다.
즉, supportsParameter 메서드가 먼저 실행되어 지원 여부를 판단하고 resolveArgument 메서드가 실행되어, 객체를 파라미터에 바인딩하는 것이다.
예시
@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtHandler jwtHandler;
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
boolean isUserClass = JwtUser.class.equals(parameter.getParameterType());
return isLoginUserAnnotation && isUserClass;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
JwtUser jwtUser = jwtHandler.getJwtUser(Objects.requireNonNull(request));
Objects.requireNonNull(mavContainer).getModel().addAttribute("user", jwtUser);
return jwtUser;
}
}
- supportsParameter : @LoginUser가 붙은 JwtUser이라는 클래스에 객체를 바인딩할 것이기 때문에 해당 파라미터의 타입과 어노테이션이 붙었는지 확인한 후 boolean 값을 반환한다.
- resolveArgument 메서드는 ModelAndViewContainer 와 NativeWebRequest를 파라미터로 제공해준다.
- ModelAndViewContainer 는 Model과 View를 관리해주는 구현체다.
- NativeWebRequest 는 WebRequest를 상속받는 인터페이스인데, getNativeRequest | getNativeRequest 메서드를 통해 요청, 응답 객체를 받을 수 있다.
(HttpServletRequest, HttpServletResponse)
- 그리고 스프링이 구현체를 참조할 수 있도록 빈으로 등록해주면 된다. (@Component)
이제 컨트롤러의 파라미터로 @LoginUser JwtUser를 넣어주기만 하면 방금 정의했던 resolveArgument에서 반환되는 객체가 user라는 변수에 할당되게된다.
결론
- 우리가 Spring에서 사용하는 @RequestParam 과 같은 어노테이션은 컨트롤러 내부 로직 실행 전에 ArgumentResolver 가 처리해준다.
- HandlerMethodArgumentResolver 를 구현함으로써 중복 코드를 줄일 수 있다.
- HandlerMethodArgumentResolver 의 supportsParameter 메서드는 ArgumentResolver의 지원 여부, resolveArgument는 파라미터에 바인딩 되는 객체를 정의한다.
참고
- 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 이동욱 저
- 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 김영한
- https://blog.advenoh.pe.kr/spring/HandlerMethodArgumentResolver-%EC%9D%B4%EB%9E%80/