BackEnd/Spring

어노테이션은 사실 깡통이다.

Potwings 2024. 9. 22. 19:48

이전글 : https://potwings.tistory.com/66

 

[Spring MVC] Filter에서 파라미터 처리하는 방법

Servlet의 Filter를 구현하는 Filter클래스에서 POST로 전달되는 Body에서 JSON을 불러와 처리해야할 일이 있었다.따라서 Filter에서 아래와 같이 Request에서 값을 불러왔다. @Override public void doFilter(ServletRequ

potwings.tistory.com

 

이전 글에서 Request에서 JSON으로 된 파라미터를 불러올 때 아래와 직접 코드 작성하여 파라미터를 받아왔었다.

허나 평소에 Controller에서 JSON을 전달받을 때는 파라미터 앞에 @RequestBody를 추가해주는 것 만으로 불러올 수 있었다.

 

 

따라서 @RequestBody는 어떻게 처리를 해주길래 위와 같은 코드를 생략할 수 있는지 궁금해 그 내부를 확인해보게 되었다.

 

근데 막상 열어보니 RequestBody에는 아무 내용이 없었다.

@Target(ElementType.PARAMETER) // 적용 대상 - 파라미터
@Retention(RetentionPolicy.RUNTIME) // 컴파일러에 의해 class 파일에 기록 & 런타임에 유지
@Documented // javaDoc에 자동으로 기록
public @interface RequestBody {

	boolean required() default true;
}

 

그렇다면 내용이 아무것도 없음에도 불구하고 @RequestBody는 어떻게 Body의 데이터를 불러와 처리해줄까?

 

 

@RequestBody는 단순 마킹 용도이다.

이를 확인하기 위해 그 과정을 직접 디버깅 해보았다.

근데 생각보다 너무 복잡하니 궁금한 사람만 확인해보자

 

더보기

파라미터에 대한 처리를 진행하기 위해서 HandlerMethodArgumentResolverComposite의 resolveArgument 를 진행한다.

  @Nullable
  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter); // 사용할 resolver 탐색
    if (resolver == null) {
      throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
    } else {
      return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); // 파라미터 처리
    }
  }

 

여기서 getArgumentResolver해당 파라미터를 처리할 수 있는 resolver를 탐색하는 역할을 한다.

  @Nullable
  private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
    if (result == null) {
      Iterator var3 = this.argumentResolvers.iterator();

      while(var3.hasNext()) {
        HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
        // 해당 resolver 지원 여부 확인
        if (resolver.supportsParameter(parameter)) {
          result = resolver;
          this.argumentResolverCache.put(parameter, resolver); // 캐싱 진행
          break;
        }
      }
    }

    return result;
  }

 

이 메소드에서는 파라미터의 어노테이션을 확인하여 어떤 resolver를 통해 처리할지 결정한다.

또 결정 후에는 캐싱 진행하여 이후의 요청에 대해선 좀 더 빠르게 처리할 수 있게 해준다.

 

RequestBody는 RequestResponseBodyMethodProcessor를 통하여 처리되는데 아래와 같이 파라미터에 RequestBody가 있는지를 확인한다.

  public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(RequestBody.class);
  }

 

사용할 resolver를 결정하였으면 이제 resolver의 resolveArgument 메소드를 진행한다.

 

우리는 RequestBody에 대해 확인하고 있으므로 그 resolver인 RequestResponseBodyMethodProcessor의 resolveArgument메소드를 확인해보자

  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    parameter = parameter.nestedIfOptional();
    // 파라미터에 대한 처리 진행
    Object arg = this.readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    String name = Conventions.getVariableNameForParameter(parameter);
    
    //... 이하 생략
}

 

여기서는 파라미터들만 확인 후 실제 파라미터에 대한 처리는 readWithMessageConverters에서 진행한다.

 

이제 실제 처리를 위해 contentType(application/json)과 targetClass(변환할 자바 클래스)를 확인한다.

그리고 resolver를 찾을 때와 동일하게 지원하는 converter를 결정 후 변환을 진행한다.

  @Nullable
  protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

	//... 생략

        HttpMessageConverter converter;
        Class converterType;
        GenericHttpMessageConverter genericConverter;
        while(true) {
          if (!var11.hasNext()) {
            break label94;
          }

          converter = (HttpMessageConverter)var11.next();
          converterType = converter.getClass();
          genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
          // converter 확인
          if (genericConverter != null) {
            if (genericConverter.canRead(targetType, contextClass, contentType)) {
              break;
            }
          } else if (targetClass != null && converter.canRead(targetClass, contentType)) {
            break;
          }
        }

        if (message.hasBody()) {
          HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
		  // Converter를 통한 변환 진행
          body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : converter.read(targetClass, msgToUse);
          body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
        } else {
          body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
        }

		// ...생략
}

 

이와 같은 처리를 통해서 @RequestBody는 전달받은 json을 자바 객체로 변환하여준다.

 

간단히 설명하자면 파라미터에 @RequestBody가 존재하면 해당 파라미터는 변환이 필요하다 체크를 해둔 후 resolver를 통하여 데이터 변환을 진행해주는 것이다.

 

즉 @RequestBody는 실제 처리를 하는 것이 아닌 "이 파라미터 처리 부탁드려요~" 하는 역할로 보면 되겠다.

 

그럼 다른 어노테이션은?

java 공식문서의 어노테이션 부분을 확인해보면 (https://docs.oracle.com/javase/tutorial/java/annotations/)

 

"어노테이션은 주석이 달려있는 코드에 직접적으로 영향을 미치지 않는다." 라고 나와있다.

 

모든 어노테이션을 확인해보진 못했으나 lombok 같은 경우도 실제 동작하는 내용은 없었다.

 

결론

어노테이션은 직접 동작하는 주체가 아닌 다른 주체(컴파일러 등등)에게 동작을 요청하는 지시자로 생각하면 되겠다.