코인 개발에 본격적으로 참여하기 전에 코인 마이그레이션에 적용된 커스텀 어노테이션의 작용 방식을 분석하라는 과제를 받았다. 다음은 커스텀 어노테이션을 어떻게 적용하는 지와 그것을 분석하는 내용이다.
공통적인 로직이 있다면 스프링에서 interceptor, filter, aop등의 다양한 방법으로 처리할 수 있다. 하지만 공통적인 로직을 처리해서 어떤 결과 값을 컨트롤러에 넘겨줘야 한다면 어떻게 하는 것이 좋을까
예를 들어, 클라이언트 측에서 header에 토큰을 담아서 request를 보내는 경우가 있다고 가정하자. 그러면 아래 거의 모든 api에서 해당 요청을 보낸 사용자가 누구인지 사용자 정보가 필요하다
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
@GetMapping("/test1")
public void test1(@RequestHeader String token) {
// 이 API에서는 사용자 정보가 필요하다
}
@PostMapping("/test2")
public void test2(@RequestHeader String token) {
// 이 API에서도 사용자 정보가 필요하다
}
@GetMapping("/test3")
public void test3(@RequestHeader String token) {
// 이 API에서도 사용자 정보가 필요하다
}
@GetMapping("/test4")
public void test4(@RequestHeader String token) {
// 이 API에서도 사용자 정보가 필요하다
}
}
이 경우 중복되는 로직을 메소드를 만들면 해결될까?
public User getLoginUser(String token) {
return userRepository.findByEmail(token).orElseThrow();
}
package com.feelcoding.argumentresolverdemo.controller;
import com.feelcoding.argumentresolverdemo.User;
import com.feelcoding.argumentresolverdemo.dto.SignUpRequestDto;
import com.feelcoding.argumentresolverdemo.dto.SignUpResponseDto;
import com.feelcoding.argumentresolverdemo.service.AuthService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
private final AuthService authService;
@GetMapping("/test1")
public void test1(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// 어쩌구 저쩌구
}
@PostMapping("/test2")
public void test2(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// 어쩌구 저쩌구
}
@GetMapping("/test3")
public void test3(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// 어쩌구 저쩌구
}
@GetMapping("/test4")
public void test4(@RequestHeader String token) {
User user = authService.getLoginUser(token);
// 어쩌구 저쩌구
}
}
하지만 여전히 반복되는 코드가 들어가 있다.
이런 경우에 @Auth어노테이션만 달면 토큰을 읽어서 사용자의 아이디를 반환 할 수 있으면 얼마나 좋을까? 이런식으로 말이다
@GetMapping("/test1")
public void test1(@Auth Long userId) {
// 어쩌구 저쩌구
}
@PostMapping("/test2")
public void test2(@Auth Long userId) {
// 어쩌구 저쩌구
}
@GetMapping("/test3")
public void test3(@Auth Long userId) {
// 어쩌구 저쩌구
}
@GetMapping("/test4")
public void test4(@Auth Long userId) {
// 어쩌구 저쩌구
}
이러한 이유로 커스텀 어노테이션을 만드는 것이다.
먼저 Auth 어노테이션을 만들자
어노테이션을 만들 때 interface 앞에 @를 붙이면 된다.
밑과 같은 방식으로 말이다.
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface Auth {
UserType[] permit() default {};
boolean anonymous() default false;
}
UserType은 enum 클래스로 사장님과 학생 두가지 값을 갖고 있다. anonymous는 로그인 하지 않고 사용하는 유저를 위한 값이다.
@Target 과 @Retention
우선 @Retention은 어노테이션의 지속 시간을 정한다. 우리는 실행중에 사용할 것이니 RUNTIME을 적용했다.
@Target(PARAMETER)는 이 어노테이션을 파라미터에 적용할 것이란 뜻이다.
아래와 같은 방식으로 말이다.
@GetMapping("/articles/{id}")
public ResponseEntity<ArticleResponse> getArticle(
@Auth(permit = {STUDENT}, anonymous = true) Long userId,
@PathVariable("id") Long articleId,
@IpAddress String ipAddress
) {
// 내용
}
HandlerMethodArgumentResolver
다음은 HandlerMethodArgumentResolver를 상속 받아 커스텀 HandlerMethodArgumentResolver클래스를 만들면 된다.
HandlerMethodHandlerMethodArgumentResolver는 컨트롤러 메서드에서 특정한 조건에 맞는 파라미터가 있을 때 원하는 값을 바인딩 해주는 인터페이스라고 한다.
@Component
@RequiredArgsConstructor
public class AuthArgumentResolver implements HandlerMethodArgumentResolver {
private final UserRepository userRepository;
private final AuthContext authContext;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Auth.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
Auth authAt = parameter.getParameterAnnotation(Auth.class);
requireNonNull(authAt);
List<UserType> permitStatus = Arrays.asList(authAt.permit());
Long userId = authContext.getUserId();
if (isAnonymous(userId, authAt)) {
return null;
}
User user = userRepository.getById(userId);
if (permitStatus.contains(user.getUserType())) {
return user.getId();
}
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
throw AuthException.withDetail("header: " + request);
}
private static boolean isAnonymous(Long userId, Auth authAt) {
if (userId == null) {
if (authAt.anonymous()) {
return true;
}
throw AuthException.withDetail("userId is null");
}
return false;
}
}
인터페이스 HandlerMethodArgumentResolver를 상속 받으면 supportsParameter와 resolveArgument 두 메서드를 구현해야 한다
이 클래스를 살펴보자
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Auth.class);
}
우리가 만든 커스텀 어노테이션인 Auth가 있는지 확인하고 없을 경우 false를 반환한다
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
Auth authAt = parameter.getParameterAnnotation(Auth.class);
requireNonNull(authAt);
List<UserType> permitStatus = Arrays.asList(authAt.permit());
Long userId = authContext.getUserId();
if (isAnonymous(userId, authAt)) {
return null;
}
User user = userRepository.getById(userId);
if (permitStatus.contains(user.getUserType())) {
return user.getId();
}
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
throw AuthException.withDetail("header: " + request);
}
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final AuthArgumentResolver authArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authArgumentResolver);
}
}
이렇게 생성하면 이제 authArgumentResolver가 스프링에서 인식될 수 있다!
HandlerMethodArgumentResolver는 항상 addArgumentResolvers를 거쳐서 추가해야한다.
참고자료 출처 :
+https://velog.io/@sin8282/Custom-annotation-parameterMap-%EB%A7%8C%EB%93%A4%EA%B8%B0
+https://breakcoding.tistory.com/402