NoSuchMethodError

내 이 세상 도처에서 쉴 곳을 찾아보았으나, 마침내 찾아낸, 컴퓨터가 있는 구석방보다 나은 곳은 없더라.

NoSuchMethodError

CI서버에서 대략 삼분의 일 정도의 테스트가 실패하고 있었다. 로그를 확인해보니 어이없게도 실패하는 테스트에서 NoSuchMethodError가 발생했고, 모두 Google의 컬렉션 라이브러리를 사용하는 부분이었다. 이클립스에서 테스트를 실행시킬 때는 아무런 문제가 없는데 CI 서버에서는 실패하는 것이었다.

에러 이름으로 보자면 메서드를 찾지 못해 생긴 문제다. 컴파일할 때와 실행할 때 클래스 경로에 차이가 있다면 이런 문제가 발생할 수 있다. 문제가 생기는 라이브러리는 guava-r09.jar 파일에 있는 것이었고 컴파일 할 때나 실행할 때나 같은 파일을 클래스 경로에 추가했다. 이런 문제가 생기는 이유를 알 수 없었다.

조금 더 확인을 해보니, 내 PC에서도 Ant 태스크로 JUnit 테스트를 돌릴 때 동일한 문제가 발생한다는 것을 알게 되었다. 이클립스에서 그냥 돌릴 때와 Ant로 돌릴 때의 차이점이 무엇일까? 아무리 생각해도 알 수 없었다. 혹시 guava 라이브러리가 클래스 경로에 중복해 존재하는지 확인하기 위해 클래스 경로의 모든 jar 파일을 확인하기도 했다. 분명 클래스 경로에 중복된 클래스가 있고, 클래스 로더가 내가 원하는 클래스가 아닌 다른 클래스를 로딩해 발생한 문제인 것은 알겠는데, 그 이상 나아갈 수가 없었다.

이렇게 어제 하루를 날렸다. 오늘 아침에 다시 구글로 검색을 하다가 귀중한 정보를 찾았다. JVM 파라미터 중에 로딩되는 클래스 정보를 표시하도록 하는 옵션이 있다는 것이다.

java -verbose:class <other args=>

옵션을 추가해 테스트를 실행해보니 문제를 바로 확인할 수 있었다. com.google.common.collect.Maps 클래스를 내가 의도한 guava-r09.jar가 아닌 checkstyle-5.3-all.jar에서 로딩하고 있는 것이었다. 허거덕! 결국 이클립스에서 테스트를 실행시킬 때와 Ant로 돌릴 때와의 차이점은, Ant로 돌릴 때 정적 분석을 위해 클래스 경로에 추가한 몇몇 jar 파일이었다. Checkstyle의 jar 파일에 Google 컬렉션 라이브러리 클래스가 함께 들어있어 이 때문에 문제가 생기리라고는 상상도 하지 못했다.

물론 이건 설정을 잘못한 문제다. 테스트를 실행할 때 checkstyle-5.3-all.jar가 클래스 경로에 있어야 할 이유는 전혀 없다. checkstyle-5.3-all.jar는 Checkstyle을 돌릴 때만 필요하다. 빌드 스크립트를 만들 때 클래스 경로를 구분하기 귀찮아 그냥 하나의 변수에 몰아넣고 아무데서나 이 변수를 참조해 쓴 게 잘못이다.

구글에서 NoSuchMethodError로 검색했을 때 상위에 나오는 국내 블로그 글들은 문제에 대한 설명은 있지만 해결하는 방법에 대한 설명은 부족해 보인다. 그냥 중복된 jar 파일이 있는지 확인해 지우니 잘 되더라 하는 정도다.

정확한 문제 해결 방법을 정리하자면 다음과 같은 정도가 될 것 같다:

java를 실행시킬 때 -verbose:class 옵션을 주어 에러가 발생하는 클래스가 원하는 jar 파일(또는 클래스 경로)에서 로딩되었는지를 확인한다.