Library

Java "ClassLoader"

Andrew's Akashic Records 2023. 7. 6. 10:46
728x90

Java 클래스 로더(Class Loader)는 JVM(Java Virtual Machine)에서 클래스 파일들을 로드하는데 사용되는 일종의 추상 클래스입니다. 이는 런타임에 동적으로 자바 클래스를 로드하거나 네트워크에서 다른 소스로부터 클래스를 로드하는 역할을 합니다.

클래스 로더의 주요 기능은 다음과 같습니다:

  1. 로딩(Loading): 이 단계에서 클래스 로더는 클래스나 인터페이스의 바이너리 데이터를 메모리에 로드합니다. 클래스 로더는 이 프로세스를 통해 완전한 바이너리 트리를 생성하고, 이를 JVM 메모리에 로드합니다.

  2. 연결(Linking): 로딩이 완료되면 연결 단계가 시작됩니다. 이 단계는 검증, 준비, 그리고 (선택적으로) 해석의 세부 단계로 구분될 수 있습니다.
    • 검증(Verification): 이 단계에서는 바이트 코드가 적절하고 안전한지를 확인합니다. 즉, 클래스 파일이 JVM 규격에 따라 형식적으로 올바른지를 검사합니다.
    • 준비(Preparation): 이 단계에서 JVM은 클래스 또는 인터페이스에 필요한 메모리를 할당합니다. 이는 주로 정적 변수에 대한 메모리를 할당하고 해당 변수에 기본 값을 할당하는 과정을 포함합니다.
    • 해석(Resolution): 이 단계는 선택적인 단계로, 심볼릭 메모리 참조를 직접 메모리 참조로 변환합니다.
  3. 초기화(Initialization): 이 단계에서는 클래스 변수를 올바른 시작 값으로 설정하는 작업이 수행됩니다. 이 초기화는 로딩된 클래스의 코드에서 정적 초기화 블록이 실행됨으로써 이루어집니다.

JVM에는 세 가지 기본 클래스 로더가 내장되어 있습니다:

  1. 부트스트랩 클래스 로더(Bootstrap Class Loader): 이 로더는 자바의 핵심 클래스를 로드합니다. 예를 들어 java.lang.* 패키지의 클래스를 로드합니다. 이 로더는 JVM에 내장되어 있으며, JVM이 시작될 때 함께 시작됩니다.

  2. 확장 클래스 로더(Extension Class Loader): 이 로더는 확장 디렉터리($JAVA_HOME/lib/ext 또는 시스템 변수 java.ext.dirs가 가리키는 디렉터리)에 있는 클래스를 로드합니다.

  3. 시스템 클래스 로더(System Class Loader): 이 로더는 시스템 클래스 경로(CLASSPATH 환경 변수 또는 -classpath/-cp 옵션으로 지정된 경로)에 있는 클래스를 로드합니다.

자바에서는 사용자 정의 클래스 로더를 작성하여 클래스를 동적으로 로드하는 데 사용할 수 있습니다. 사용자 정의 클래스 로더를 사용하면 더욱 복잡한 로딩 요구사항을 처리하거나 클래스를 암호화하는 등의 보안 요구사항을 충족시킬 수 있습니다.

 

자바에서 클래스를 로드할 때 사용하는 클래스 로더는 계층적이며, 각 로더는 특정 위치에서 클래스를 찾으려고 시도합니다. 이것이 "클래스를 찾는 순서" 또는 "클래스 로딩 순서"라고 불리는 것입니다.


클래스 로딩의 과정은 다음과 같습니다:

  1. 부트스트랩 클래스 로더(Bootstrap Class Loader): 첫 번째로, JVM의 부트스트랩 클래스 로더는 자바의 핵심 클래스를 로드하려고 시도합니다. 이 로더는 JVM의 일부로서, 주로 $JAVA_HOME/jre/lib 디렉토리에 있는 클래스를 로드합니다. 만약 요청된 클래스가 이 위치에서 발견되지 않으면 다음 단계로 넘어갑니다.

  2. 확장 클래스 로더(Extension Class Loader): 부트스트랩 클래스 로더가 요청된 클래스를 찾지 못하면, 확장 클래스 로더가 확장 디렉토리($JAVA_HOME/lib/ext 또는 시스템 변수 java.ext.dirs가 가리키는 디렉터리)에서 해당 클래스를 찾으려고 시도합니다. 만약 요청된 클래스가 이 위치에서 발견되지 않으면 다음 단계로 넘어갑니다.

  3. 애플리케이션 클래스 로더(Application Class Loader): 확장 클래스 로더도 요청된 클래스를 찾지 못하면, 애플리케이션 클래스 로더(또는 시스템 클래스 로더라고도 함)가 시스템 클래스 경로(CLASSPATH 환경 변수 또는 -classpath/-cp 옵션으로 지정된 경로)에서 해당 클래스를 찾으려고 시도합니다. 만약 요청된 클래스가 이 위치에서 발견되지 않으면 ClassNotFoundException이 발생합니다.

참고로, 이렇게 JVM 내부의 클래스 로더들이 작동하는 방식을 '부모 우선(parent-first)' 전략이라고도 합니다. 이 전략은 자식 클래스 로더가 클래스를 로드하기 전에 부모 클래스 로더에게 먼저 기회를 준다는 것을 의미합니다. 이러한 방식으로, JVM은 중복 로딩을 방지하고 시스템의 안정성을 유지합니다. 그러나 특수한 경우에는 사용자 정의 클래스 로더를 만들어 '자식 우선(child-first)' 전략을 사용할 수도 있습니다.

 

자바에서는 사용자 정의 클래스 로더를 만들어 특정 요구 사항을 충족시킬 수 있습니다. 사용자 정의 클래스 로더를 만들려면 java.lang.ClassLoader 클래스를 상속받아야 합니다. findClass(String name) 메소드를 오버라이드하고, 이 메소드 내에서 클래스를 로드하는 로직을 구현하면 됩니다.

 

아래에 간단한 사용자 정의 클래스 로더의 예를 들어보겠습니다:

import java.io.*;

public class CustomClassLoader extends ClassLoader {

    @Override
    protected 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;
    }
}

위의 CustomClassLoaderfindClass() 메소드를 오버라이드하여 클래스 파일에서 직접 바이트코드를 로드하는 기능을 구현하고 있습니다. 클래스 이름으로 파일을 찾아서 해당 파일의 내용을 바이트 배열로 읽어들입니다. 그 후, defineClass() 메소드를 호출하여 바이트 배열을 사용하여 Class 객체를 만듭니다.

 

이 클래스 로더를 사용하여 클래스를 로드하는 방법은 다음과 같습니다:

CustomClassLoader customClassLoader = new CustomClassLoader();
try {
    Class<?> c = customClassLoader.loadClass("MyClass");
    //...
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

위의 예에서 "MyClass"는 로드하려는 클래스의 이름입니다.

참고: 이 사용자 정의 클래스 로더는 매우 기본적인 예입니다. 실제로 사용자 정의 클래스 로더를 작성하려면 보안, 효율성, 클래스 파일 위치 등에 대해 고려해야 할 수 있습니다.

 

다이나믹 클래스 로딩은 런타임에 클래스를 동적으로 로드하는 기능을 의미합니다. 자바에서는 java.lang.ClassLoader 클래스를 확장하여 다이나믹 클래스 로더를 만들 수 있습니다.

 

간단한 다이나믹 클래스 로더는 다음과 같이 만들 수 있습니다:

import java.io.*;

public class DynamicClassLoader extends ClassLoader {
    private String classPath;

    public DynamicClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] loadClassData(String className) {
        String path = classPath + "/" + className.replace('.', '/') + ".class";
        try {
            InputStream input = new FileInputStream(path);
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            int data = input.read();
            while (data != -1) {
                output.write(data);
                data = input.read();
            }
            input.close();
            return output.toByteArray();
        } catch (IOException e) {
            return null;
        }
    }
}

위의 DynamicClassLoader는 생성자에 클래스 파일들이 있는 디렉토리를 받아서, 요청받은 클래스 이름으로 해당 디렉토리에서 클래스 파일을 찾아서 로드하는 기능을 수행합니다.

 

이 클래스 로더를 사용하여 클래스를 동적으로 로드하는 방법은 다음과 같습니다:

DynamicClassLoader dynamicClassLoader = new DynamicClassLoader("/path/to/classes");
try {
    Class<?> c = dynamicClassLoader.loadClass("MyClass");
    //...
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

위의 예에서 "MyClass"는 로드하려는 클래스의 이름이고, "/path/to/classes"는 클래스 파일들이 있는 디렉토리의 경로입니다.

참고: 이 다이나믹 클래스 로더는 매우 기본적인 예입니다. 실제로 다이나믹 클래스 로더를 작성하려면 보안, 효율성, 클래스 파일 위치 등에 대해 고려해야 할 수 있습니다. 또한, 에러 처리도 좀 더 구체적으로 해야할 것입니다. 예를 들어, loadClassData() 메소드에서 클래스 파일 읽기에 실패하면 null을 반환하도록 되어 있는데, 이 부분을 구체적인 예외 처리로 변경할 수 있을 것입니다.

 

java.lang.ClassLoader 클래스는 클래스를 로드하는데 사용되는 기본 메소드들을 제공합니다. 클래스 로더의 주요 메소드는 다음과 같습니다:

 

  1. loadClass(String name): 주어진 이름의 클래스를 로드합니다. 이 메소드는 일반적으로 ClassLoader에 의해 오버라이드되지 않으며, 메소드 로직에 따라 적절한 클래스 로딩 순서와 캐싱을 처리합니다.

  2. findClass(String name): 주어진 이름의 클래스를 찾습니다. 이 메소드는 loadClass() 메소드에 의해 호출되며, 일반적으로 클래스 로더를 구현할 때 오버라이드해야 합니다.

  3. defineClass(String name, byte[] b, int off, int len): 바이트 배열로부터 클래스를 정의합니다. 이 메소드는 보통 findClass() 내부에서 바이트코드를 로드할 때 사용됩니다.

  4. findLoadedClass(String name): 이미 로드된 클래스를 찾습니다. 이 메소드는 loadClass() 메소드에 의해 호출되며, 메소드의 호출을 피하기 위해 캐싱된 클래스를 반환합니다.

  5. getParent(): 이 클래스 로더의 부모 클래스 로더를 반환합니다. 모든 클래스 로더는 부모 클래스 로더를 가지며 (최상위 부트스트랩 클래스 로더 제외), 이 메소드를 통해 부모를 참조할 수 있습니다.

  6. getResource(String name): 지정된 리소스를 찾습니다. 이 메소드는 클래스 경로에 있는 파일을 URL로 반환합니다.

  7. getResources(String name): 지정된 리소스의 모든 인스턴스를 찾습니다. 이 메소드는 클래스 경로에 있는 모든 파일의 URL을 반환합니다.

  8. getResourceAsStream(String name): 지정된 리소스를 InputStream으로 반환합니다. 이 메소드는 특히 클래스 경로에 있는 파일을 스트림으로 읽을 때 유용합니다.

이들은 ClassLoader 클래스의 주요 메소드들입니다. 그러나 더 많은 메소드와 기능이 있으며, 사용자 정의 클래스 로더를 만드는 데 사용할 수 있습니다.

 

Java의 기본 클래스 로더 전략은 '부모 우선(Parent-First)' 입니다.

이 전략은 자식 클래스 로더가 클래스를 로드하려고 할 때 먼저 부모 클래스 로더에게 로드를 시도할 기회를 줍니다. 만약 부모 클래스 로더가 요청된 클래스를 로드할 수 있으면, 그 클래스를 반환하고, 그렇지 않으면 자식 클래스 로더가 클래스를 로드하려고 시도합니다.

 

부모 우선 전략은 중복 로딩을 방지하고, 클래스들 사이의 호환성을 유지하는데 도움이 됩니다.

'자식 우선(Child-First)' 또는 '자식 우선(Child-First)' 전략은 일반적으로 사용자 정의 클래스 로더에서 사용되며, 이 전략을 사용하려면 ClassLoader를 상속받아 적절히 오버라이드해야 합니다. 이 전략에서는 자식 클래스 로더가 먼저 클래스를 로드하려고 시도하고, 실패하면 부모 클래스 로더에게 로드를 요청합니다. 이 전략은 특정 애플리케이션에서 클래스의 다른 버전을 로드해야 할 때 유용할 수 있습니다.

728x90