NoSuchMethodException & NoSuchMethodError 해결하기

애플리케이션에서 사용하는 라이브러리의 버전을 업데이트하거나 혹은 새로운 라이브러리를 추가하는 과정에서 종종 NoSuchMethodError을 마주하게 된다.
이번에는 NoSuchMethodErrors 대한 내용과 해결하는 방법에 대해서 알아보자.

NoSuchMethodErrors

NoSuchMethodErrors는 런타임(runtime) 시점에 존재하지 않는 메서드(method)를 호출하는 경우에 발생한다.
일반적으로 존재하지 않는 메서드를 사용하려는 경우 컴파일(compile) 시점에 컴파일러가 에러를 발생시킨다. 그런데 런타임 시점에 에러가 발생했다는 말은 즉, 컴파일때는 해당 메서드가 존재했지만 런타임 시점에 찾을 수 없었다는 것을 의미한다.

원인

NoSuchMethodErrors가 발생하는 일반적인 상황에 대해서 좀 더 알아보자.

라이브러리 Breaking Change

NoSuchMethodErrors가 발생하는 원인 중 하나는 애플리케이션에서 사용하는 라이브러리 중 하나가 한 버전에서 다음 버전으로 변경될 때, Breaking Change를 포함하는 경우이다.
Breaking Change란 라이브러리가 제공하던 API에 변경 사항이 있음을 의미한다. API가 사라진다던가 return type 혹은 parameters가 변경되는 경우들이 있다.

그러나, 위에서도 이야기한 것처럼 일반적으로 존재하지 않는 메서드를 사용하려는 경우 컴파일 시점에 컴파일러가 에러를 발생시키는데 어떻게 런타임 시점에 에러가 발생할 수 있는 것일까?

최근에는 SpringBoot로 애플리케이션을 개발하고 Tomcat 자체를 포함해 fatjar(uberjar)로 패키징(packaging) 후 배포해서 jar 파일을 실행하는 것으로 애플리케이션을 실행하는 것이 자연스럽게 여겨진다.

Embeded Tomcat을 사용하지 않았던 때를 생각해보자. Spring 애플리케이션을 개발하고 war로 패키징한 후, Tomcat이 설치된 폴더 아래의 webapps에 war를 배포 후 Tomcat을 재기동 하는 것으로 애플리케이션을 실행했었다.

이것이 의미하는 것이 무엇일까? Servlet Container인 Tomcat과 함게 애플리케이션이 실행되고 있는 런타임 시점에 HTTP 요청이 들어오면 Tomcat은 해당 요청에 대한 HttpServletRequest와 HttpServletResponse를 만들고 우리가 작성한 Spring 애플리케이션 코드를 실행한다.
우리는 애플리케이션 코드에서 Tomcat으로부터 전달받은 HttpServletRequest와 HttpServletResponse를 사용하는데, 개발하는 시점에 Spring 애플리케이션에서는 HttpServletRequest와 HttpServletResponse를 어떻게 사용할 수 있는 것일까?

Gradle을 사용한다면 build.gradle을, Maven을 사용하고 있다면 pom.xml을 확인해보자. 다음과 같이 HttpServletRequest와 HttpServletResponse를 포함하고 있는 javax.servlet-api 라이브러리를 포함하고 있을 것이다.

1
2
// ex) build.gradle
compileOnly("javax.servlet:javax.servlet-api:3.0.1")

다만 implementation 혹은 api가 아닌 compileOnly를 사용해서 의존성을 선언하고 있다. 이것이 의미하는 것은 애플리케이션을 작성하고 컴파일 시점까지만
3.0.1 버전의 javax.servlet-api를 사용하겠다는 것이다. 그 후 컴파일이 완료되고 Tomcat에 의해서 애플리케이션이 실행되는 런타임 시점에는 Tomcat에 의해 classpath에 포함된 javax.servlet-api를 사용하게 된다.

Tomcat은 버전에 따라 사용하는 Servlet 버전이 다르다. 따라서 Spring 애플리케이션에서 compileOnly 의존성으로 사용하고 있는 Servlet 라이브러리의 버전과 실제 애플리케이션이 구동되는 Tomcat에서 사용하는 버전에 차이가 있다면 생각과는 다르게 동작할 수 있는 가능성이 있는 것이다.

Apache Tomcat Versions
tomcat_version

만약 Spring 애플리케이션의 Servlet 버전을 3.0 버전에 맞춰 개발하고 있었는데 애플리케이션이 실행되는 Tomcat이 6.0.x(Servlet 버전 2.5) 혹은 8.0.x(Servlet 버전 3.1)이라면 의도치 않은 Breaking Change를 겪게될 수 있는 것이다.

라이브러리 버전 Overriding

다른 원인으로는 라이브러리 버전 Overriding 문제가 있다. 다음을 가정해보자.

애플리케이션에서 라이브러리 A를 사용하고 있다. 라이브러리 A는 다른 라이브러리 B에 대해 의존성(dependency)를 갖고 있다. 애플리케이션에서 라이브러리 B를 직접 호출하지는 않지만 라이브러리 B를 사용하기 때문에 A까지 포함되게 되는데 이때, 라이브러리 B는 해당 애플리케이션에 대해 전이 의존성(transitive dependency)이라고 부른다.

이 경우, 만약 라이브러리 B를 이용하는 또 다른 라이브러리 C가 있고 이를 애플리케이션에서 사용하고 있다면 build system에 의해 버전 충돌로 인한 문제가 발생할 수 있다.

  • 애플리케이션 <- 라이브러리 A <- 라이브러리 B (버전 1.x)
  • 애플리케이션 <- 라이브러리 C <- 라이브러리 B (버전 2.x)

Gradle과 Maven 같은 빌드 시스템은 이와 같은 버전 충돌 문제가 발생했을 때, 두 버전 중 하나를 선택하게 된다.

위의 예시에서 라이브러리 B의 2.x 버전을 선택하게 되었을 때, 라이브러리 C에서는 문제가 발생하지 않지만 라이브러리 A에서는 문제가 발생할 수 있다. (라이브러리 B가 버전 1.x와 2.x 사이에 Breaking Change가 존재할 수 있기 때문이다.)

해결책

크게 3단계로 해결할 수 있다.

문제의 Class 찾기

먼저 문제의 메서드를 포함하는 클래스를 찾아야한다. NoSuchMethodError 로그로부터 쉽게 찾을 수 있다.

1
2
Exception in thread "main" java.lang.NoSuchMethodError: 
com.jongmin.example.Service.hello(Ljava/lang/String;)Ljava/lang/String;

클래스를 찾았다면 검색 혹은 사용하고 있는 IDE를 통해서 해당 클래스가 포함된 JAR(라이브러리)를 찾을 수 있다.

Class 호출하는 곳 찾기

다음으로 메서드를 호출하는 위치를 찾아야 한다. 이 정보는 에러 로그의 stack trace를 이용해 찾을 수 있다.

1
2
3
Exception in thread "main" java.lang.NoSuchMethodError: 
com.jongmin.example.Service.hello(Ljava/lang/String;)Ljava/lang/String;
at com.jongmin.example.NoSuchMethod.main(ProvokeNoSuchMethodError.java:7)

위의 로그에서는 NoSuchMethod 클래스가 런타임에 존재하지 않는 hello 메서드를 호출하려고 하는 것을 확인할 수 있다. 이제 해당 클래스가 속한 라이브러리를 찾아야한다.

버전 확인

이제 NoSuchMethodError가 발생하는 위치와 런타임 시점에 존재하지 않는 메서드를 알았으므로 해결할 수 있다.

현재 프로젝트의 모든 dependency를 출력해보자.

Gradle을 사용하고 있다면 다음의 명령어를 사용한다.

1
./gradlew dependencies > dependencies.txt

Maven을 사용한다면 다음의 명령어를 사용한다.

1
mvn dependency:list > dependencies.txt

dependencies.txt 파일을 확인하면 런타임 시점에 존재하지 않는 메서드와 이를 호출하는 클래스가 포함 된 라이브러리를 찾을 수 있다.

예를 들면, 다음과 같은 출력 결과를 확인할 수 있다.

1
2
\--- org.springframework.data:spring-data-commons:1.8.2.RELEASE
| \--- org.springframework:spring-core:3.2.10.RELEASE -> 4.3.20.RELEASE (*)

위의 결과를 보면 spring-data-commons 라이브러리가 3.2.10 버전의 spring-core에 의존하지만 다른 라이브러리도 버전 4.3.20의 spring-core에 의존하고 있어 3.2.10 버전이 4.3.2 버전으로 Overriding된 것이다.

그럼 이제 어떤 라이브러리가 spring-core 4.3.2 버전 라이브러리에 의존하고 있는지 찾을 수 있다.

마지막으로 spring-core 라이브러리의 두 버전 중 어떤 버전이 spring-core를 필요로하는 두 라이브러리의 종속성을 만족시키는지 확인하고 결정해야 한다. 일반적이라면 안정적인 라이브러리라면 업데이트 되더라도 하위 호환성(backward compatibility)를 유지하기 때문에 최신 버전을 사용하면 될 것이다.

NoSuchMethodException

NoSuchMethodException은 NoSuchMethodError와 관련이 있지만 다른 context에서 발생한다. NoSuchMethodError는 JAR 파일이 컴파일 시점과 런타임 시점에 다른 버전을 갖는 경우 발생한다. 반면 NoSuchMethodException은 reflection을 이용해 존재하지 않는 메서드를 사용하려고하면 발생한다.

예를 들면, 다음과 같은 코드이다.

1
String.class.getMethod("notExist");

NoSuchMethodException & NoSuchMethodError 해결하기

https://jongmin92.github.io/2019/12/05/Java/nosuchmethod/

Author

KimJongMin

Posted on

2019-12-05

Updated on

2021-03-22

Licensed under

댓글