[Java] ClassLoader(클래스 로더)
클래스 로더는 런타임중에 JVM(Java Virtual Machine)에서 동적으로 Java 클래스를 로드하는 역할을 합니다.
클래스 로더는 JRE(Java Runtime Environment)의 일부입니다. 따라서 JVM은 클래스 로더 덕분에 Java 프로그램을 실행하기 위해서 파일 시스템이나 파일에 대해 알 필요가 없습니다.
Java의 클래스는 실행 시 한번에 메모리에 로드되는것이 아니라 애플리케이션에서 필요로 할 때 메모리로 로드됩니다. 클래스가 필요한 순간에 메모리로 로드하는 역할을 하는 것이 ClassLoader(클래스 로더) 입니다.
내장 클래스 로더의 종류
자바에는 3가지 클래스 로더가 있습니다.
- Bootstrap 클래스 로더
- Extension 클래스 로더 (자바 9부터 확장 매커니즘이 deprecated 되면서 Extension Class Loader도 없어진것인지 정확하지 않습니다 ㅠㅠ 추가 공부하여 오겠습니다)
- Application(or System) 클래스 로더
Bootstrap 클래스 로더
Java 클래스는 Java.lang.ClassLoader인스턴스에 의해 로드됩니다. 그런데 ClassLoader도 클래스이기 때문에 ClassLoader.class는 누가 로드 해주는지가 문제입니다. 이런 ClassLoader.class 를 로드하는 것이 Bootstrap 클래스 로더(원시 클래스 로더) 입니다.
주로 $JAVA_HOME/jre/lib 디렉토리 에 있는 rt.jar 및 기타 핵심 라이브러리와 같은 JDK 내부 클래스를 로드하는 역할을 합니다.
모든 클래스 로더의 부모이기도 합니다. 그렇기 때문에 Bootstrap 클래스 로더는 자바가 아닌 네이티브 언어로 작성되어 클래스로더가 null로 표시됩니다.
@Test
void test() {
System.out.println(ArrayList.class.getClassLoader());
}
Extension 클래스 로더
부트스트랩 클래스 로더의 자식이고 자바 표준 코어 클래스의 확장 클래스를 로드하는 역할입니다.
주로 $JAVA_HOME/lib/ext 디렉토리 또는 java.ext.dirs 시스템 프로퍼티 내부에 있는 클래스를 로드합니다.
자바 9부터 extension 매커니즘이 deprecated되어 어떻게 됐는지 확인중입니다..
Application(System) 클래스 로더
애플리케이션 또는 시스템 클래스 로더는 모든 애플리케이션 레벨의 클래스를 로드합니다.
우리가 작성한 클래스는 이 Application 클래스 로더에 의해 로드됩니다.
class FileTest {
@Test
void test() {
System.out.println(FileTest.class.getClassLoader());
}
}
클래스 로더의 동작
클래스 로더는 JRE(Java Runtime Environment)의 일부입니다. JVM이 런타임에 클래스를 요청할 때 클래스 로더는 클래스를 찾고 정규화된 이름을 사용해서 로드해옵니다.
ClassLoader.loadClass() 메서드는 런타임에 클래스를 로드할 책임이 있습니다. 정규화된 클래스 이름으로 로드를 합니다.
JVM은 Application ClassLoader(가장 아래에 있는 자식 클래스 로더) 에게 최초의 요청을 보냅니다.
클래스가 아직 로드되지 않았다면 상위(부모) 클래스 로더에 책임을 위임합니다.
Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader
만약 부모 클래스 로더에서도 클래스를 로드하지 못했다면 다시 자식 클래스 로더에게 맡깁니다.(재귀적으로 진행)
자식 클래스 로더는 java.net.URLClassLoader.findClass() 메서드를 호출해서 파일 시스템 자체에서 클래스를 찾습니다.
마지막 자식 클래스 로더도 클래스를 로드하지 못한다면 ClassNotFoundException 또는 NoCladdDefFoundError가 발생합니다.
java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348) // Class.forName 호출
클래스 로더의 3가지 기능
- Delegation Model(위임 모델)
- 클래스 로더는 클래스 또는 리소스를 찾기위해 요청을 받았을 때 ClassLoader 인스턴스가 상위 클래스 로더에게 책임을 위임하는 위임 모델을 따릅니다.
- 단 Bootstrap 클래스 로더도 로드에 실패한 경우에는 시스템 클래스 로더를 사용해서 클래스 자체를 로드하려고 합니다.
- Unique Class(유일 클래스)
- Delegation Model에 의해서 위쪽으로 책임을 위임하려고 하기때문에 고유한 클래스를 보장하기 쉽습니다.
- 상위 클래스 로더가 찾을 수 없는 경우에만 현재 인스턴스가 스스로 찾으려고 합니다.
- Visibility
- 자식 클래스 로더는 부모 클래스 로더에서 로드한 클래스를 볼 수 있습니다.
- Application 클래스 로더가 클래스 A를 로드하고 Extension 클래스 로더가 클래스B를 로드했다면 Application 클래스 로더는 클래스A, 클래스B 모두 볼 수 있습니다. 하지만 Extension 클래스 로더는 클래스 B만 볼 수 있습니다.
시스템 클래스 로더에 의해 로드된 클래스는 확장 및 부트스트랩 클래스 로더에 의해 로드된 클래스에 대한 가시성을 갖지만 반대의 경우는 그렇지 않습니다.
이를 설명하기 위해 클래스 A가 응용 프로그램 클래스 로더에 의해 로드되고 클래스 B가 확장 클래스 로더에 의해 로드되면 응용 프로그램 클래스 로더에 의해 로드된 다른 클래스에 관한 한 A 및 B 클래스가 모두 표시됩니다.
그럼에도 불구하고 클래스 B는 확장 클래스 로더에 의해 로드된 다른 클래스에 관한 한 볼 수 있는 유일한 클래스입니다.
커스텀 클래스 로더
내장 클래스 로더는 파일이 이미 파일 시스템에 있는 경우에 대부분 충분히 사용할 수 있습니다.
그러나 로컬 하드 드라이브나 네트워크에서 클래스를 로드해야 하는 시나리오에서는 사용자 지정 클래스 로더를 사용해야 할 수도 있습니다.
커스텀 클래스를 만드는 법은 ClassLoader를 상속받고 findClass() 메소드를 Override 하면 됩니다.
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
ClassLoader 의 주요 메서드
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
loadClass() 메소드는 name으로 지정된 클래스를 로드합니다. 클래스를 로드하는 거라면 resolve 를 true값으로 줍니다. 하지만 클래스가 존재하는지만 확인하는 것이라면 resolve 값을 false로 주면 됩니다.
이 메소드는 클래스 로드의 진입점 역할을 합니다.
protected final Class<?> defineClass( String name, byte[] b, int off, int len) throws ClassFormatError
defineClass() 메소드는 바이트 배열을 클래스의 인스턴스로 변환하는 역할을 합니다. 데이터에 유효한 클래스가 없는 경우 ClassFormatError가 발생합니다.
protected Class<?> findClass(String name) throws ClassNotFoundException
findClass() 메소드는 정규화된 이름을 매개 변수로 사용하여 클래스를 찾습니다. 클래스를 로드하기 위한 delegation Model(위임 모델)을 따르는 커스텀 클래스 로더 구현에서 이 메서드를 재정의해야 합니다.
또한 loadClass() 는 부모 클래스 로더가 요청된 클래스를 찾을 수 없는 경우 이 메서드를 호출합니다.
클래스 로더의 부모가 클래스를 찾지 못하면 기본적인 코드에서는 ClassNotFoundException이 발생합니다.
public final ClassLoader getParent()
이 메서드는 위임을 위해 부모 클래스 로더를 반환합니다.
부트스트랩 클래스 로더는 null로 나타냅니다.
public URL getResource(String name)
이 메소드는 주어진 이름을 가진 자원을 찾으려고 시도합니다.
먼저 리소스의 상위 클래스 로더에 위임합니다. 부모가 null 이면 가상 머신에 빌드된 클래스 로더의 경로를 검색합니다.
실패하면 메서드는 findResource(String) 를 호출 하여 리소스를 찾습니다. 입력으로 지정된 리소스 이름은 클래스 경로에 대해 상대적이거나 절대적일 수 있습니다.
리소스를 읽기 위한 URL 개체를 반환하거나 리소스를 찾을 수 없거나 호출자가 리소스를 반환할 수 있는 적절한 권한이 없는 경우 null을 반환합니다.
Java는 클래스 경로에서 리소스를 로드한다는 점에 유의하는 것이 중요합니다.
마지막으로 Java에서 리소스 로드 는 환경이 리소스를 찾도록 설정되어 있는 한 코드가 실행되는 위치가 중요하지 않기 때문에 위치가 독립적인 것으로 간주됩니다. 즉 Java 환경이 리소스를 찾도록 설정되어 있다면 어느 위치에서 실행하든 위치와 관계없이 실행됩니다.
반환되는 값은 리소스를 읽기 위한 URL 객체 입니다.
참고자료