애노테이션은 프로그램에 추가적인 정보를 제공해주는 메타데이터이다.
메타데이터란 컴파일 과정과 런타임에서 코드를 어떻게 컴파일하고 처리할 것인지에 대한 정보를 말한다
잘 활용하면 체계가 잡혀있는 깔끔한 코드를 작성할 수 있다.
애노테이션 옵션에 따라 컴파일 전까지만 유효하도록 할 수 도 있고, 컴파일 시기에 처리할 수도 있고, 런타임에 처리할 수도 있다.
@Target({ElementType.[적용대상]})
@Retention(RetentionPolicy.[정보유지되는 대상])
public @interface [어노테이션명]{
public 타입 elementName() [default 값]
...
}
@Retention
: 애노테이션이 어느 시점까지 영향을 미치는지 결정
@RetentionPolicy.SOURCE
: 컴파일 전까지만 유효
@RetentionPolicy.CLASS
: 컴파일러가 클래스를 참조할 때까지 유효
@RetentionPolicy.RUNTIME
: 컴파일 이후 런타임 시기에도 JVM에 의해 참조 가능
@Target
: 애노테이션을 적용할 대상 선택
@Documented
: 애노테이션을 javadoc에 포함시킴
@Inherited
: 애노테이션의 상속을 가능하게 함
@Repeatable
: 연속적으로 애노테이션을 선언할 수 있게 함
애노테이션은 특별한 종류의 인터페이스로 취급한다
@interface
로 선언한다
@interface
는 자동으로 Annotation 클래스를 상속한다.
내부의 메서드들은 abstract 키워드가 자동으로 붙는다.
애노테이션 인터페이스는 extends 절을 가질 수 없다
애노테이션 타입 선언은 제네릭일 수 없다
메서드는 매개변수를 가질 수 없다
메서드는 타입 매개변수를 가질 수 없다
메서드 선언은 throw 절을 가질 수 없다
정보를 가짐으로써 애노테이션의 역할은 끝난다.
정보를 이용해 역할을 하는 다른 무언가가 필요하다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
boolean required() default true;
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggedInMember {
boolean required() default true;
}
다음은 프로젝트에서 팀원이 로그인 기능을 구현하기 위해 만든 커스텀 애노테이션 @Login
과 @LoggedInMember
이다.
이 애노테이션을 활용해 어떻게 로그인을 구현했을까?
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final JwtTokenProvider jwtTokenProvider;
private static final String AUTHORIZATION_HEADER_PREFIX = "Bearer ";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(!(handler instanceof HandlerMethod)) return true;
HandlerMethod handlerMethod = (HandlerMethod) handler;
Login loginAnnotation = handlerMethod.getMethodAnnotation(Login.class);
if (loginAnnotation == null) return true;
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if(header != null) {
validHeader(header);
validAccessToken(header);
return true;
}
validLoginRequired(loginAnnotation);
return true;
}
private void validHeader(String header) {
if(!header.startsWith(AUTHORIZATION_HEADER_PREFIX)){
throw new InvalidAccessTokenException();
}
}
private void validAccessToken(String header) {
String accessToken = header.replaceFirst(AUTHORIZATION_HEADER_PREFIX, "");
jwtTokenProvider.validToken(accessToken);
}
private void validLoginRequired(Login loginAnnotation) {
if(loginAnnotation.required()) throw new NotExistAccessTokenException(); // required true인데 토큰이 없는 경우
}
}
먼저 인터셉터를 활용해 로그인을 구현했다.
로그인이 필요한 메서드라면 @Login
애노테이션을 단다.
그럼 인터셉터의 prehandle에서 @Login
애노테이션이 달려 있는지 검사한다.
달려 있다면 헤더를 통해 로그인 여부를 검증해 예외를 던지거나 통과시킨다.
이런 식으로 특정 조건을 요구하는 애노테이션을 만들어 인터셉터를 통해 검증할 수 있다.
@Component
@RequiredArgsConstructor
public class LoggedInArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtTokenProvider jwtTokenProvider;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoggedInMember.class)
&& Long.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String headerAuthorization = webRequest.getHeader(HttpHeaders.AUTHORIZATION);
LoggedInMember loggedInMember = parameter.getParameterAnnotation(LoggedInMember.class);
if (!loggedInMember.required() && headerAuthorization == null) {
return null;
}
validExistAccessTokenInHeader(headerAuthorization);
String accessToken = headerAuthorization.split(" ")[1];
return jwtTokenProvider.getMemberId(accessToken);
}
private void validExistAccessTokenInHeader(String headerAuthorization) {
if(headerAuthorization == null) throw new NotExistAccessTokenException();
}
}
다음과 같이 ArgumentResolver를 활용해 로그인 한 멤버의 id를 가져오기 위한 @LoggedInMember
애노테이션을 활용할 수 있다.
supportsParameter를 통해 파라미터에 LoggedInMember 애노테이션이 달려있는지 확인한다. 그리고 동시에 Long 타입의 파라미터인지 확인한다. supportsParameter의 리턴값이 true여야 resolveArgument 메서드로 진행한다.
resolveArgument 메서드에서는 요청의 AUTHORIZATION 헤더와 파라미터의 애노테이션을 가져온다.
먼저 애노테이션의 required 값이 false이고 headerAuthorization이 null이라면 @LoggedInMember
애노테이션이 달려 있는 파라미터 이지만 로그인 하지 않은 멤버도 접근할 수 있는 경우다. 이 경우 null을 반환한다.
아닐 경우 헤더를 검증하고 accessToken을 얻어서 memberId를 반환한다.
@GetMapping("/mypage/account-settings")
@Login
public MemberAccountInfoResponse showAccountInfo(@LoggedInMember Long memberId) {
return memberService.getMemberAccount(memberId);
}
컨트롤러 단에서 이런 식으로 활용된다.