Custom Annotation 만들기 - 메타 어노테이션
회사 프로젝트에서 로그인 안 한 유저와 로그인 한 유저가 사용할 수 있는 기능을 하나의 Controller에 모두 모여있는 상황이었다. 그래서 로그인을 해야만 사용할 수 있는 기능에 인가된 회원인지 확인하는 어노테이션을 커스텀으로 만들어보려고 한다.
1. 간단 프로세스 설명
- 해당 프로젝트에서 사용자 인증에 JWT를 사용하고 있다.
- 로그인에 성공하면 accessToken을 요청의 'Authorization' 헤더에 담아 보내고, 로그인한 사용자의 아이디를 토큰의 'claim'에 추가해 준다.
- 인가된 회원인지 확인하는 방법은 Controller 단계에서 요청이 들어온 header에 Authorization 요소가 있는지 확인하고, 그 토큰에서 userId(로그인 아이디)를 추출할 수 있는지 확인하는 방향로 정했다.
2. 메타 어노테이션이란?
"메타-어노테이션(Meta-Annotation)"이라는 용어는 어노테이션을 정의할 때 사용되는 어노테이션을 지칭한다.
즉, 어노테이션을 위한 어노테이션이다. Java에서는 커스텀 어노테이션을 정의할 때, 그 어노테이션의 동작 방식이나 적용 대상, 유지 정책 등을 지정하기 위해 메타-어노테이션을 사용한다.
주로 사용되는 메타-어노테이션은 다음과 같다:
- @Retention: 이 어노테이션은 어노테이션이 유지되는 기간을 지정한다. 예를 들어, RetentionPolicy.SOURCE, RetentionPolicy.CLASS, RetentionPolicy.RUNTIME 등이 있다.
- @Target: 이 어노테이션은 해당 어노테이션이 적용될 수 있는 대상을 지정한다. 예를 들어, 메소드, 필드, 클래스 등이 있다.
- @Documented: 이 어노테이션은 해당 어노테이션이 Javadoc에 포함될지 여부를 지정한다.
- @Inherited: 이 어노테이션은 어노테이션이 하위 클래스에 상속될지 여부를 지정한다.
이러한 메타-어노테이션을 사용하여 커스텀 어노테이션의 동작을 지정하는 방식을 "메타-어노테이션 방법"이라고 부른다.
3. 실습 코드
3-1. Custom Annotation interface 생성
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Authentication {
}
- @Retention(RetentionPolicy.RUNTIME):
- @Retention 어노테이션은 어노테이션 정보가 어느 시점까지 유지될 것인지를 지정한다.
- RetentionPolicy.RUNTIME을 지정함으로써, 런타임 중에도 이 어노테이션 정보를 읽을 수 있다.
- 만약 RetentionPolicy.CLASS나 RetentionPolicy.SOURCE를 지정한다면, 런타임 중에는 해당 어노테이션 정보를 읽을 수 없다.
- @Target({ElementType.METHOD, ElementType.TYPE}):
- @Target 어노테이션은 해당 어노테이션이 적용될 수 있는 대상의 종류를 지정한다.
- ElementType.METHOD와 ElementType.TYPE을 지정함으로써, 이 어노테이션은 메서드와 클래스 혹은 인터페이스에 적용할 수 있음을 나타낸다.
- 따라서, 위의 코드에서 정의한 @Authentication 어노테이션은 런타임 시에도 정보를 유지할 수 있고, 메서드나 클래스/인터페이스에 적용할 수 있다. 이를 통해 런타임 중에 메서드나 클래스의 인증 상태를 체크하는 등의 처리를 할 수 있다.
3-2. 모든 Controller 타기전에 작동할 advisor 생성
import api.utils.JwtProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class JwtAdvisor {
private final JwtProvider jwtProvider;
@ModelAttribute("userId")
public String authorization() {
String userId = null;
// 현재 HTTP 요청 정보를 가져와 HttpServletRequest 객체에 저장합니다.
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
// 현재 요청에 가장 잘 매칭되는 핸들러 메서드를 가져와 HandlerMethod 객체에 저장합니다.
HandlerMethod handlerMethod = (HandlerMethod) RequestContextHolder.currentRequestAttributes()
.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
// 앞에서 가져온 HandlerMethod 객체에서 Java의 Method 객체를 추출합니다. 이 객체는 실행될 메서드에 대한 정보를 담고 있습니다.
Method method = handlerMethod.getMethod();
// 실행될 메서드에 @Authentication 어노테이션이 붙어 있는지 확인합니다. 있다면 auth 변수에 저장됩니다.
Authentication auth = method.getAnnotation(Authentication.class);
// @Authentication 어노테이션이 붙어 있다면, 이하의 로직을 수행합니다.
if(Objects.nonNull(auth)){
// 요청 헤더에서 "authorization" 값을 가져와 token 변수에 저장합니다.
String token = request.getHeader("authorization");
try{
String validateResult = jwtProvider.validateToken(token);
userId = validateResult;
}catch (Exception ignored){} // validation 예외 발생해도 무시
// userId 값을 반환합니다. 이 값은 @ModelAttribute("userId")에 의해 모델 어트리뷰트로 설정됩니다.
return userId;
}
// @Authentication 어노테이션이 없는 경우, null을 반환합니다.
return null;
}
}
1. 메소드 내용
- @RestControllerAdvice: 예외 처리나, 바인딩 설정, 모델 어트리뷰트 등을 모든 @Controller에 전역으로 적용할 수 있게 한다.
- 핸들러 메서드와 해당 메서드에 붙어있는 @Authentication 어노테이션을 확인한다.
- 만약 @Authentication 어노테이션이 붙어 있다면, HTTP 헤더에서 "authorization" 필드로 JWT 토큰을 가져와 검증한다. 검증을 통과하면 해당 유저의 ID를 반환한다.
2. @Authentication 어노테이션이 붙어 있는 메서드에 대해서만 JWT 검증을 수행한다. 이렇게 하여 특정 API 요청에 대한 접근 제어나 사용자 인증을 더 세밀하게 관리할 수 있다.
※ 여기서 메소드 위에 붙은 @ModelAttribute 어노테이션의 두가지 사용법에 대해서도 알아가자.
@ModelAttribute 어노테이션은 Spring MVC에서 모델의 속성을 추가하거나 바인딩하기 위해 사용된다. 이 어노테이션은 주로 컨트롤러의 메서드 인자나 메서드 자체에 붙을 수 있으며, 그 역할은 다음과 같다:
1. 메서드에 붙은 경우: 해당 메서드는 컨트롤러 메서드가 호출되기 전에 먼저 실행된다. 메서드의 반환 값은 지정한 이름("userId" in this case)으로 모델에 자동으로 추가된다. 이 값은 뷰에서 사용할 수 있고, 또한 요청 처리 도중에도 다른 메서드에서 접근할 수 있다.
2. 메서드 인자에 붙은 경우: 요청 파라미터를 해당 인자로 바인딩하는 역할을 한다.
지금 위 코드에서는 @ModelAttribute("userId")가 메서드 위에 붙어있으므로, authorization() 메서드의 반환 값 (즉, userId)이 "userId"라는 이름으로 모델에 추가된다. 이렇게 하면 이 값은 뷰 템플릿에서 접근할 수 있고, 현재 요청-응답 라이프사이클 내에서 다른 컨트롤러 메서드에서도 접근할 수 있다.
단순히 말해서, @ModelAttribute("userId")를 사용함으로써 authorization() 메서드가 반환하는 userId 값을 다른 뷰나 컨트롤러 메서드에서도 쉽게 사용할 수 있게 된다.
3-3. 실제 사용하는 모습 - Controller class
@GetMapping("/auth/{param1}")
@Authentication
public ResponseEntity<ResponseDto<ResponseDto>> method1(
@ModelAttribute("userId") String userId
) {
return null;
}
4. 마무리
이렇게 해서 하나의 컨트롤러에서 로그인 유저와 로그인 안 한 유저가 사용할 수 있는 요청을 구별했다. 이 두 개를 구별하는 가장 좋은 방법은 컨트롤러부터 구별해서 구성하는 게 제일 좋겠지만 이미 선개발이 진행된 상황에서 기획이 바뀌면서 이렇게 개발 방향이 중간에 바뀐 게 조금 아쉽긴 하지만 나름 해결 방법을 잘 찾은 것 같다.
다음 포스팅에서 커스텀 어노테이션 만드는 다른 방법으로 AOP를 이용해서 만드는 방법을 작성해야겠다.
Custom Annotation 만들기 - AOP(Aspect-Oriented Programming)
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] Spring Batch 코드 작성 후 실행 시 Table doesn't exist 에러 해결 (0) | 2023.11.07 |
---|---|
[Spring Boot] 의존성 버전문제로 인한 오류 해결 (1) | 2023.11.07 |
[Spring Boot] RequestDTO로 요청받을때 @RequestBody를 작성하는 경우와 작성하지 않는 경우 (1) | 2023.11.07 |
이해하기 쉬운 CORS 및 API 개발 안내서 (Access-Control-Allow-Headers) (1) | 2023.11.05 |
[Spring Boot] Custom Annotation 만들기 - AOP(Aspect-Oriented Programming) (1) | 2023.10.24 |