티스토리 뷰
1. 클래스 패스 섀도잉(Classpath Shadowing)이란?
[ 클래스 패스란? ]
JVM이 자바 애플리케이션을 실행하기 위해서는 .java 파일을 JVM이 이해할 수 있는 형태로 컴파일한 .class 파일이 필요하다. 그리고 클래스 파일은 .class와 같이 단건으로 저장될 수 있고, .jar와 같이 여러 클래스들이 압축 파일로 뭉쳐진 형태로도 존재할 수 있다.
JVM은 .class과 .jar 파일 등을 탐색하여 메모리 위에 올리게 되는데, 이때의 탐색 경로를 클래스 패스(Classpath)라고 부른다. 즉, “우리가 만든 코드를 실행할 때 필요한 클래스들을 어디서 찾아야 하나?”를 JVM에게 알려주는 것이라고 볼 수 있다. 클래스 패스에는 보통 다음이 들어가며, 그 외에도 사용하는 도구에 따라 다양한 경로들이 추가된다.
/classes
/build/classes
/target/classes
/lib/some-library.jar
예를 들어 Gradle 프로젝트라면 보통 이런 것들이 포함된다.
build/classes/java/main
build/classes/kotlin/main
build/resources/main
외부 의존성 JAR들
[ 클래스 로더란? ]
그렇다면 JVM은 어떻게 클래스 패스에서 클래스 파일을 찾아서 메모리에 올릴 수 있을까? 그것은 이를 처리하는 클래스 로더(Class Loader) 라는 컴포넌트가 존재하기 때문이다. 클래스 로더는 JVM이 main 부터 메서드를 실행하다가, 클래스가 필요해지는 순간 클래스 로드를 통해 해당 클래스를 찾는다.
예를 들어 다음과 같은 코드가 있다고 하자.
public class Main {
public static void main(String[] args) {
UserService userService = new UserService();
}
}
JVM이 Main을 실행하다가 UserService가 필요해지는 순간, 클래스 로더는 UserService.class를 찾는다. 그리고 이 클래스 파일을 읽어서 JVM 안에 로딩한다.
com/mangkyu/UserService.class
이때 해당 클래스 파일이 존재하는 위치를 탐색하는 기본 위치가 클래스 패스이고, 만약 클래스 패스가 다음과 같이 등록되었다고 하자.
app/classes
lib/a.jar
lib/b.jar
그러면 최종적으로 JVM은 다음의 경로에서 파일을 찾는 것이다.
app/classes/com/example/UserService.class
lib/a.jar!/com/example/UserService.class
lib/b.jar!/com/example/UserService.class
즉, 클래스 로더는 JVM이 메서드를 실행하는 데 필요한 클래스를 클래스 패스에서 찾아 메모리에 올려주는 역할을 한다.

[ 클래스 로더의 종류 ]
클래스 로더는 역할에 따라 여러 개가 존재한다.
- Bootstrap ClassLoader: 최상위 클래스 로더로, Object, String, Integer, List 등 JDK의 핵심 클래스를 로딩함
- Platform ClassLoader: Java 9 이후의 클래스 로더로, DBC, XML, logging 같은 Java SE 플랫폼 관련 클래스들을 로딩함
- Application ClassLoader: -cp, -classpath로 지정한 클래스, 애플리케이션 클래스, 외부 라이브러리 JAR 등의 클래스들을 로딩함
그렇다면 어떠한 클래스 로더가 요청을 먼저 처리할지에 대한 단계도 존재하는데, 이를 정리하면 다음과 같다. Bootstrap ClassLoader 부터 순차적으로 Platform ClassLoader 그리고 Application ClassLoader 을 거쳐 클래스 파일을 찾게 된다.

부모 클래스 로더에게 먼저 물어보는 이유는, 핵심 Java 클래스 보호 때문이다. 예를 들어 누가 패키지부터 클래스 이름까지 자바의 String 클래스와 완전히 동일한 String 클래스를 만들었다고 하자.
package java.lang;
public class String {
}
부모 위임 모델이 없다면 애플리케이션의 가짜 java.lang.String이 로딩될 위험이 있다. 하지만 부모 위임 모델에서는 java.lang.String을 먼저 Bootstrap ClassLoader가 찾아서 로딩하므로, 애플리케이션 클래스패스에 있는 가짜 String은 사용되지 않는다.

JVM은 클래스 로더를 통해 자바 클래스를 읽기 때문에, JVM 내부에서 클래스의 정체성은 단순히 패키지명과 클래스명만으로 결정되지 않는다. 대신 클래스 로더까지 더해져서 최종적으로 구분이 된다. 즉, 다음 두 클래스는 이름이 같아도 서로 다른 타입일 수 있다.
- ClassLoader A가 로딩한 com.mangkyu.User
- ClassLoader B가 로딩한 com.mangkyu.User

[ 클래스 패스 섀도잉이란? ]
우리가 만드는 클래스 그리고 외부 라이브러리를 통해 읽어오는 jar 파일에 존재하는 클래스들은 모두 Application ClassLoader가 읽게 된다. 그렇다면 만약 다음과 같이 패키지부터 클래스 이름, 그리고 이를 읽어들이는 클래스 로더까지 모든 것이 동일한 클래스가 존재한다면 어떻게 될까?
- /build/classes/com/mangkyu/User.class
- a.jar!/com/mangkyu/User.class
- b.jar!/com/example/User.class
이러한 경우에는 Application ClassLoader가 어떠한 경로를 먼저 탐색하는가에 달려 있다. a.jar를 먼저 본다면 a.jar의 User.class를 먼저 보게 된다. 그리고 한 번 로딩된 클래스는 같은 클래스 로더 안에서 다시 다른 버전으로 바뀌지 않는다. 이를 클래스 패스 섀도잉이라고 부른다.
- 클래스 로더는 같은 이름의 클래스를 여러 곳에서 발견하면, 자신이 탐색하는 순서상 먼저 찾은 클래스를 로딩한다.
- 한 번 로딩된 같은 이름의 클래스는 같은 ClassLoader 안에서 다시 다른 JAR의 클래스로 바뀌지 않는다.

예를 들어 동일한 FQCN(Fully Qualified Class Name, 패키지 경로를 포함하여 클래스를 고유하기 식별하는 전체 이름)을 가진 클래스가 있다고 하자.
- build/classes/kotlin/main/com/mangkyu/User.class
- libs/common-utils.jar!/com/mangkyu/User.class
일반적인 실행 환경에서 애플리케이션 클래스는 외부 라이브러리보다 클래스패스 앞쪽에 놓인다. 따라서 ApplicationClassLoader는 build/classes/kotlin/main/com/mangkyu/User.class를 읽고 이를 활용하게 되는 것이다.
참고로 이러한 동작 방식을 활용하면, 우리는 외부 라이브러리 등에 존재하는 클래스의 버그를 확인하기 위해 클래스 패스 섀도잉 기법을 활용할 수 있다.
'Java & Kotlin' 카테고리의 다른 글
| [Spring] MySQL의 wait_timeout 설정과 HikariCP 6.1.0에 추가된 maxLifeTime 분산 기능 (250) | 2026.02.03 |
|---|---|
| [Java] synchronized의 한계를 극복하기 위한 Lock 인터페이스와 ReentrantLock 클래스 (2) | 2026.01.13 |
| [Java] 동시성 제어를 위한 synchronized 키워드의 의미와 한계점 (2) | 2026.01.06 |
| [JVM] 힙 객체 헤더의 비효율과 이를 줄이기 위한 새로운 자바 객체 헤더(New Java Object Header: Compact Object Headers) (1) | 2025.10.14 |
| [JVM] JVM 내부의 힙 객체 헤더(Heap Object Headers on JVM Internals) (1) | 2025.10.07 |
