JWT 토큰 방식으로 회원가입, 로그인, 로그아웃 기능을 구현했습니다. 구현한 내용 중에서 회원가입한 회원인지, 아닌지에 대해서 @RequsetHeader("Authorization") String token
코드를 통해서 Bearer 타입의 JWT 토큰을 받고, 이를 Service로 넘겨 Payload 내용을 확인하여 실제 유저인지 아닌지 체크하는 과정을 거칩니다.
public void logout(String token) {
if (!jwtTokenProvider.validateToken(token)) {
throw new CustomException(Code.ACCESS_TOKEN_UNAUTHORIZED);
}
String nickname = jwtTokenProvider.getSubject(token);
log.info("logout nickname check: {}", nickname);
Member member = memberRepository.findByNickname(nickname).orElseThrow(
() -> new CustomException(Code.NOT_EXIST_NICKNAME));
refreshTokenRepository.delete(member.getId());
}
위 사진과 같이 구현했을 때, 항상 의문이 들었던 점은 유저만 사용할 수 있는 모든 메소드에 해당 로직을 통해서 유저인지 확인하는 코드를 작성해야할까? 였습니다. 검색해 본 결과 해결할 수 있는 방법으로 Argument Resolver
라는 방식을 알게 되었습니다. Argument Resolver를 알아보기 전에 먼저 Binding 개념에 대해서 알아봅시다.
웹개발에서 바인딩(Binding)이란, 어떤 값들을 변수에 묶어버리는 것을 의미합니다. 대표적으로 3가지 바인딩 방식이 있습니다.
@RequestBody
- HTTP Body를 변수에 바인딩하기 위해 사용되는 어노테이션 @PostMapping("/restaurants")
public CommonResponse<?> createRestaurant(@RequestBody RestaurantCreateRequest restaurantCreateRequest) {
restaurantService.createRestaurant(new RestaurantCreateParam().of(restaurantCreateRequest));
return CommonResponse.of(Code.OK);
}
@RequestParam
- Controller에서 쿼리 스트링을 변수에 바인딩하기 위해 사용되는 어노테이션 @GetMapping("")
public CommonResponse<?> readRestaurants(@RequestParam(value = "keyword", required = false) String keyword) {
List<RestaurantItemResponse> restaurantItemResponses = restaurantService.readRestaurants(keyword);
return CommonResponse.of(restaurantItemResponses);
}
@PathVariable
- 가변적인 경로를 변수에 바인딩하기 위해 사용되는 어노테이션 @DeleteMapping("/{id}/restaurants")
public CommonResponse<?> deleteRestaurant(@PathVariable Long id) {
restaurantService.deleteRestaurant(id);
return CommonResponse.of(Code.OK);
}
대표적인 바인딩 방식 이외에도 HTTP Header, Session, Cookie 등 직접적이지 않은 방식 혹은 외부 데이터 저장소로부터 데이터를 바인딩해야할 때가 있는데, 이때 사용되는 것이 Argument Resolver 입니다. Argument Resolver를 사용하면 Controller Method Parameter 중에서 특정 조건에 맞는 Parameter가 있다면, 요청에 들어온 값을 이용해 원하는 객체를 만들어 바인딩해줄 수 있습니다.
정리해보자면 Argument Resolver(HandlerMethodArgumentResolver)는 Spring MVC에서 Controller Method의 Parameter를 자동으로 변환하고 주입해주는 편의를 제공해줍니다. 따라서, Argument Resolver 단에서 JWT Token의 Payload를 먼저 읽어 유저의 식별자를 추출하고, 해당 유저가 존재한다면 유저 Entity를 반환하고, 아니면 예외를 처리할 수 있습니다. 이로써 불필요한 인증 과정의 중복성을 줄여 보다 더 서비스 로직을 구현하는데 집중할 수 있습니다.
동작 방식은 위 사진과 같다.
Argument Resolver에 대한 개념과 동작 방식에 대해서 알았으니, 예제 코드를 확인해보도록 하자. 우선, Argument Resolver를 Spring FW에서 구현하기 위해서는 파라미터를 통해 바인딩 될 객체를 @interface로 만들어야 합니다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserArg {
}
그 다음, HandlerMethodArgumentResolver
인터페이스를 상속받은 Custom ArgumentResolver를 구현해줍니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtTokenInterceptor jwtTokenInterceptor;
private final JwtTokenProvider jwtTokenProvider;
private final MemberRepository memberRepository;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserArg.class);
}
@Override
public Member resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String token = jwtTokenInterceptor.resolveToken(request);
String nickname = jwtTokenProvider.getSubject(token);
log.info("resolver nickname: {}", nickname);
return memberRepository.findByNickname(nickname).orElse(null);
}
}
그 다음, 열심히 만든 Custom ArgumentResolver를 WebMvcConfigurer에 저장해줍니다.
@Configuration
@RequiredArgsConstructor
public class ResolverWebconfig implements WebMvcConfigurer {
private final UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
@GetMapping("/api/test")
public CommonResponse<?> test(@UserArg Member member) {
return CommonResponse.of(member);
}
@UserArg 어노테이션이 붙은 Parameter에 대해서 Argument Resolver가 바인딩 해주기 때문에 Controller에서는 바인딩된 객체를 그냥 사용해주면 됩니다.
위 사진과 같이 Postman 테스트 결과 정상적으로 Member Entity의 정보를 가져오는 것을 확인할 수 있습니다.