Java ClassLoader는 프로젝트 개발에서 중요하지만 드물게 사용되는 구성 요소 중 하나입니다. 저는 아직까지 제 프로젝트에서 ClassLoader를 확장한 적이 없습니다. 하지만 자바 클래스 로딩을 사용자 정의할 수 있는 자체 ClassLoader를 가지는 아이디어는 흥미로운 것 같습니다. 이 문서에서는 Java ClassLoader의 개요를 제공한 다음 Java에서 사용자 정의 클래스 로더를 만드는 방법으로 넘어갈 것입니다.
Java ClassLoader란 무엇인가요?
우리는 자바 프로그램이 Java 가상 머신(JVM)에서 실행된다는 것을 알고 있습니다. 자바 클래스를 컴파일하면 JVM은 플랫폼과 기계에 독립적인 바이트코드를 생성합니다. 이 바이트코드는 .class 파일에 저장됩니다. 클래스를 사용하려고 할 때 ClassLoader가 메모리에 로드합니다.
내장 ClassLoader 유형
자바에는 세 가지 유형의 내장 ClassLoader가 있습니다.
- Bootstrap Class Loader – JDK 내부 클래스를 로드합니다. rt.jar 및 다른 핵심 클래스, 예를 들어 java.lang.* 패키지 클래스 등을 로드합니다.
- Extensions Class Loader – JDK 확장 디렉토리에서 클래스를 로드합니다. 일반적으로 $JAVA_HOME/lib/ext 디렉토리입니다.
- 시스템 클래스 로더 – 이 클래스 로더는 현재 클래스 패스에서 클래스를 로드합니다. -cp 또는 -classpath 명령 줄 옵션을 사용하여 프로그램을 호출하는 동안 클래스 패스를 설정할 수 있습니다.
클래스 로더 계층 구조
클래스 로더는 클래스를 메모리에 로드하는 계층 구조입니다. 클래스를 로드하는 요청이 발생하면 부모 클래스 로더에게 위임합니다. 이렇게 하면 실행 환경에서 고유성이 유지됩니다. 부모 클래스 로더에서 클래스를 찾지 못하면 클래스 로더 자체가 클래스를 로드하려고 시도합니다. 아래의 자바 프로그램을 실행하여 이를 이해해 봅시다.
package com.journaldev.classloader;
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println("class loader for HashMap: "
+ java.util.HashMap.class.getClassLoader());
System.out.println("class loader for DNSNameService: "
+ sun.net.spi.nameservice.dns.DNSNameService.class
.getClassLoader());
System.out.println("class loader for this class: "
+ ClassLoaderTest.class.getClassLoader());
System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());
}
}
출력:
class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37
Java 클래스 로더의 작동 방식은?
위 프로그램 출력에서 클래스 로더의 작동 방식을 이해해 봅시다.
- java.util.HashMap의 클래스 로더는 null로 나타나며, 이는 Bootstrap 클래스 로더를 반영합니다. DNSNameService 클래스의 클래스 로더는 ExtClassLoader입니다. 클래스 자체가 CLASSPATH에 있으므로 시스템 클래스 로더가 이를 로드합니다.
- HashMap을 로드하려고 할 때, 시스템 ClassLoader는 확장 ClassLoader에게 위임합니다. 확장 ClassLoader는 이를 부트스트랩 ClassLoader에게 위임합니다. 부트스트랩 ClassLoader는 HashMap 클래스를 찾아 JVM 메모리에 로드합니다.
- DNSNameService 클래스에도 동일한 과정이 적용됩니다. 그러나 부트스트랩 ClassLoader는 $JAVA_HOME/lib/ext/dnsns.jar에 위치한 클래스를 찾지 못합니다. 그래서 확장 ClassLoader에 의해 로드됩니다.
- Blob 클래스는 MySql JDBC 커넥터 jar(mysql-connector-java-5.0.7-bin.jar)에 포함되어 있으며 프로젝트의 빌드 경로에 있습니다. 이 역시 시스템 ClassLoader에 의해 로드됩니다.
- 자식 ClassLoader에 의해 로드된 클래스는 부모 ClassLoader에 의해 로드된 클래스에 대한 가시성을 가집니다. 따라서 시스템 ClassLoader에 의해 로드된 클래스는 확장 및 부트스트랩 ClassLoader에 의해 로드된 클래스에 대한 가시성을 가집니다.
- 만약 동등한 ClassLoader가 있다면, 서로에 의해 로드된 클래스에 접근할 수 없습니다.
자바에서 왜 커스텀 ClassLoader를 작성해야 하나요?
Java 기본 ClassLoader는 대부분의 경우에 대해 로컬 파일 시스템에서 클래스를 로드할 수 있습니다. 그러나 런타임에서 클래스를 예상하거나 FTP 서버나 제3자 웹 서비스를 통해 클래스를 로드해야 하는 경우 기존 클래스 로더를 확장해야 합니다. 예를 들어 AppletViewer는 원격 웹 서버에서 클래스를 로드합니다.
Java ClassLoader 메소드
- JVM이 클래스를 요청하면 ClassLoader의
loadClass()
함수를 호출하여 클래스의 완전히 분류된 이름을 전달합니다. - loadClass() 함수는 클래스가 이미 로드되었는지 확인하기 위해
findLoadedClass()
메소드를 호출합니다. 동일한 클래스를 여러 번 로드하지 않도록 해야 합니다. - 클래스가 아직 로드되지 않은 경우, 요청을 부모 ClassLoader에게 위임하여 클래스를 로드합니다.
- 부모 ClassLoader가 클래스를 찾지 못하면 findClass() 메소드를 호출하여 파일 시스템에서 클래스를 찾습니다.
Java 사용자 정의 ClassLoader 예제
우리는 ClassLoader 클래스를 확장하고 loadClass(String name) 메소드를 오버라이딩하여 우리 자체 ClassLoader를 생성할 것입니다. 클래스 이름이 com.journaldev로 시작한다면 우리의 사용자 정의 클래스 로더를 사용하여 클래스를 로드하고 그렇지 않으면 부모 ClassLoader의 loadClass() 메소드를 호출하여 클래스를 로드합니다.
1. CCLoader.java
이것은 아래 메소드를 갖는 사용자 정의 클래스 로더입니다.
private byte[] loadClassFileData(String name)
: 이 메소드는 클래스 파일을 파일 시스템에서 바이트 배열로 읽습니다.private Class<?> getClass(String name)
: 이 메소드는 loadClassFileData() 함수를 호출하고 부모 defineClass() 메소드를 호출하여 클래스를 생성하고 반환합니다.public Class<?> loadClass(String name)
: 이 메소드는 클래스를 로드하는데 책임이 있습니다. 클래스 이름이 com.journaldev(샘플 클래스)로 시작한다면 getClass() 메소드를 사용하여 로드하고 그렇지 않으면 부모 loadClass() 함수를 호출하여 로드합니다.public CCLoader(ClassLoader parent)
: 이것은 부모 ClassLoader를 설정하는 생성자입니다.
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* Our Custom ClassLoader to load the classes. Any class in the com.journaldev
* package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
*
*/
public class CCLoader extends ClassLoader {
/**
* This constructor is used to set the parent ClassLoader
*/
public CCLoader(ClassLoader parent) {
super(parent);
}
/**
* Loads the class from the file system. The class file should be located in
* the file system. The name should be relative to get the file location
*
* @param name
* Fully Classified name of the class, for example, com.journaldev.Foo
*/
private Class getClass(String name) throws ClassNotFoundException {
String file = name.replace('.', File.separatorChar) + ".class";
byte[] b = null;
try {
// 이것은 파일에서 바이트 코드 데이터를 로드합니다.
b = loadClassFileData(file);
// defineClass는 ClassLoader 클래스에서 상속받았습니다.
// 이는 바이트 배열을 클래스로 변환하는 메서드입니다. defineClass는 final입니다.
// 따라서 오버라이딩할 수 없습니다.
Class c = defineClass(name, b, 0, b.length);
resolveClass(c);
return c;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
/**
* Every request for a class passes through this method. If the class is in
* com.journaldev package, we will use this classloader or else delegate the
* request to parent classloader.
*
*
* @param name
* Full class name
*/
@Override
public Class loadClass(String name) throws ClassNotFoundException {
System.out.println("Loading Class '" + name + "'");
if (name.startsWith("com.journaldev")) {
System.out.println("Loading Class using CCLoader");
return getClass(name);
}
return super.loadClass(name);
}
/**
* Reads the file (.class) into a byte array. The file should be
* accessible as a resource and make sure that it's not in Classpath to avoid
* any confusion.
*
* @param name
* Filename
* @return Byte array read from the file
* @throws IOException
* if an exception comes in reading the file
*/
private byte[] loadClassFileData(String name) throws IOException {
InputStream stream = getClass().getClassLoader().getResourceAsStream(
name);
int size = stream.available();
byte buff[] = new byte[size];
DataInputStream in = new DataInputStream(stream);
in.readFully(buff);
in.close();
return buff;
}
}
2. CCRun.java
이것은 main 함수를 가진 테스트 클래스입니다. 우리는 ClassLoader의 인스턴스를 생성하고 loadClass() 메서드를 사용하여 샘플 클래스를 로드합니다. 클래스를 로드한 후에는 Java Reflection API를 사용하여 해당 메서드를 호출합니다.
import java.lang.reflect.Method;
public class CCRun {
public static void main(String args[]) throws Exception {
String progClass = args[0];
String progArgs[] = new String[args.length - 1];
System.arraycopy(args, 1, progArgs, 0, progArgs.length);
CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
Class clas = ccl.loadClass(progClass);
Class mainArgType[] = { (new String[0]).getClass() };
Method main = clas.getMethod("main", mainArgType);
Object argsArray[] = { progArgs };
main.invoke(null, argsArray);
// 아래 메서드는 Foo가 우리의 사용자 정의 클래스 로더인 CCLoader에 의해 로드되는지를 확인하는 데 사용됩니다.
//
Method printCL = clas.getMethod("printCL", null);
printCL.invoke(null, new Object[0]);
}
}
3. Foo.java와 Bar.java
이들은 우리의 사용자 정의 클래스로더에 의해 로드되는 테스트 클래스입니다. 이들은 ClassLoader 정보를 출력하기 위해 printCL() 메서드를 가지고 있습니다. Foo 클래스는 우리의 사용자 정의 클래스 로더에 의해 로드됩니다. Foo는 Bar 클래스를 사용하므로 Bar 클래스도 우리의 사용자 정의 클래스 로더에 의해 로드됩니다.
package com.journaldev.cl;
public class Foo {
static public void main(String args[]) throws Exception {
System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
Bar bar = new Bar(args[0], args[1]);
bar.printCL();
}
public static void printCL() {
System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
}
}
package com.journaldev.cl;
public class Bar {
public Bar(String a, String b) {
System.out.println("Bar Constructor >>> " + a + " " + b);
}
public void printCL() {
System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
}
}
4. Java 사용자 정의 ClassLoader 실행 단계
먼저, 모든 클래스를 명령 줄을 통해 컴파일합니다. 그 후에는 CCRun 클래스를 실행하면서 세 개의 인수를 전달합니다. 첫 번째 인수는 클래스 로더에 의해 로드되는 Foo 클래스의 완전한 분류 이름입니다. 나머지 두 개의 인수는 Foo 클래스의 main 함수와 Bar 생성자로 전달됩니다. 실행 단계와 출력은 아래와 같습니다.
$ javac -cp . com/journaldev/cl/Foo.java
$ javac -cp . com/journaldev/cl/Bar.java
$ javac CCLoader.java
$ javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$ java CCRun com.journaldev.cl.Foo 1212 1313
Loading Class 'com.journaldev.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.journaldev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$
출력을 살펴보면, ‘com.journaldev.cl.Foo’ 클래스를 로드하려고 합니다. 이 클래스는 java.lang.Object 클래스를 확장하고 있으므로, 먼저 Object 클래스를 로드하려고 합니다. 그래서 요청은 CCLoader의 loadClass 메소드로 전달되고, 이 메소드는 부모 클래스로 위임합니다. 그래서 부모 클래스 로더가 Object, String 및 기타 java 클래스를 로드합니다. 우리의 ClassLoader는 파일 시스템에서만 Foo 및 Bar 클래스를 로드합니다. printCL() 함수의 출력에서 명확히 확인할 수 있습니다. loadClassFileData() 기능을 변경하여 바이트 배열을 FTP 서버에서 읽거나, 참조하는 써드파티 서비스를 호출하여 클래스 바이트 배열을 실시간으로 가져올 수 있습니다. 이 기사가 Java ClassLoader의 작동 방식을 이해하고 파일 시스템에서 가져오는 것 이상의 작업을 수행하는 방법을 확장하는 데 도움이 되기를 바랍니다.
사용자 정의 ClassLoader를 기본 ClassLoader로 사용하기
우리는 Java Options을 사용하여 JVM이 시작될 때 사용자 정의 클래스로더를 기본으로 설정할 수 있습니다. 예를 들어, java 클래스로더 옵션을 제공한 후 ClassLoaderTest 프로그램을 다시 실행할 것입니다.
$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/journaldev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.journaldev.classloader.ClassLoaderTest
Loading Class 'com.journaldev.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$
CCLoader는 com.journaldev
패키지에 있는 ClassLoaderTest 클래스를 로딩하고 있습니다.
Classloader 예제 코드는 저희 GitHub Repository에서 다운로드할 수 있습니다.
Source:
https://www.digitalocean.com/community/tutorials/java-classloader