클래스 로더

블러거·2026년 1월 29일

JVM

목록 보기
14/26
post-thumbnail

클래스 로더

클래스 로딩 단계 중 첫번째 로딩 단계 중 완전한 이름을 보고 해당 클래스를 정의하는 바이너리 바이트 스트림 가져오기 는 ClassLoader 라는 JVM 외부 컴포넌트에서 이뤄진다.

이는 JVM 설계 의도이다. JVM 설계자들은 이 행동이 외부에서 이뤄져서 JVM 과 독립적으로 다양하게 사용자가 사용할 수 있도록 의도되었다.

클래스 로더의 독립성

클래스 로더가 로딩한 클래스는 클래스 로더 별로 독립적인 네임 스페이스를 가진다.
따라서 같은 클래스라 하더라도 다른 클래스 로더가 로딩하면 동등성 비교가 false 이다.

따라서 동등성을 사용하는 equals(), isAssignableFrom(), instanceOf 등의 구문들에 영향을 끼칠 수 있다.

클래스 로더의 부모 위임 모델

JDK 8 까지 클래스 로더는 3계층 부모 위임 클래스 로딩 아키텍처 를 사용했다.
JDK 9 이후에는 모듈 추가에 의해 조금 변형되었지만 큰 틀은 비슷하다.

클래스 로더 종류

  1. 부트스트랩 클래스 로더
    • 로드하는 것
      • rt.jar, tools.jar 같은 JVM 이 클래스 라이브러리로 인식하는 파일
      • JAVA_HOME/lib 경로의 파일
      • -xbootclasspath 설정위치 파일
    • 가장 기본적인 JDK 클래스들을 로드하는게 이 부트스트랩 클래스 로더이다.
    • 부트스트랩 클래스 로더는 자바에서 직접 참조할 수 없다. C++ 로 구현되어 있다. 만약 커스텀 클래스 로더에서 부트스트랩 클래스 로더로 작업을 위임하고 싶다면, 클래스 로더 참조 자리에 null 을 넣어야 한다.

rt.jar 에는 java.lang, java.io 등이 들어있다.

  1. 확장 클래스 로더
    • sun.misc.Launcher$ExtClassLoader
    • 로드하는 것
      • JAVA_HOME/lib/ext 경로의 파일
      • java.ext.dirs 시스템 변수값 위치의 파일
    • 사용자가 위의 위치에 범용 라이브러리 클래스 파일을 넣어 SE 기능을 확장하여 사용
  1. 애플리케이션 클래스 로더
    • sun.misc.Launcher$AppClassLoader
    • 로드하는 것
      • 클래스 패스상의 클래스 라이브러리들
    • ClassLoader.getSystemClassLoader(); 의 결과가 이 클래스 로더
    • 따로 클래스 로더를 만들지 않으면, 애플리케이션에서 실행에 기본적으로 사용되는 클래스 로더

클래스 패스 : 애플리케이션 클래스 로더가 로딩할 클래스 파일들의 위치

위임 모델

클래스 로더가 로딩 요청을 받으면, 먼저 상위 클래스 로더에 위임하여 상위 클래스 로더에서 로딩을 시도한다.

만약 상위 클래스 로더의 로딩 대상이 아니라면 그제서야 하위 클래스로더로 넘어간다.

따라서 어느 클래스 로더를 사용하든 가장 먼저 부트스트랩 클래스 로더로 로딩을 시도한다.

이 구조는 공통적인 클래스는 같은 클래스 로더가 생성함을 보장한다.
예를 들어 공통적으로 사용하는 java.lang.Object 의 경우 어떤 커스텀 클래스 로더에 의해 로드하더라도 하나의 Object 클래스가 생성되는게 맞다. 계층구조 위임 모델은 이를 보장한다.

계층 구조 위임 모델의 구현은 상속보다는 컴포지션을 사용한다. 커스텀 클래스 로더를 만들 일이 있으면 참고하자.

커스텀 클래스 로더

추상 클래스인 java.lang.ClassLoader 를 상속하여 구현하면 된다.

커스텀 클래스 로더를 만들때 핵심적인 두 메서드가 있다.

  • loadClass()
  • findClass()

커스텀 클래스 로더를 만들 때에는 findClass() 만 오버라이딩하면 된다.

java.lang.ClassLoader 은 이미 컴포지션, 위임 개념이 들어간 loadClass() 를 구현하고 있다. loadClass() 안에서 실질적으로 클래스 파일을 얻는 부분은 findClass() 이다.

부모 위임 모델과 컴포지션을 그대로 사용하면서 커스텀 클래스 로더를 만드려면 findClass() 만 오버라이딩하면 된다.

Thread Context 클래스 로더

자바의 모든 스레드는 기본적으로 TCCL 이라 부르는 클래스 로더를 가지고 있다.
Thread.currentThread().getContextClassLoader(); 를 실행해 보면 해당 클래스 로더를 볼 수 있다.
기본적으로 AppClassLoader 가 들어간다.

이런 설계의 원인은 다음과 같다.

JNDI 는 rt.jar 의 일부로서 부트스트랩 클래스 로더가 로드한다. 하지만 JNDI 는 사용자가 설정한 설정 클래스를 읽어야 한다. 부모 위임 모델에서는 상위 클래스 로더인 부트스트랩 클래스 로더가 애플리케이션 클래스를 볼 수 없다.

따라서 스레드는 주머니에 하위 클래스 로더를 따로 하나씩 들고 다니다가 필요시에 사용하도록 했다.
이것이 TCCL(Thread Context Class Loader) 이다.

JDBC, JCE, JAXB 등 SPI 서비스 관련 로딩은 모두 이 방식을 사용한다.

들어간 원인은 이 때문인데, spring 이든 여기저기서 스레드에서 클래스 로더 꺼내서 잘 활용하는듯 하다. 막 어디서 찾을 필요 없이 스레드에서 바로 꺼낼 수 있으니..

JDK 9 (모듈) 이후 클래스 로더

https://docs.oracle.com/en/java/javase/21/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-A868D0B9-026F-4D46-B979-901834343F9E

JDK 9 에는 jdk 의 왠만한 클래스를 모두 포함하는 rt.jar 가 사라지고, 내부에 있던 많은 패키지들은 각각 모듈로 바뀌었다.

이에 따라 ClassLoader 계층구조에도 몇가지 변화점이 생겼다.

  • 계층구조 자체는 유지.
  • ExtClassLoader -> PlatformClassLoader 로 변경
  • 부트스트랩, 플랫폼, 애플리케이션 클래스 로더가 jdk.internal.loader.BuiltinClassLoader 로부터 파생되도록 변경(기존에는 URLClassLoader)
  • 클래스 로더가 부모에게 위임하기 전, 로딩할 클래스가 특정 모듈에 속하는지 확인하고 그렇다면 해당 모듈의 클래스 로더로 위임하는 과정 추가.

[rt.jar 가 모듈로 쪼개짐]
JAVA_HOME/jmods 에는 컴파일용 jmod 파일들이,
JAVA_HOME/lib/modules 파일에는 런타임용 모듈들이 정의되어 있음. 내용물은 같음.
jimage extract $JAVA_HOME/lib/modules 명령어로 extract 가능

[ExtClassLoader 의 삭제]
모듈화가 되면서, 기존에 모든 커스텀 클래스 로더를 확장할 용도로 쓰이던 ExtClassLoader 가 필요 없어졌다.

확장할 기능을 모듈로 만든 뒤 의존성을 정해주는 방식으로 확장하게 권장한다.
다만 gradle/maven 의 의존성 관리가 좀 더 많이 쓰이는듯..

[PlatFormClassLoader 가 굳이 필요한가? BootstrapClassLoader 로 통합했으면 안되었나?]
platform 클래스 로더는 말 그대로 플랫폼 클래스들을 로딩하기 위해 사용한다.
실제로는 java.lang 은 BootstrapClassLoader, java.sql 은 PlatformClassLoader 처럼 클래스 로더가 로딩하는 모듈이 나뉘어 있다.

하지만 위임 모델로 PlatformClassLoader 를 통해 BootStrapClassLoader 가 담당하는 클래스도 모두 로딩할 수 있으며 사용자 입장에서 Bootstrap/Platform 각각이 어떤 클래스를 로딩하는지의 구분을 알 필요는 없다.

다만 BootStrapClassLoader 는 C++ 로 짜여져서 코드상 직접 사용할 수 없다. 그렇다고 AppClassLoader 는 애플리케이션용이라 이 클래스로 플랫폼 클래스들을 로딩하는건 부자연스럽다.
따라서 java.lang, java.sql 등 플랫폼 클래스 로딩을 제어를 자바 코드에서 하기 위해 만든 클래스 로더 계층이라 할 수 있다.

public static void main(String[] args) throws ClassNotFoundException {
    ClassLoader cl = ClassLoader.getPlatformClassLoader();
    
    Class<?> c1 = cl.loadClass("java.lang.String"); // BootStrapClassLoader 로 위임
    Class<?> c2 = cl.loadClass("java.sql.Date"); // PlatformClassLoader
    
    System.out.println(c1.getName());
    System.out.println(c2.getName());
}

Spring Boot, Tomcat 의 클래스 로더

Tomcat Class Loader

WebappClassLoader 는 톰켓에 올라갈 수 있는 여러개의 애플리케이션 각각에서 사용되는 클래스로더다. 예를 들어 war 3개를 올려놨다면 3개의 WebappClassLoader 가 사용되는 것이다.

WebappClassloader 는 /WEB-INF 경로의 애플리케이션 파일과 의존 라이브러리 jar 를 로드하는 역할이다. (war 파일 형식의 /WEB-INF 를 보자)

WebappClassLoader 의 상위 클래스로더인 Common 클래스 로더가 있다. 이는 lib 디렉토리(Tomcat 6 부터)를 클래스패스로 하는 클래스로더이며, 여러 애플리케이션이 공통으로 사용한다.

Spring Boot Class Loader

spring boot 는 임베디드 톰켓을 사용한다.
spring boot 의 bootjar 는 독립적으로 실행 가능한 jar 로서, 내부에 애플리케이션 클래스 파일과 의존 라이브러리 jar 를 가지고 있다.

즉, bootjar 는 nested jar 이다.

문제

Tomcat 의 기본적인 WebappClassLoader 는 클래스 로딩시 SystemClassLoader(보통 AppClassLoader) 를 사용할 수 있다. (WebappClassLoader 의 생성자를 보면 parent = null 시 SystemClassLoader 사용)

하지만 SystemClassLoader 는 의존 라이브러리들인 중첩된 jar 를 읽을 수 없다. 따라서 따로 중첩 jar 를 읽을 수 있는 클래스로더 메커니즘을 제공해야한다.

해결

spring boot 는 spring-boot-loader 에 정의된 LaunchedClassLoader 를 사용한다.

이 클래스 로더는 JarURLClassLoader 로서, 중첩된 Jar 를 해석할 수 있게 구현되어 있다.

그렇다면 WebappClassLoader 부모로서 LaunchedClassLoader 를 가지게 하면 될까?
아니다. WebappClassLoader 는 때때로 SystemClassLoader 를 사용할 수 있다 했다. spring boot 에서는 SystemClassLoader 가 아닌 LaunchedClassLoader 를 사용해야한다. 이 로직이 들어간 것이 TomcatEmbeddedWebappClassLoader 이다.

TomcatEmbeddedWebappClassLoader 는 spring boot 에서 온전히(SystemClassLoader 관여 없이) 부모 클래스 로더인 LaunchedClassLoader 만 사용되도록 하는 구현을 가지고 있다.

결론

스프링 부트에서는 TCCL(Thread Context Class Loader) 로 TomcatEmbeddedWebappClassLoader 를 가지고 다니면서 Nested Jar 구조에서 정상적으로 클래스 로딩이 가능하다.

intellij 같은 IDE 에서는 외부 의존성 jar 에 대한 class path 를 IDE 가 자동으로 지정해주므로 System ClassLoader(AppClassLoader) 로 의존성 라이브러리 클래스를 로드할 수 있다.
IDE 와 BootJar 에서 스프링 부트를 실행시키는 방법이 다름에서 오는 차이다.

참고

이 내용은 spring boot docs 에서도 엿볼 수 있다.

Thread.getContextClassLoader() 로 얻은 클래스 로더를 사용하라는 것이다.
ClassLoader.getSystemClassLoader() 로 얻은 클래스 로더를 사용하면 실패한다.

다음 코드를 spring boot 에서 bootJar 로 구동하여 실행해보자.
logback 라이브러리 클래스를 로드하는 코드인데, ClassLoader.getSystemClassLoader() 클래스 로더로 실행시 실패한다.

val threadClassLoader = Thread.currentThread().contextClassLoader
println(threadClassLoader.loadClass("ch.qos.logback.core.hook.DefaultShutdownHook")) // 이건 성공
println("thread = $threadClassLoader")

val systemClassLoader = ClassLoader.getSystemClassLoader()
println(systemClassLoader.loadClass("ch.qos.logback.core.hook.DefaultShutdownHook")) // 여기서 실패
println("system = $systemClassLoader")

반면 IDE 에서 실행할 경우에는 둘 다 성공한다.

다음은 Thread.getContextClassLoader() 클래스 로더의 toString() 값이다.
부모 클래스 로더로 LaunchedClassLoader 를 가지고 있음을 알 수 있다.

thread = TomcatEmbeddedWebappClassLoader
  context: ROOT
  delegate: true
----------> Parent Classloader:
org.springframework.boot.loader.launch.LaunchedClassLoader@28a418fc

참고 2 - java.util.logging

JDK 에는 로깅관련 라이브러리가 기본적으로 있다.
하지만 spring boot 를 사용할때면 logback, log4j2 등 로깅 라이브러리를 따로 사용한다.

이는 클래스 로더 문제와 관련있다.

java.util.logging 이 가진 문제가 System ClassLoader 가 아니면 클래스 로딩이 재대로 안된다.
따라서 nested jar 구조에서는 사용할 수 없다.

profile
안녕하세요!

0개의 댓글