Spring Boot version 2.1.x에서 2.2.x (spring frame 5.2 이상)으로 버전업 진행 시 Spring Cloud AWS SnsAutoConfiguration 에서 TypeNotPresentExceptionProxy 가 발생하며 실행안되는 문제

web/Spring|2020. 8. 21. 15:10

문제 발생


평소 문제가 많았던 webflux 부분 수정을 위해 spring boot 2.1.3에서 2.2.7버전으로 업그레이드를 진행하기 위해 gradle에서 spring boot version을 2.2.7로 변경하고 애플리케이션을 실행 시켰다. 

 

그런데 평소에는 자주본적이 없던 TypeNotPresentExceptionProxy 에러가 발생했다. 

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.baemin.bmart.search.BmartSearchAdminApplication]; nested exception is java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy
	at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:609)
	at org.springframework.context.annotation.ConfigurationClassParser.access$800(ConfigurationClassParser.java:110)
	at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGroupingHandler.lambda$processGroupImports$1(ConfigurationClassParser.java:811)
	at java.util.ArrayList.forEach(ArrayList.java:1257)

에러가 발생한 호출 스택 정보를 보니 ConfigurationClassParser에 processImports에서 발생한걸로 보아 @Configuration 어노테이션이 적용된 클래스에서 빈정보를 import하다가 에러가 발생한 것 같았다.

 

그래서 어떤 에러로 인해 발생한 것인지 확인해보기 위해서 TypeNotPresentExceptionProxy 클래스에 디버깅을 찍고 문제가 발생한 로그를 확인해봤다.

 

java.lang.NoClassDefFoundError org/springframework/web/servlet/config/annotation/WebMvcConfigurer 가 발행했다.

 

WebMvcConfigurer NoClassDefFoundError가 발생할 이유가 없는데 발생하여 난감해하고 있었는데 확인해보니 내부적으로 사용중이던 compile 'org.springframework.cloud:spring-cloud-aws-messaging' 라이브러리에서 SnsWebConfiguration이 있는데 이게 2.1.0버전에는 아래와 같이 구현되어 있다.

@Configuration
@ConditionalOnClass("org.springframework.web.servlet.config.annotation.WebMvcConfigurer")
public class SnsWebConfiguration implements WebMvcConfigurer {

    @Autowired
    private AmazonSNS amazonSns;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(getNotificationHandlerMethodArgumentResolver(this.amazonSns));
    }
}

여기서 Class가 class path에 잘 있는지 확인하는 @ConditionalOnClass 어노테이션의 value에 WebMvcConfigurer 클래스의 존재를 체크하도록 설정되어 있다.

 

@ConditionalOnClass의 경우에는 해당 클래스가 잘 있으면 Bean으로 등록하라는 하나의 조건인데 만약 WebMvcConfigurer이 없으면 해당 Configuraion을 안쓰면 되는데 왜 죽은거서 인가 하고 의아했다.

 

여기서 같은 실에 개발자인 용근님이 올리신 issue를 보고 알게 되었다. 

https://github.com/spring-cloud/spring-cloud-aws/issues/549

https://github.com/spring-cloud/spring-cloud-aws/issues/503

 

ArrayStoreException in SnsWebConfiguration. · Issue #549 · spring-cloud/spring-cloud-aws

Hello, Because implement and ConditionalOnClass are specified in SnsWebConfiguration at the same time, an exception occurs during the class load. When WebMvcConfigurer does not exist, Caused by: ja...

github.com

SnsWebConfiguration에서 WebMvcConfigurer를 implements하고 있는데 해당 classpath를 찾을 수 없어 발생한 오류입니다.

 

그런데 왜 단순히 spring boot version만 올렸고 spring cloud aws 버전은 2.1.0버전 그대로 사용하고 있는데, 이게 2.1.3버전에서는 발생하지 않고 2.2.7버전에서만 발생한건지 궁금해서 원인을 찾아보았다.

 

 

 

원인분석


우선 두 개의 버전에서 어떤 차이가 있는지 정확하게 알지 못하기에 spring boot가 뜨기 위해서 호출하는 ConfigurationConfigurationClassParser 부분을 집중적으로 확인해봤다.

 

두 개의 버전을 사용하는 모듈을 동시에 실행시켜 놓고 ConfigurationClassParser클래스에 processImports부분을 살펴봤다. 디버깅 코드를 찍고 오류가 시작되는 부분을 찾고 호출스택을 하나씩 역추적해봤다. 

 

의미있는 부분부터 정리하면 processImports 메소드가 호출되고 내부 코드에서 아래 processConfigurationClass를 호출하고 그 내부에서 doProcessConfigurationClass메소드를 다시 호출한다.

 

이 과정을 통해 configuration class들에 대한 메타데이터 정보를 수집하게 되는데 이때 메타데이터를 수집하기 위해서 사용되는 클래스가 SimpleMetadataReader 이다.

 

아래 두 개의 코드를 보면 차례로 2.1.3 버전의 코드와 2.2.7버전의 SimpleMetadataReader를 확인할 수 있다.

SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
   InputStream is = new BufferedInputStream(resource.getInputStream());
   ClassReader classReader;
   try {
      classReader = new ClassReader(is);
   }
   catch (IllegalArgumentException ex) {
      throw new NestedIOException("ASM ClassReader failed to parse class file - " +
            "probably due to a new Java class file version that isn't supported yet: " + resource, ex);
   }
   finally {
      is.close();
   }

   AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
   classReader.accept(visitor, ClassReader.SKIP_DEBUG);

   this.annotationMetadata = visitor;
   // (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)
   this.classMetadata = visitor;
   this.resource = resource;
}
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
   SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
   getClassReader(resource).accept(visitor, PARSING_OPTIONS);
   this.resource = resource;
   this.annotationMetadata = visitor.getMetadata();
}

private static ClassReader getClassReader(Resource resource) throws IOException {
	try (InputStream is = resource.getInputStream()) {
		try {
			return new ClassReader(is);
		}
		catch (IllegalArgumentException ex) {
			throw new NestedIOException("ASM ClassReader failed to parse class file - " +
				"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
		}
	}
}

두 개 생성자를 보면 ClassReader를 가지고 오는 부분이 메서드로 2.2.7버전에서 빠진것을 제외해보면 다른 부분이 MetadataReadingVisitor 클래스가 다르다는 것이다.

 

2.1.3버전에서는 AnnotationMetadataReadingVisitor를 사용하고 2.2.7버전에서는 SimpleAnnotationMetadataReadingVisitor를 사용하는데 AnnotationMetadataReadingVisitor은 spring 5.2버전 부터 deprecated되었다.

 * @deprecated As of Spring Framework 5.2, this class has been replaced by
 * {@link SimpleAnnotationMetadataReadingVisitor} for internal use within the
 * framework, but there is no public replacement for
 * {@code AnnotationMetadataReadingVisitor}.
 */

 

그럼 이 MetadataReadingVisitor클래스가 무엇인지 확인해보자. 이 클래스들에 대한 설명은 아래와 같은데 정리해보면 AnnotaionMetadata 인터페이스를 통해 공개되어 있는 클래스 이름과 구현된 클래스에 정의되어 있는 어노테이션 뿐만 아니라 구현된 타입을 찾기 위한 ASM Class visitor 라고 설명이 되어있다.

* ASM class visitor which looks for the class name and implemented types as
* well as for the annotations defined on the class, exposing them through
* the {@link org.springframework.core.type.AnnotationMetadata} interface.

ASM은 자바 바이트코드를 조작하고 분석하는 프레임워크인데 위에 MetadataReadingVisitor들은 결국 Configuration class import parse를 하는 작업에서 메타 데이터를 가지고 오는 SimpleMetadataReader에서 class들에 대한 정보를 가지고 오기 위한 역할을 하는 클래스들이다.

 

상위 추상클래스 ClassVisitor의 메소드를 살펴보면 visit, visitSource, visitModule, visitOuterClass, visitAnnotation, visitTypeAnnotation, visitAttribute, visitInnerClass, visitEnd등등 클래스에 대한 정보를 가져올 때 사용되는 것이라는걸 확인할 수 있다.

 

이 때 에러가 발생되면서 애플리케이션이 죽냐 안죽냐가 결정이 되는걸로 봐서 이쪽에 처리가 서로 다르게 되어있을 거라고 짐작하고 더 확인해봤다.

 

우선 2.1.3 버전에 경우에는 visitEnd() 메소드를 호출할 때 에러가 발생하게 되는데 AnnotationMetadataReadingVisitor의 visitEnd 메소드는 우선 아래와 같이 되어있다.

@Override
public void visitEnd() {
   super.visitEnd();

   Class<? extends Annotation> annotationClass = this.attributes.annotationType();
   if (annotationClass != null) {
      List<AnnotationAttributes> attributeList = this.attributesMap.get(this.annotationType);
      if (attributeList == null) {
         this.attributesMap.add(this.annotationType, this.attributes);
      }
      else {
         attributeList.add(0, this.attributes);
      }
      if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotationClass.getName())) {
         try {
            Annotation[] metaAnnotations = annotationClass.getAnnotations();
            if (!ObjectUtils.isEmpty(metaAnnotations)) {
               Set<Annotation> visited = new LinkedHashSet<>();
               for (Annotation metaAnnotation : metaAnnotations) {
                  recursivelyCollectMetaAnnotations(visited, metaAnnotation);
               }
               if (!visited.isEmpty()) {
                  Set<String> metaAnnotationTypeNames = new LinkedHashSet<>(visited.size());
                  for (Annotation ann : visited) {
                     metaAnnotationTypeNames.add(ann.annotationType().getName());
                  }
                  this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);
               }
            }
         }
         catch (Throwable ex) {
            if (logger.isDebugEnabled()) {
               logger.debug("Failed to introspect meta-annotations on " + annotationClass + ": " + ex);
            }
         }
      }
   }
}

 

여기서 annotaion 정보를 가지고 올 때 TypeNotPresentExceptionProxy에러가 동일하게 발생한다.

 

하지만 여기서 2.2.7버전과의 차이점은 이 AnnotationMetadataReadingVisitor 경우에는 에러가 발생했을 때 내부적으로 로깅만 하고 상위로 에러를 전파하지 않는다는 점이다. 그래서 결국 에러가 발생하지 않고 로깅만 되고 애플리케이션이 실행되는데 까지는 문제가 없었고 내부적으로 Sns 기능을 쓰지 않기에 문제가 없었던 것이었다.

 

 

 

그럼 이제 2.2.7버전에서는 어떤지 확인해보자. 

2.2.7에서 사용하는 SimpleAnnotationMetadataReadingVisitor의 경우에는 클래스에 정보를 얻기위해 작업하는 visit와 같은 동작들에 대해서 별도의 try catch 작업이 되어 있지 않다.

 

실제로 에러가 발생되는 visitAnnotation메소드를 보면 이 곳에서는 visitAnnotaion을 통해 정보를 가져올 때 내부적으로 MergedAnnotaionReadingVisitor visitor를 사용하지만 별도에 에러에 대한 처리가 되어 있지 않는걸 볼 수 있다. 

@Override
@Nullable
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
	return MergedAnnotationReadingVisitor.get(this.classLoader, this::getSource,
			descriptor, visible, this.annotations::add);
}

 

그로 인해 visitor를 통해 configuration class에 내부정보를 가져오다가 에러가 발생하게 되면 processImport에 에러가 전파되어 BeanDefinitionStoreExeption이 발생하면서 결국 application이 실행이 되지 않는 것이다.

ConfigurationClassParser클래스 내부에 processImports 코드

 

 

 

해결방법


문제가 어찌되었든 우리는 spring boot의 버전을 올려야한다. 그래서 정식으로 수정이 완료된 2.2.3버전을 사용을 하던지 또는 임시로 webmvc 라이브러리를 추가해주는 것이다.

 

 

해결방법 1.

https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-aws-messaging

// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-aws-messaging
compile group: 'org.springframework.cloud', name: 'spring-cloud-aws-messaging', version: '2.2.3.RELEASE'

 

해결방법 2.

compile('org.springframework:spring-webmvc')

 

 

댓글()