WEB/Spring

Interceptor 활용하기 ( feat . ArgumentResolver, Custom Annotation )

 

동기분의 도움으로 처음으로 Interceptor 를 적용해보아서 나중에 사용할 일이 있을 때 찾아보려고 써봅니다!

아래와 같은 Controller 에 Header 에서 인증을 위한 값을 받아야지만 접근할 수 있도록 설정하겠습니다!

 

@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrdersController {

  private final OrdersService ordersService;


  @Auth(type = ApiServiceType.HOMEPAGE)
  @GetMapping
  public ResponseEntity<List<OrdersDto>> getOrders(
      @RequestParam(required = false) String searchKeyword,
      @AuthAccount Long accountId
  ) {
    List<OrdersDto> response = ordersService.getAccessibleOrders(accountId,
        searchKeyword);

    return new ResponseEntity<>(response, HttpStatus.OK);
  }
}

보통은 아래와 같이 경로를 추가해서 interceptor를 걸어주는데 커스텀 어노테이션으로 활용할수도 있더라구요! ( 정말 생각도 못했다,,)

registry.addInterceptor(interceptor)
    .addPathPatterns("/**")
    .excludePathPatterns("/main");

공식문서!

https://www.baeldung.com/java-custom-annotation

아래와 같이 커스텀 어노테이션을 만들어 줍니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auth {

  ApiServiceType type();
}


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthAccount {

}

@Retention 은 말그대로 이 어노테이션이 언제까지 유지할지를 정합니다. 

- RUNTIME 런타임 까지 유지한다. = 사실상 사라지지 않는다. (메모리에 같이 올라감)

- SOURCE 소스코드 까지 유지한다. =  .java 소스까지 유지한다 (컴파일되어 클래스 파일이 되면 사라진다) 

- CLASS 클래스파일 까지 유지한다. = 컴파일한 class 파일까지 유지한다 (메모리로 읽어오면 사라짐)

 

사실 어떤식으로 사용해야할지 감이 안온다 ... 몇가지 찾아본게 있다면

 

스프링의 대부분의 어노테이션은  RUNTIME 으로 되어있다고 한다.

왜냐하면 실행중인 상태에서 스프링 컴포넌트 스캔이 스캔을 해야하기 때문에!

@getter 같은 경우는 롬복이 코드를 생성해주는 거니까 바이트코드에 소스가 들어갈 필요가 없어서 SOURCE 로 되어있다.

Maven 이나 Gradle 에서 받은 라이브러리 들은 class 파일로 있기 때문에 어노테이션의 정보를 정보를 조회하기 위해 CLASS 로 되어있다.

 

@Target 은 자바 컴파일러가 annotation이 어디에 적용될지 결정하기 위해 사용합니다! 

ElementType.PACKAGE : 패키지 선언
ElementType.TYPE : 타입 선언
ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
ElementType.CONSTRUCTOR : 생성자 선언
ElementType.FIELD : 멤버 변수 선언
ElementType.LOCAL_VARIABLE : 지역 변수 선언
ElementType.METHOD : 메서드 선언
ElementType.PARAMETER : 전달인자 선언
ElementType.TYPE_PARAMETER : 전달인자 타입 선언
ElementType.TYPE_USE : 타입 선언

 

 

공식문서! 에서 보면 array 로 여러개를 사용할 수도 있네요!

https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/ElementType.html

 

ElementType (Java Platform SE 8 )

The constants of this enumerated type provide a simple classification of the syntactic locations where annotations may appear in a Java program. These constants are used in java.lang.annotation.Target meta-annotations to specify where it is legal to write

docs.oracle.com

 

지금은 METHOD , PARAMETER 를 어떻게 사용했는지 보겠습니다!

처음 컨트롤러에서 아래와 같이 쓰인것은 커트텀 어노테이션인 @Auth 의 ElmentType 이 METHD 인데 메서드를 선언할 수 있다는 겁니다!  그래서 위에 코드를 확인해보시면  ApiServiceType은 enum 으로 메서드를 선언해놓았습니다! 

@Auth(type = ApiServiceType.HOMEPAGE)

@AuthAccount 는 ElemntType 이 parameter 로  컨트롤러에서 파라미터로 받고 있는걸 확인할 수 있습니다

 

 

본격적으로 Interceptor 적용을 해보겠습니다!

Interceptor 적용할 class 를 만들어줍니다!

@Component
@RequiredArgsConstructor
public class Interceptor implements HandlerInterceptor {

  private final AuthService authService;


  // 호출한 Controller 가 실행되기전 실행되는 메서드 (사전 제어)
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    // MethodAnnotation 으로 컨트롤러에 Auth.class 로 지정한 값을 가지고온다
    // 현재 Api.ServiceType.HOMPAGE 로 지정했음(Controller class 확인)
    Auth auth = handlerMethod.getMethodAnnotation(Auth.class);
    
    if (auth == null) { 
      return true;
    }

    AuthCheck authCheck = AuthCheck.builder()
    	.authId(request.getHeader("auth_id"))
        .authSecret(request.getHeader("auth_secret"))
        // hadlerMethod 로 가지고온 auth
        .apiServiceType(auth.type())
        .build();
        
	// 인증 로직에서 실패
    if (!authService.authentication(authCehck)) {
      throw new InvalidClientException();
    }
	// return 이 true 일경우 해당 Controller로 넘어감 
    return true; 
  }
}
implements HandlerInterceptor

여기서 HandlerInterceptor 를 implemnts 받아서 Interverptor 를 적용하면 됩니다. 공식문서에서 아래와 같이 설명되어있습니다!

Spring interceptor 는 HandlerInterceptor 를 이용하여 구현하거나 HandlerInterceptorAdapter 를 이용해 확장할 수 있다고 나와있네요! 

다시 말해서 인터페이스를  구현하거나 추상클래스를  오버라이딩을 통해서 자신만의 인터셉터를 만드는거네요!

 

    HandlerMethod handlerMethod = (HandlerMethod) handler;

 

이 부분 에서 handler 는 현재 실행하려는 메서드 자체를 의미합니다, 현재 실행되는 컨트롤러를 파악하는 작업이 가능하죠!

그래서 handlerMethod.get 을 통해 현재 실행한 컨트롤러의 Auth.class 값을 가지고 오거라~ 입니다! 만약 Auth.class 를 붙이지 않은 컨트롤러이면 interceptor를 거쳐갈 필요가 없으니 true 를 반환해서 Controller 가 실행되게 합니다

 

 

이제 HandlerMethodArgumentResolver 가 어떻게 쓰이는지 확인해봅시다

 

주어진 요청을 처리할 때, 메서드 파라미터를 인자값들에 주입 해주는 인터페이스 입니다!

인증에 필요한 parameter 이 중복으로 발생될 수 있기 때문에 공통으로 입력되는 작업들을 한번에 처리하고 싶을 때 사용합니다!

 

 

 

@Component
public class ArgumentResolver implements HandlerMethodArgumentResolver {

  @Override
  public boolean supportsParameter(MethodParameter methodParameter) {
    return methodParameter.hasParameterAnnotation(AuthAccount.class);
  }

  @Override
  public Object resolveArgument(MethodParameter methodParameter,
      ModelAndViewContainer modelAndViewContainer, NativeWebRequest request,
      WebDataBinderFactory webDataBinderFactory) throws Exception {

    String accountId = request.getHeader("accountId");

    return crypto.toDecryptAsLong(accountId);
  }
}

supportsParameter 어떤 파라미터에 대해 작업을 할건지 정의 하는 메서드 입니다. 

저희는 커스텀 어노테이션인 @AuthAccount 를 사용 했으니 hsParameterAnnotaion 을 통해 확인합니다. 

만약 커스텀 어노테이션인 아닌 파라미터로 객체 (Dto) 로 사용하고 싶다면 아래와 같이 사용합니다. 하지만 orderDto 를 파라미터로 받고 있는 컨트롤러가 많다면... 같은 작업을 수행하는게 아니라면(보통 그렇겠죠..?) 어디서 알수 없는 값이 튀어나올지 예상할 수 없을 거같아 비추한다

return methodParameter.getParameterType().equals(orderDto.class);

 resolveArgument 는 바인딩할 객체를 조작하는 메서드 입니다!

저는 저기서 AuthAccount 로 받은 accountId 이 암호화되어 있기 때문에 복호화 해주어 리턴해주고 있습니다.

 

이제 이 인터셉터와 리볼버를 사용하도록 등록해줄 차례입니다! 

 

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

  private final ArgumentResolver argumentResolver;
  private final Interceptor interceptor;

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(interceptor);
  }

  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolver) {
    resolver.add(argumentResolver);
  }
}

 

휴^^....;; 틀린부분이 있다면 댓글 남겨주세요! 

'WEB > Spring' 카테고리의 다른 글

Spring boot + JWT + RefreshToken 구현하기  (10) 2021.01.29
Spring boot에서 JSP 사용하기  (0) 2021.01.25
Mapper Interface?  (0) 2020.07.23
Spring 총정리 3.Annotation  (0) 2020.06.13
Spring 총정리 2.Spring 3대 작동원리  (0) 2020.06.06