부모 위임 모델의 작동 원리
부모 위임 모델은 자바 가상 머신(JVM) 내 클래스 로딩 과정에서 적용되는 핵심 규칙으로, 강제적인 제약이 아닌 권장 방식입니다. 하위 클래스 로더는 자신이 직접 클래스를 로드하기 전에 상위 클래스 로더에게 우선적으로 로딩을 위임합니다. 상위 로더가 해당 클래스를 처리하지 못하면, 하위 로더가 스스로 로드하는 구조입니다.
자바 시스템에는 세 가지 계층적 클래스 로더가 존재하며, 순서는 다음과 같습니다:
- Bootstrap Class Loader: C++로 구현된 내부 로더로, 자바 코드에서는 직접 접근할 수 없으며
null로 반환됩니다. 주로<JAVA_HOME>/lib폴더 내의 기본 라이브러리(예: rt.jar)를 로드합니다. 사용자는 JVM 옵션을 통해 경로를 재정의할 수 있습니다. - Extension Class Loader: 확장 클래스 로더로,
<JAVA_HOME>/lib/ext디렉터리에 위치한 확장 라이브러리를 관리합니다. - Application Class Loader: 애플리케이션 클래스 로더로, 개발자가 작성한 클래스 및 외부 라이브러리(JAR 파일 등)를 로드하는 역할을 합니다.
각 로더의 확인 예시:
System.out.println(HashMap.class.getClassLoader()); // null (Bootstrap)
System.out.println(JarFileSystemProvider.class.getClassLoader()); // ExtClassLoader
System.out.println(Main.class.getClassLoader()); // AppClassLoader
이 메커니즘은 java.lang.ClassLoader 클래스의 loadClass 메서드에서 구현되어 있으며, 주요 흐름은 다음과 같습니다:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
long startTime = System.nanoTime();
try {
// 상위 로더에게 위임
if (parent != null) {
clazz = parent.loadClass(name, false);
} else {
clazz = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 상위 로더에서 실패 시, 다음 단계로 진행
}
if (clazz == null) {
// 상위 로더에서 찾지 못했으므로, 자체 로딩 시도
long endTime = System.nanoTime();
clazz = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(endTime - startTime);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(endTime);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
핵심은 findClass 메서드가 실제 클래스 로딩을 담당한다는 점입니다. 따라서 사용자 정의 클래스 로더를 작성할 때는 loadClass를 오버라이드하기보다는 findClass만을 재정의하는 것이 표준이며, 이는 부모 위임 규칙을 유지하는 데 필수적입니다.
부모 위임 모델의 목적
- 중복 방지: 하나의 클래스는 최초 한 번만 로드되며, 부모 로더가 이미 로드했다면 하위 로더는 이를 무시합니다. 중복 로딩을 막아 시스템의 안정성을 보장합니다.
- 보안성 확보: 핵심 클래스(예:
java.lang.*)는 항상 부트스트랩 로더에 의해 로드되도록 보장됩니다. 만약 외부 로더가 이러한 클래스를 조작하거나 대체할 수 있다면, 시스템의 기본 동작이 깨질 수 있습니다.
부모 위임 모델의 파괴 사례
다음과 같은 상황에서 부모 위임 모델이 의도적으로 또는 간접적으로 깨집니다:
- 사용자 정의 클래스 로더에서
loadClass재정의: 하위 로더가 자신의 로딩 로직을 직접 구현하고, 부모 로더 호출을 생략하면 위임 체인이 깨집니다. - SPI(Service Provider Interface) 활용 시: 특히
java.util.ServiceLoader는 서비스 제공자의 구현을 동적으로 로드해야 하는 경우가 많습니다. 예를 들어, JDBC 드라이버는 각 데이터베이스 업체가java.sql.Driver인터페이스를 구현하여 배포합니다.
다음 코드는 ServiceLoader 내부의 핵심 부분입니다:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String className = nextName;
nextName = null;
Class<?> clazz = null;
try {
// 여기서 부모 위임이 무시됨 → 부모가 아닌 특정 로더 사용
clazz = Class.forName(className, false, loader);
} catch (ClassNotFoundException e) {
fail(service, "Provider " + className + " not found");
}
if (!service.isAssignableFrom(clazz)) {
fail(service, "Provider " + className + " is not a valid implementation");
}
try {
S instance = service.cast(clazz.newInstance());
providers.put(className, instance);
return instance;
} catch (Exception e) {
fail(service, "Failed to instantiate provider " + className, e);
}
throw new Error("Unexpected error");
}
여기서 중요한 점은 Class.forName(className, false, loader)를 사용하고 있으며, loader 매개변수는 일반적으로 애플리케이션 로더입니다. 만약 단순히 Class.forName(className)만 사용했다면, ServiceLoader가 속한 rt.jar에 있는 부트스트랩 로더가 실행되며, 외부의 커스텀 또는 제3자 JAR 파일을 로드할 수 없습니다. 따라서 명시적으로 하위 로더를 전달함으로써 부모 위임을 우회하게 되고, 이는 본질적으로 모델을 깨뜨리는 행위입니다.
- 런타임 리로딩 (Hot Swap/Reload): 프로그램 실행 중에 클래스를 재로드하려는 경우, 기존 로더가 이미 클래스를 로드한 상태이므로 다시 로드할 수 없습니다. 이를 해결하기 위해 새로운 클래스 로더를 생성하고, 그 안에서 클래스를 새로 로드하는 방식이 사용되며, 이 역시 부모 위임 모델을 극복하는 것입니다.