[Spring] AOP와 ArguementResolver를 이용하여 반복작업 줄이기

kshired·2022년 7월 20일
0

Spring

목록 보기
2/11
post-thumbnail

제가 요즘 개발하고 있는 소프트웨어 13기 팀 몬스테라의 CS Broker 백엔드 서버는 Kotlin 과 Spring으로 이루어진 스택으로 개발을 진행하고 있습니다.

문제 상황

이 API 서버는 인증 및 인가에서 Stateless를 유지하기 위해, 제일 자주 사용되는 JWT를 사용하기로 했습니다.

근데, 권한이 필요한 API는 본 로직을 실행하기 전에 반복적으로 JWT를 통해 얻은 SecurityContext를 체크하는 반복로직을 추가해야했습니다.

예를 들어, 아래처럼 말이죠.

fun some_logic1_need_to_check_authority(){
	check_jwt_in_header()
    get_user_info_in_jwt()
    
	real_logic1()
}

fun some_logic2_need_to_check_authority(){
	check_jwt_in_header()
    get_user_info_in_jwt()

	real_logic()
}

이 반복작업이 너무 불편하다고 느꼈고, 이것을 해결하기 위해 여러가지 방법을 순차적으로 진행했습니다.

해결 과정 1

AOP ( Aspect Oriented Programming ) 은 관점 지향 프로그래밍이라고 불리는 기술입니다.

PointCut 등을 통해 반복 작업을 제거 할 수 있고, 특정 상황에 따라 파라미터를 주입한다던지 할 수 있습니다.

헤더에 있는 토큰은 AOP를 통해 해결 할 수 있지 않을까?

위와 같은 생각을 가지고 저는 다음과 같은 코드를 추가했습니다.

  1. 헤더에 있는 jwt를 파싱

  2. 파싱에 성공하면, jwt를 validation

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class IsLogin

@Component
@Aspect
class IsLoginAspect(private val tokenProvider: AuthTokenProvider) {
    
    @Before("@annotation(com.csbroker.apiserver.common.auth.IsLogin)")
    fun isLogin(ProceedingJoinPoint joinPoint) {
        var request: HttpServletRequest? = null
        
        for (arg in joinPoint.args) {
            if (arg is HttpServletRequest) {
                request = arg
                break
            }
        }
        
        if(request != null){
        	val tokenStr = getAccessToken(request)
            val token = tokenProvider.convertAuthToken(tokenStr)
            if (!token.isValid) {
            	throw UnauthroizedException("Access token이 올바르지 않습니다.")
        	}
        }

        throw UnauthroizedException("Access token이 없습니다.")
    }
}

이제 token 존재 유무 및 validation을 마쳤기 때문에, 각 method에서 토큰이 있는지 그리고, 토큰이 유효한지 체크를 하지 않아도 됩니다.

하지만..

그래도 HttpServletRequest에 접근하고, 정보를 파싱하는 로직을 계속 넣어야합니다.

다음처럼 말이죠..

val tokenStr = getAccessToken(request)
val token = tokenProvider.convertAuthToken(tokenStr)
val email = token.email
// real logic...

오직 헤더 접근을 위해 받아오는 HttpServletRequest를 사용하기도 싫었고, 파라미터정보를 파싱하는 로직 자체도 계속 반복되는게 너무 스트레스 받아서 이제 Arguemtn Resolver의 마법을 부리기로 했습니다.

해결 과정 2

ArguemtnResolver는 Spring에서 제공하는 기술 중 하나로, 특정 메소드의 파라미터를 주입해주는 방법입니다.

저는 여기서 ArgumentResolver를 통해 user 정보가 필요한 곳에 AuthToken 객체를 넣어주기로 했습니다.

Annotation 구현하기

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class LoginUser

저는 위와 같이 LoginUser라는 Runtime시에 VALUE_PARAMETER에 사용할 수 있는 어노테이션을 구현하였스브니다.

ArguementResolver 구현하기

@Component
class LoginUserArgumentResolver(
	private val tokenProvider: TokenProvider
) : HandlerMethodArgumentResolver {
    override fun supportsParameter(parameter: MethodParameter): Boolean {
        parameter.getParameterAnnotation(LoginUser::class.java) ?: return false

        if (!parameter.parameterType.isAssignableFrom(AuthToken::class.java)) {
            return false
        }

        return true
    }

    override fun resolveArgument(
        parameter: MethodParameter,
        mavContainer: ModelAndViewContainer?,
        webRequest: NativeWebRequest,
        binderFactory: WebDataBinderFactory?
    ): Any? {
        val request = webRequest.nativeRequest as HttpServletRequest

        val tokenStr = getAccessToken(request)
        val token = tokenProvider.convertAuthToken(tokenStr)

        if (!token.isValid) {
            throw UnauthorizedException("인증되지 않은 토큰입니다.")
        }

        return token
    }
}

HandlerMethodArgumentResolver는 위에서 구현한 어노테이션의 유무를 체크하여, 특정 파라미터에 값을 넣어 줄 수 있습니다.

이를 통해, 저는 @LoginUser 라는 어노테이션이 붙은 파라미터에 올바른 JWT가 들어와서 authentication이 할당된 경우 그 값을 파라미터에 삽입할 수 있었습니다.

ArgumentResolver 작동하게 설정하기

@Configuration
class WebMcvConfig(
    private val loginUserArgumentResolver: LoginUserArgumentResolver
) : WebMvcConfigurer {
    override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
        resolvers.add(loginUserArgumentResolver)
    }
}

현재 WebMvcConfigurerAdapter가 deprecate 되었습니다.

그렇기 때문에 위와 같이 WebMvcConfigurer 를 상속하여 구현한 WebMvcConfigaddArgumentResolver 메소드에 새로 만든 loginUserArgumentResolver를 추가해야합니다.

해결을 통해 얻게 된 결과 살펴보기

해결하기 전

@GetMapping
fun getUser(request: HttpServletRequest): ApiResponse<UserResponseDto> {
	val tokenStr = getAccessToken(request)
    
    if(tokenStr == null){
    	throw UnauthorizedException("access token does not exist")
    }
    
    val token = tokenProvider.convertAuthToken(tokenStr)
    
    if(!token.isValid){
    	throw UnauthorizedException("권한이 없습니다.")
    }

	val findUser = this.userService.findUserByEmail(token.email)
    	?: throw UnauthorizedException("${token.email} 은 유효한 이메일이 아닙니다.")
            
	return ApiResponse.success(findUser.toUserResponseDto()) 
}

해결 후

@GetMapping
@IsLogin
fun getUser(@LoginUser authToken: AuthToken): ApiResponse<UserResponseDto> {
	val findUser = this.userService.findUserByEmail(authToken.email)
            ?: throw UnauthorizedException("${authToken.email} 은 유효한 이메일이 아닙니다.")
            
	return ApiResponse.success(findUser.toUserResponseDto()) 
}

위를 살펴보면 알 수 있듯이, @LoginUser가 달린 파라미터에 자동적으로 authToken의 값이 할당 되기 때문에 반복적인 체크 과정을 삭제 할 수 있었습니다.

행복하게 해결 할 수 있었습니다 :)

profile
글 쓰는 개발자

0개의 댓글