Spring Boot
일반

자바 ClassLoader: 클래스는 언제 살아나는가?

중년개발자
중년개발자

@loxo

약 9시간 전

2

자바 ClassLoader: 클래스는 언제 살아나는가

ClassLoader는 이름 그대로 .class 파일을 찾아 JVM 메모리에 올리는 존재다.

하지만 단순한 파일 로더라기보다, 자바에서 다음을 결정하는 핵심 장치다.

“어떤 클래스를, 누가, 언제 메모리에 올리고 초기화할 것인가?”

1. 자바 프로그램이 실행되는 순간

다음 코드가 있다고 하자.

java
public class Main { public static void main(String[] args) { User user = new User(); } }

JVM이 처음부터 모든 클래스를 읽는 것은 아니다. 필요한 순간에 클래스를 찾아 로딩한다.

즉, 클래스는 보통 처음 사용되는 시점에 로딩된다.


2. ClassLoader의 기본 구조

일반적인 자바 애플리케이션에는 계층적인 ClassLoader가 있다.

Bootstrap ClassLoader

자바의 가장 기본적인 클래스를 로딩한다.

java
java.lang.String java.lang.Object java.util.List

JVM 자체에 가까운 로더라서 자바 코드에서 확인하면 null로 표시될 수 있다.

java
System.out.println(String.class.getClassLoader()); // null

null은 로더가 없다는 뜻이 아니라 Bootstrap ClassLoader가 로딩했다는 뜻이다.

Platform ClassLoader

JDK의 플랫폼 모듈과 관련된 클래스를 담당한다. 과거 Java 8까지는 비슷한 위치에 Extension ClassLoader가 있었다.

Application ClassLoader

우리가 작성한 클래스와 Maven·Gradle로 추가한 일반 라이브러리를 로딩한다.

java
System.out.println(Main.class.getClassLoader());

대부분 다음과 비슷한 결과가 나온다.

text
jdk.internal.loader.ClassLoaders$AppClassLoader

3. 클래스 로딩은 세 단계로 이해하면 된다

정확히는 더 세분화되지만 초보자 입장에서는 다음 세 단계가 핵심이다.

① Loading

ClassLoader가 User.class를 찾아 JVM에 읽어 들인다.

이때 JVM 안에는 해당 클래스를 나타내는 Class 객체도 만들어진다.

java
Class<?> clazz = User.class;

clazzUser 인스턴스가 아니라, User라는 클래스 자체를 표현하는 객체다.

② Linking

읽어온 바이트코드가 안전한지 검사하고 static 변수를 위한 공간 등을 준비한다.

java
static int count = 10;

준비 단계에서는 우선 타입의 기본값이 들어간다.

java
count = 0;

아직 코드에 작성한 10이 적용되는 시점은 아니다.

③ Initialization

이제 static 변수의 초기값과 static 블록이 실제로 실행된다.

java
class User { static int count = 10; static { System.out.println("User 클래스 초기화"); } }

개념적인 실행 순서는 다음과 같다.

text
count에 기본값 0 할당 count에 10 할당 static 블록 실행

4. static은 객체가 아니라 클래스에 붙어 있다

인스턴스 변수는 객체마다 따로 생긴다.

java
class User { String name; }
java
User a = new User(); User b = new User(); a.name = "철수"; b.name = "영희";

a.nameb.name은 서로 다른 공간이다.

반면 static 변수는 클래스에 하나만 존재한다.

java
class User { static int count; }
java
User.count++; User a = new User(); User b = new User(); System.out.println(a.count); // 1 System.out.println(b.count); // 1

표현은 가능하지만 static 변수는 다음처럼 클래스 이름으로 접근하는 것이 올바르다.

java
User.count

핵심은 다음과 같다.

구분소속생성 시점개수
인스턴스 변수객체new 실행 시객체마다 하나
static 변수클래스클래스 초기화 시클래스당 하나

5. static 블록은 언제 실행될까?

java
class DatabaseDriver { static { System.out.println("드라이버 준비"); } }

다음과 같이 클래스를 처음 적극적으로 사용하면 초기화된다.

java
new DatabaseDriver(); DatabaseDriver.someStaticMethod(); System.out.println(DatabaseDriver.someStaticField); Class.forName("DatabaseDriver");

그리고 static 초기화는 일반적으로 한 번만 실행된다.

java
new DatabaseDriver(); new DatabaseDriver(); new DatabaseDriver();

출력:

text
드라이버 준비

객체는 세 개지만 클래스 초기화는 한 번이다.

다만 정확히 말하면 “JVM 전체에서 무조건 한 번”이 아니라 다음이 맞다.

하나의 ClassLoader가 로딩한 클래스당 한 번 초기화된다.


6. static 변수도 정말 하나뿐일까?

보통의 애플리케이션에서는 하나라고 생각해도 된다. 하지만 ClassLoader가 다르면 같은 클래스의 static 변수도 따로 존재할 수 있다.

ClassLoader A가 읽은 User와 ClassLoader B가 읽은 User는 JVM에서 서로 다른 클래스로 취급된다.

자바에서 클래스의 진짜 정체는 단순히 클래스 이름만으로 결정되지 않는다.

text
클래스의 전체 이름 + 로딩한 ClassLoader

따라서 이름이 똑같아도 로더가 다르면 타입이 다르다.

text
com.example.User + ClassLoader A com.example.User + ClassLoader B

이 원리 때문에 애플리케이션 서버나 플러그인 환경에서 가끔 이런 오류가 발생한다.

text
ClassCastException: com.example.User cannot be cast to com.example.User

겉으로는 같은 클래스인데, 서로 다른 ClassLoader가 로딩한 클래스일 수 있다.


7. 부모 위임 모델

Application ClassLoader가 클래스를 요청받았다고 해서 바로 자신이 찾는 것은 아니다. 먼저 부모에게 요청한다.

이를 부모 위임 모델이라고 한다.

덕분에 개발자가 가짜 java.lang.String을 작성해도 쉽게 표준 String을 대체할 수 없다.

java
package java.lang; public class String { }

부모인 Bootstrap ClassLoader가 진짜 String을 먼저 제공하기 때문이다.

핵심 목적은 다음과 같다.

  • 자바 핵심 클래스 보호
  • 같은 클래스의 중복 로딩 방지
  • 클래스 검색 순서의 일관성 유지

8. 클래스 로딩과 객체 생성은 다르다

이 둘을 구분하면 ClassLoader가 쉽게 이해된다.

java
User user1 = new User(); User user2 = new User();

개념적으로는 다음 순서다.

text
1. User 클래스가 로딩되어 있는지 확인 2. 없다면 User.class 로딩 3. static 변수와 static 블록 초기화 4. user1 객체 생성 5. user2 객체 생성

클래스는 하나지만 객체는 여러 개다.


9. 코드로 확인하기

java
public class ClassLoaderExample { static { System.out.println("1. 클래스 초기화"); } private String name; public ClassLoaderExample(String name) { this.name = name; System.out.println("2. 객체 생성: " + name); } public static void main(String[] args) { System.out.println("3. main 실행"); ClassLoaderExample a = new ClassLoaderExample("A"); ClassLoaderExample b = new ClassLoaderExample("B"); System.out.println( ClassLoaderExample.class.getClassLoader() ); } }

출력 순서는 다음과 비슷하다.

text
1. 클래스 초기화 3. main 실행 2. 객체 생성: A 2. 객체 생성: B jdk.internal.loader.ClassLoaders$AppClassLoader

main()도 static 메서드이므로, main()이 실행되기 전에 해당 클래스의 static 초기화가 먼저 끝나야 한다.


10. 왜 지금도 알아야 할까?

ClassLoader는 오래된 주제처럼 보이지만 다음 문제의 밑바닥에는 여전히 이 원리가 있다.

  • static 싱글턴이 언제 만들어지는가
  • Spring Bean과 static 변수는 무엇이 다른가
  • ClassNotFoundExceptionNoClassDefFoundError
  • 리플렉션과 Class.forName()
  • JDBC 드라이버 로딩
  • WAS의 애플리케이션 격리
  • 플러그인 시스템
  • Spring Boot DevTools 재시작
  • 라이브러리 버전 충돌
  • 메타데이터 영역의 메모리 누수

특히 서버를 재배포했는데 static 데이터가 이상하게 남거나, 같은 클래스인데 캐스팅이 안 되는 문제는 ClassLoader와 연결되는 경우가 많다.

한 문장으로 정리

ClassLoader는 .class 파일을 JVM에 올리고, 클래스마다 static 공간을 만들며, 같은 이름의 클래스라도 누가 로딩했는지에 따라 서로 다른 타입으로 구분하는 장치다.

그리고 static의 핵심은 다음 문장이다.

static은 객체마다 생기는 값이 아니라, 특정 ClassLoader가 로딩한 클래스에 귀속되는 값이다.

#Java#ClassLoader#JVM#Class Loading#Java Development

댓글 0

Ctrl + Enter를 눌러 등록할 수 있습니다
※ AI 다듬기는 내용을 정제하는 보조 기능이며, 최종 내용은 사용자가 확인해야 합니다.