Spring ArgumentResolver 로 인증처리 중복 코드 제거

devdo·2022년 11월 23일
1

Project

목록 보기
10/11
post-thumbnail

ArgumentResolver 란?

  • 정확히는, HandlerMethodArgumentResolver 불립니다.
  • HandlerMethodArgumentResolver 는 스프링 3.1 에서 추가된 Interface 입니다.
  • 스프링 3. 이전에는 WebArgumentResolver 라는 Inteface 였습니다.
  • Spring 공식 문서에는 다음과 같이 설명되어 있습니다.

Strategy interface for resolving method parameters into argument values in the context of a given request.

주어진 요청을 처리할 때, 메서드 파라미터를 인자값들에 주입 해주는 전략 Interface인 것입니다.

다시말해, ArgumentResolver 에서 가공한 뒤 Contorller 의 parameter 형식의 Annotation 으로 넘겨주는 방식입니다.

  • 간단하게는 기능으로 설명하자면, Controller로 들어온 파라미터를 가공하거나 수정 기능을 제공하는 객체입니다.

  • ArgumentResolver를 Controller 단에서 사용하면 중복 코드(HttpSession에서 세션 로드, HttpServletRequest에서 요청 url 및 ip 정보 로드 등)를 깔끔하게 처리할 수 있습니다. interceptor, AOP 기능으로도 중복코드가 남아 있는 로그인 인증 처리같은 경우에 많이 사용합니다.


HandlerMethodArgumentResolver를 사용하는 이유

HandlerMethodArgumentResolver 를 이용하여 Custom Annotaion을 만들어 User 정보를 쉽게 가져오기를 실습하였습니다.

회원을 관리하는 API 를 만들게 되면 꼭 필요로 한 것이 인증처리입니다.

이때 HandlerInterceptorAdapter 를 이용하여 HttpServletRequest 에 사용자 정보를 가져오기 위해서 Controller parameter 에서 꺼내서 사용하는 방식입니다.

만약, HttpServletRequest 을 사용하지 않고 static 변수나 공유 가능한 변수 형태로 사용하게 된다면 Thread safe 하지 않기 때문에 Multi-thread 환경에서는 다른 사용자의 정보를 노출하게 되는 위험한 상황을 만들 수 있습니다.

그렇다고, 매번 API 에서 HttpServletRequest 를 parameter로 받아 get, set 하는 방식은 굉장히 번거롭고 많은 코드의 duplicate를 발생시킵니다.

이러한 중복 코드 문제를 해결하기 위한 방법이 HandlerMethodArgumentResolver 를 이용한 것입니다.


Spring Security vs ArugmentResovler

처음 ArgumentResolver을 학습할 때 기존에 사용하던 Spring Security 과 자주 헷갈렸습니다. 이 둘의 차이점은 무엇일까요?

Spring Security는 ArgumentResolver 메카니즘 등을 이용한 인증관련 통합 라이브러리라고 보면 되겠습니다.

Form 로그인, OAuth인증, 사용자획득, 권한처리 등의 틀을 어느정도 마련해주는 통합 라이브러리인 것이죠.

ArgumentResolver 위에서 설명한대로 앞단의 Controller에서 들어온 파라미터를 가공하거나 수정 기능을 가진 Spring에서 제공하는 객체입니다.

간단한 인증은 Spring Filter(or Interceptor), ArgumentResolver같은 객체를 이용해서 직접 구현해도 문제가 없습니다.

이번에 사용되는 프로젝트에서는 세션을 사용해서 사용자정보를 인증하는 방식을 채택했습니다. 간단히 세션 값을 가져오고 확인하는 절차이니 이런 경우에는 ArugmentResovler를 사용해도 상관없을 것입니다. 또는 둘 다 사용해도 상관이 없겠습니다.


구현 소스

1) WebConfig

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final AuthUserResolver authUserResolver;

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

2) @AuthUser

: 인증(로그인)된 User interface 만들기
애노테이션을 만들 때는 클래스 선언부에 class 대신 @interface를 사용합니다.

이 인터페이스는 향후 AOP 적용을 위해서이기도 합니다.
ArugmentResovler를 위해서 필수적인 것은 아니지만, 기본적으로 같이 많이 사용합니다.

@Target(ElementType.PARAMETER) // --- 1
@Retention(RetentionPolicy.RUNTIME) // --- 2
public @interface AuthUser {
}
  • @Target(ElementType.PARAMETER) : 파라미터에 해당 애노테이션을 사용할 수 있다는 뜻.
  • @Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있다는 뜻.

3) AuthUserResolver(HandlerInterceptorAdapter 구현체 작성)

: HandlerInterceptorAdapter 를 이용하여 사용자 정보를 인증을 합니다. 실제 검증부분은 resolveArgument 메서드에서 실행됩니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthUserResolver implements HandlerMethodArgumentResolver {

    private static final String USER_ID = "USER_ID";

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(AuthUser.class);
    }
    
    @Override
    public Object resolveArgument(
            MethodParameter parameter,
            ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) throws Exception {

        final Long userId = (Long) webRequest.getAttribute(USER_ID, WebRequest.SCOPE_SESSION);
        log.info("resolver userId : {}", userId);
        if (userId != null) {
            return UserInfo.builder().id(userId).build();
        } else {
            throw new WSApiException(ErrorCode.NOT_FOUND_USER);
        }
    }
}


4) Controller에서 파라미터 적용

다음 User 정보가 필요한 api를 요청할 때 Controller단에서 @AuthUser를 인식하고 인증처리를 위한 ArgumentResolver가 실행됩니다.

    // 일반유저가 스터디그룹에 참여
    @PostMapping("/studyGroup/{studyGroupId}")
    public ResponseEntity<?> participate(
            @PathVariable("studyGroupId") Long studyGroupId,
            @AuthUser Long userId
            ) {
        log.info("users participate studyGroup, " +
                "studyGroupId: {}, userInfo: {}", studyGroupId, userId);
        UserGroup participateUG = userService.participate(studyGroupId, userId);
        log.info("participateUG: {}", participateUG);

        return ResponseEntity.ok(participateUG);
    }

테스트

만약, user_id 정보를 주지 않는다면 다음과 같이 errorCode를 던집니다.
이는 Service 내 ExceptionHandler가 아닌, ArugmentResolver가 시행되는 겁니다.


구현과정 정리

  • HandlerMethodArgumentResolver 인터페이스를 상속받는 구현체 클래스를 생성한다.
  • 인터페이스를 개발하고 컨트롤러 api 요청 파라미터로 붙여주면 Argument Resolver가 발동한다. 하지만 필수적인 것은 아니다. 파라미터로 HttpServletRequest가 있으면 된다.
  • 이 인터페이스는 개발자가 커스텀하는 Argument Resolver에 대한 여러 메서드를 지원한다.
  • supportsParameter 메서드는 들어온 파라미터에 대해 resolveArgument 메서드를 실행할지 말지 판단한다.
  • 리턴 값이 true면 결과적으로 resolveArgument 메서드를 실행하게 되는데, 이 메서드는 파라미터를 가공하는 역할을 한다. 리턴값으로 유저 정보(ex. UserInfo)를 전달한다.


참고

profile
배운 것을 기록합니다.

0개의 댓글