스프링을 싫어하는 사람들이 많이 하는 말이 있다. 스프링은 어노테이션이 덕지덕지 붙어서 극혐이라고..
하지만 어노테이션도 잘 쓰면 로직을 간단히 추상화해줄 수 있어서 좋은 점이 많다.
어노테이션을 이용해서 로그인한 유저만 api를 호출할 수 있도록 하고, 미로그인 유저는 400 에러를 발생하도록 필터링하는 기능을 구현해보려고 한다.
@NeedLogin
@Operation(summary = "아티클 작성하기")
@PostMapping("/{username}/articles")
fun writeArticle(
@PathVariable("username") username: String,
@RequestBody request: BlogArticleWriteRequestDTO,
) {
TODO("로직은 알아서 잘 딱 깔끔하게")
}
설명하기 앞서, 원하는 결과물은 이렇다.
api의 위에 @NeedLogin
어노테이션을 붙이는 것 만으로 API가 로그인을 필요로 한다는 것을 명시적으로 나타내줄 수 있다.
언어는 코틀린으로 짜여있지만, Java로도 코드가 크게 다르진 않을 것이니 따라가봅시다
레츠고~
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class NeedLogin
간단하게 어노테이션을 만들어주자. API가 실행될 때 로그인 상태를 검증하는 것이니 RUNTIME이고, 함수에 붙여줄 생각이니 타겟은 FUNCTION으로 한다.
@Component
class NeedLoginInterceptor(
private val tokenProvider: TokenProvider,
) : HandlerInterceptor {
override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any,
): Boolean {
// NeedLogin 어노테이션이 붙어있지 않은 함수면 패스
if (!hasNeedLoginAnnotation(handler)) {
return true
}
// 로그인된 상태인지 체크
if (!hasValidAccessToken(request)) {
throw UserNotAuthorizedException()
}
return true
}
private fun hasValidAccessToken(request: HttpServletRequest): Boolean {
return getAccessCookie(request)
?.let { tokenProvider.validateToken(it) }
?: false
}
private fun hasNeedLoginAnnotation(handler: Any): Boolean {
return handler is HandlerMethod &&
handler.method.isAnnotationPresent(NeedLogin::class.java)
}
}
api가 호출될 때 마다 검증해야 하기 때문에 Interceptor를 사용해준다. HandlerInterceptor
를 상속받아서 NeedLoginInterceptor
라는 클래스를 만들었다.
'api가 실행되기 전에' 로그인 상태를 검증해야 하므로 preHandle
에 로직을 적어넣었다.
NeedLogin 어노테이션이 붙어있는지 체크해서 없을 경우 패스하기 위해 리턴 시켜준다.
그 후 각자의 방법으로 로그인 여부를 체크해준 후 미로그인 상태일 경우 Exception을 터트려주면 된다. 나는 AccessToken을 이용하는 방법을 사용했기 때문에 해당 방법으로 구현해주었다.
@Configuration
class WebConfig(
private val needLoginInterceptor: NeedLoginInterceptor
) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(needLoginInterceptor)
.addPathPatterns("/**")
}
}
마지막으로는 인터셉터를 적용해주기 위해 WebMvcConfigurer에다가 내가 만든 인터셉터를 적용해주었다.
그러면 끝!
@NeedLogin
@Operation(summary = "아티클 작성하기")
@PostMapping("/{username}/articles")
fun writeArticle(
@PathVariable("username") username: String,
@RequestBody request: BlogArticleWriteRequestDTO,
) {
TODO("로직은 알아서 잘 딱 깔끔하게")
}
나머지는 이렇게 api 위에다가 NeedLogin 어노테이션만 붙여주면 된다.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(UserNotAuthorizedException::class)
fun handleUserNotAuthorizedException(exception: UserNotAuthorizedException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ErrorResponse(exception.message))
}
}
마지막으로 예외가 발생했을 때 400 에러와 exception 메시지를 출력해주기 위해 ExceptionHandler로 받아다가 원하는 에러 코드와 메시지를 전달해주면 된다.
{
"error": "로그인이 필요합니다."
}
그러면 이렇게 API를 실행했을 때 401 에러와 함께 메시지가 떨어져나오는 모습을 볼 수 있다~
앞으로도 인터셉터와 어노테이션 기능을 잘 활용하여 로직을 잘 추상화하고 반복을 제거해보자