모던 자바에서 URLClassLoader 사용하기


필자가 회사에서 개발 중인 스프링 기반의 뷰 서버는 기본 기능으로 넣을 수 없는 마이너한 기능들을 외부에서 구현하여 JAR 파일로 배포한다. 해당 JAR 파일들은 뷰 서버가 시작될 때, 로드하기 위해서 URLClassLoader 클래스를 사용했다. JAR 파일을 로드하고, 뷰 서버에서 동작하도록 몇가지 일들을 해야하는데, 필자가 기대한 URLClassLoader 클래스의 기능은 다음과 같다.

  1. 다수의 JAR 파일을 동적으로 추가하기
  2. 선택한 JAR 파일 내의 리소스 가져오기
  3. 선택한 JAR 파일에 구현된 클래스의 특정 메소드 실행하기

참고로 뷰 서버 개발 초기에는 자바 6, 7 버전만 지원했었다. 하지만 최근에는 자바 8 버전 이상을 지원하기로 정책을 변경했는데, 어느날 갑자기 자바 9 버전에서 뷰 서버가 동작하지 않는다는 이슈가 올라왔다.

그래서 원인을 찾아보니 자바 9 버전부터 클래스 로딩 전략이 바뀌어서 JAR 파일을 로드하기 위해 만들었던 코드는 못쓰게 되었다. 기존에는 다음과 같이 현재 스레드의 컨텍스트 로더를 바로 URLClassLoader 클래스로 캐스팅해서 사용할 수 있었는데, 자바 9 버전부터는 예외가 발생한다.

Exception in thread "main" java.lang.ClassCastException:
java.base/jdk.internal.loader.ClassLoaders$AppClassLoader
cannot be cast to java.base/java.net.URLClassLoader
at monitor.Main.logClassPathContent(Main.java:46)
at monitor.Main.main(Main.java:28)

기존에는 URLClassLoader 인스턴스를 바로 가져와서 사용했지만 자바 리플렉션을 통해 내부에서만 참조할 수 있는 인스턴스나 메소드 등을 사용해야만 했기 때문에 구현이 깔끔하지 못했다. (본문에서는 다루지 않을 내용이니 일단 패스하겠다.)

자바 9 버전부터는 새로운 클래스 로더를 생성하여, 기존의 컨텍스트 클래스 로더에 업데이트 해주는 형태로 사용하길 권장한다.

근데 조금 문제가 있다. 만약에 a.jar와 b.jar 파일에 동일한 경로의 리소스가 똑같이 존재한다면 나중에 추가된 b.jar 파일의 리소스를 먼저 찾게 되므로 a.jar 파일의 리소스를 가져올 방법이 없게 된다.

그래서 필자가 생각한 방법은 JAR 파일을 추가할 때, 리소스를 찾기 위한 URLClassLoarder 인스턴스를 미리 생성해두어 JAR 파일의 경로를 키로 가진 맵에 추가하는 것이었다.

즉, 컨텍스트 클래스 로더에는 추가한 JAR 파일 리스트가 하나의 URLClassLoader 인스턴스로 업데이트 되고, 특정 JAR 파일의 메소드를 실행하거나 리소스를 찾을 때에는 맵에서 가져와 목적에 맞게 사용하면 된다.


다음은 자바 7 버전으로 컴파일한 caculator.jar, helloworld.jar 파일을 로드해서 특정 텍스트 파일을 내용을 확인하고, 메소드 실행 결과 값을 비교해보는 테스트 코드이다.


다음은 필자가 구현한 ModernURLClassLoader 클래스의 전체 코드이다. 자바 7, 8, 9, 10 버전에서 잘 동작하는 것을 확인했다.

프로젝트 다운로드

본문에서 다룬 내용은 모두 GitHub 저장소에 올려놨으니 바로 테스트 해볼 수 있다.

https://github.com/seogi1004/modern-urlclassloader