[Spring Boot] SpringBoot + JWT 인증 리팩토링 (HandlerMethodArgumentResolver)

이홍준·2023년 7월 13일
0

Spring Boot

목록 보기
7/11

기존에 작성했었던 인증관련 로직을 사용했을때 중복되는 부분이 많았다. 대표적으로 컨트롤러에서 토큰값으로 사용자의 식별자 값을 추출하는 부분이다. 이 부분은 항상 고민이 많았다. 사실 2줄정도 추가만하면 되지만 뭔가 이런 Copy & Paste의 반복은 계속 보고 싶지 않다보니 해당 관련 공부를 시작하게 되었다.

Lagacy Code

    private final AuthService authService;     // DI

    public ResponseEntity<?> createPost(@RequestBody CreatePostRequest createPostRequest){
        Long memberId = authService.extractMemberIdFromToken();
        CreatePostResponse response = postService.createPost(memberId, createPostRequest);
        return ResponseEntity.ok(response);
    }
  • 기존 코드는 인증관련 서비스인 Authservice에 있는 정의했었던 토큰을 추출하는 함수인 extractMemberIdFromToken()를 호출함으로써 memberId를 바인딩 해주었다.
  • 하지만 뭔가 공통적으로 처리할 방법이 없을까하고 생각을 해보았다.

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver 라는 인터페이스를 상속받은 클래스로 사용하면 특정 타입의 파라미터에 대한 처리를 할수 있다. 그래서 컨트롤러에 있는 메소드의 매개인자를 설정해줄수 있다.

  • supportsParameter(MethodParameter parameter): Resolver가 어떤 파라미터를 지원하는지 판단한다. 반환값이 true일때만 어노테이션을 지정한 변수 값을 바인딩해준다.

  • resolveArgument: 실제로 파라미터값을 지정해주는 로직이다. 해당 반환값이 지정한 파라미터 값이 된다.


import lombok.*;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
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;

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
	// 토큰을 추출하는 함수
    private Long extractMemberIdFromToken() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(authentication);
        if (authentication == null || authentication.getName() == null) {
            throw new RuntimeException("토큰정보가 유효하지 않습니다.");
        }
        return Long.parseLong(authentication.getName());
    }
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(CurrentUser.class) != null
                && parameter.getParameterType().equals(Long.class);
    }

    @Override
    public Object resolveArgument(
            MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        return extractMemberIdFromToken();
    }
}

WebMvcConfigurer

resolver를 사용하기 위해서 WebMvcConfigurer의 addArgumentResolvers를 상속받아 정의햬던 CurrentUserArgumentResolver를 리스트에 추가해줘야한다.

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
    private final CurrentUserArgumentResolver currentUserArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(currentUserArgumentResolver);
    }
}

Annotation 정의

현재 SecurityContextHolder에 저장되어있는 인증객체를 전달해야 하는데, CurrentUser라는 커스텀 어노테이션을 정의한다.

package example.ganada.common.annotaion;

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 CurrentUser {

}

조치 결과

조치결과 컨트롤러에 직접 작성하지 않더라도 이번에 정의했던 코드를 통해 어노테이션만 지정하면 같은 기능을 할 수 있다. 또 PostController는 이제 AuthService를 의존할 필요가 없어졌다.
그리고 인증관련에 있어서 변경사항이 있으면 한 곳 만 바꿔주면 이 기능을 사용하는 모든 컨트롤러를 수정할 필요가 없어졌다.

    @PostMapping
    public ResponseEntity<?> createPost(@RequestBody CreatePostRequest createPostRequest, @CurrentUser Long memberId){
        CreatePostResponse response = postService.createPost(memberId, createPostRequest);
        return ResponseEntity.ok(response);
    }
profile
I'm a web developer.

0개의 댓글