클래스 로더
고친 사람 github-actions[bot]
클래스 로더는 프로그램이 쓰겠다고 한 클래스를 이름만 보고 찾아내 자바 가상 머신 안으로 올려 줍니다. 프로그램이 시작할 때 한꺼번에 올리지 않고 그 클래스를 처음 쓰는 순간에 하나씩 올립니다. 그래서 무엇을 쓸지 미리 다 정해 두지 않고도 프로그램이 돌아갑니다.
쉽고 빠른 이해
클래스 로더는 클래스 이름만 받아 그 클래스를 쓸 수 있게 만들어 주는 부품입니다. 코드가
com.example.Order 를 처음 건드리는 순간에 이 일이 일어납니다.
이게 없으면 프로그램이 쓸 코드를 시작 전에 전부 한 덩이로 붙여 두어야 합니다. 쓸지 안 쓸지 모르는 코드까지 미리 올라가고, 도는 도중에 새 코드를 들일 방법도 없습니다.
어떻게 도는가:
- 코드가 어떤 클래스를 처음 건드립니다
- 로더가 그 이름으로 클래스 파일을 찾아 읽습니다
- 내용을 검사하고 준비를 마친 뒤 쓸 수 있는 타입으로 넘겨줍니다
대가가 있습니다. 찾는 때가 실행 도중이라 빠진 클래스는 컴파일이 아니라 실행에서 드러납니다. 같은 이름의 클래스라도 올린 로더가 다르면 서로 다른 타입으로 취급됩니다.
자바 프로그램이면 늘 이 부품을 지납니다. 고를 수 있는 것은 직접 만든 로더를 둘지 말지뿐입니다.
상세
도서관에서는 책 제목만 대면 사서가 그 책을 찾아 줍니다. 빌려 온 책에는 그 도서관 도장이 찍혀 있습니다. 제목도 판도 같은 책이라도 학교 도서관에서 빌린 것은 동네 도서관이 받아 주지 않습니다.
클래스 로더는 클래스 이름을 받아 그 이름에 해당하는 클래스 파일을 찾아 읽고, 실행할 수 있는
타입으로 만들어 JVM(Java Virtual Machine, 자바 가상 머신)에 넘기는 부품입니다. 코드에
com.example.Order 라고만 적혀 있어도 그 이름이 어느 파일에 담겨 있는지 찾아내는 일이 여기서
끝납니다.
이런 부품이 따로 있는 까닭은 자바 코드가 다른 클래스를 이름으로만 부르기 때문입니다. 컴파일이 끝난 바이트코드 안에도 메모리 주소가 아니라 이름이 적혀 있습니다. 그 이름을 실제 코드에 이어 붙이는 일은 프로그램이 도는 동안 누군가 해야 합니다. 그 몫을 클래스 로더가 집니다.
이름이 타입이 되는 세 걸음
클래스 하나가 쓸 수 있는 상태가 되기까지는 세 걸음을 지납니다.
첫 걸음은 읽어 들이기입니다. 로더가 클래스 이름을 파일 경로로 바꿉니다. 그리고 클래스패스처럼 미리 정해 둔 탐색 경로를 차례로 뒤져 그 파일의 바이트를 읽습니다.
둘째 걸음은 잇기입니다. 읽어 온 바이트가 규칙에 맞는지 검사합니다. 클래스가 쓸 메모리를 잡습니다. 코드에 이름으로만 적힌 대상은 실제 대상에 이어 붙입니다.
이 셋을 묶어 잇기라고 부릅니다. 규칙에 맞는지 보는 일은 바이트코드 검증이 맡습니다.
셋째 걸음은 초기화입니다. 클래스에 적어 둔 초기 값과 static 블록이 이때 한 번 돌아갑니다.
flowchart TD
A["코드가 클래스 이름을 처음 건드린다"] --> B1
subgraph LOAD["읽어 들이기"]
B1["클래스 이름"] --> B2["파일 경로로 바꾼다"]
B2 --> B3["탐색 경로를 차례로 뒤져 바이트를 읽는다"]
end
B3 --> C1
subgraph LINK["잇기"]
C1["바이트가 규칙에 맞는지 검사한다"] --> C2["클래스가 쓸 메모리를 잡는다"]
C2 --> C3["이름으로만 적힌 대상을 실제 대상에 붙인다"]
end
C3 -.->|그 클래스를 정말 쓰는 때| D["초기화 · 초기 값과 static 블록을 한 번 돌린다"]
D --> E["쓸 수 있는 타입이 된다"]
세 걸음이 언제나 붙어서 일어나지는 않습니다. 읽어 들이기와 초기화는 서로 다른 때에 일어납니다. 파일을 읽어 둔 뒤에도 초기화는 그 클래스를 정말 쓰는 때까지 미룹니다. 다음 절은 읽어 들이기가 언제 일어나는지를 봅니다.
클래스를 올리는 때
로더가 움직이는 때는 프로그램이 시작하는 때가 아닙니다. 코드가 그 클래스를 처음 건드리는 때입니다. 객체를 새로 만들거나, 클래스 변수를 읽거나, 그 클래스의 메서드를 부르는 것이 모두 건드리는 일입니다.
이렇게 미뤄 두면 한 번도 안 타는 경로의 클래스는 끝까지 안 올라갑니다. 대신 빠진 클래스가 있어도 컴파일은 멀쩡히 끝납니다. 그 코드가 실제로 불리는 때가 되어서야 오류가 납니다.
클래스를 올리는 길은 하나가 아닙니다. 지금까지 본 길은 코드가 클래스를 건드리면 로더가 알아서
움직이는 길입니다. 다른 하나는 이름을 문자열로 건네 그때 올려 달라고 하는 길입니다.
Class.forName("com.example.Order") 처럼 씁니다.
길이 둘이라 오류도 둘로 갈립니다. ClassNotFoundException 은 이름으로 찾아 달라고 한 클래스가
탐색 경로에 없을 때 납니다. NoClassDefFoundError 는 컴파일할 때 이어져 있던 클래스가 실행할
때 사라지고 없으면 납니다.
앞은 이름을 직접 건넨 쪽에서 납니다. 뒤는 컴파일 때 이어져 있던 쪽에서 납니다. 둘 다 같은 말을 합니다. 로더가 그 이름을 파일로 잇지 못했다는 것입니다.
부모에게 먼저 묻는 순서
로더는 하나가 아닙니다. 여럿이 부모와 자식으로 이어진 계층을 이룹니다. 요청을 받은 로더는 먼저 부모에게 넘기고, 부모가 못 찾았다고 돌려줄 때만 자기가 찾아 나섭니다.
계층의 맨 위는 부트스트랩 로더입니다. 자바 핵심 라이브러리를 올립니다. 가상 머신 자신의 일부입니다.
그 아래 플랫폼 로더가 핵심 밖의 표준 라이브러리를 맡습니다. 맨 아래 애플리케이션 로더(줄여서 앱 로더)가 클래스패스에 적힌 내가 짠 코드와 가져다 쓰는 라이브러리를 맡습니다.
flowchart TD
REQ["올려 달라"] --> AP
subgraph H["로더 계층"]
AP["애플리케이션 로더 · 맨 아래 · 내 코드와 라이브러리"]
PL["플랫폼 로더 · 가운데 · 나머지 표준 라이브러리"]
BS["부트스트랩 로더 · 맨 위 · 자바 핵심 라이브러리"]
end
AP -->|먼저 부모에게| PL
PL -->|먼저 부모에게| BS
BS -->|java.lang.String 처럼 내게 있으면| F1["부트스트랩이 올린다 · 여기서 끝난다"]
BS -.->|내겐 없다| PL
PL -.->|내겐 없다| AP
AP --> F2["com.example.Order 는 내가 찾아 올린다"]
위로 올려 보내는 까닭은 핵심 라이브러리를 아무나 갈아 끼우지 못하게 하기 위해서입니다.
java.lang.String 이라는 이름의 클래스를 누군가 클래스패스에 몰래 끼워 넣어도, 그 이름은 늘
부트스트랩 로더가 먼저 잡습니다. 같은 클래스가 여러 번 올라가는 것도 이 순서가 막아 줍니다.
어느 로더가 올렸는지는 클래스 객체에 물어볼 수 있습니다.
String.class.getClassLoader() // null
Order.class.getClassLoader() // 앱 로더
핵심 라이브러리에서 null 이 나오는 것은 올린 로더가 없다는 뜻이 아닙니다. 부트스트랩 로더가
가상 머신 안쪽에 있어 자바 객체로 드러나지 않는다는 뜻입니다.
이름과 로더가 함께 정하는 신원
도는 동안 클래스의 신원을 정하는 것은 이름 하나가 아닙니다. 이름과 그것을 올린 로더가 함께 신원을 이룹니다. 같은 클래스 파일을 로더 둘이 각각 올리면 가상 머신은 그 둘을 서로 다른 타입으로 봅니다.
그래서 한쪽이 만든 객체를 다른 쪽의 타입으로 받으려 하면 ClassCastException 이 납니다. 이름도
같고 파일도 같은데 타입이 다르다는 오류라 처음 보면 읽히지 않습니다.
flowchart TD
F["같은 클래스 파일 · com.example.Order"]
F --> L1["로더 가"]
F --> L2["로더 나"]
L1 --> T1["로더 가가 올린 Order 타입"]
L2 --> T2["로더 나가 올린 Order 타입"]
T1 -.->|서로 바꿔 넣으면| X["ClassCastException"]
T2 -.-> X
불편해 보이는 이 규칙이 한 프로세스 안에서 여러 프로그램을 따로 굴리게 해 줍니다. 앱마다 로더를 따로 두면 같은 이름의 클래스를 앱마다 다른 내용으로 들고 있어도 서로 부딪히지 않습니다. 서블릿 컨테이너가 웹 애플리케이션마다 로더를 하나씩 두는 것이 이 성질을 쓰는 구조입니다.
직접 만들어 갈아 끼우는 로더
클래스 로더는 갈아 끼울 수 있는 부품입니다. 바이트를 어디서 가져올지는 로더가 정하므로, 직접 만든 로더는 파일 대신 네트워크에서 받아 오거나, 암호를 풀어 가며 읽거나, 그때그때 만들어 낸 바이트를 그대로 올릴 수 있습니다.
이 성질 위에 동적 로딩을 쓰는 구조가 섭니다. 플러그인을 나중에 꽂는 프로그램, 코드를 고치면 껍데기를 내리지 않고 새 클래스로 바꿔 다는 개발 서버가 모두 로더를 새로 만들어 쓰는 쪽입니다.
대가는 메모리입니다. 로더가 올린 클래스들은 그 로더와 운명을 같이하므로, 로더를 가리키는 참조가 어딘가에 하나라도 남아 있으면 가비지 컬렉터가 그 클래스들을 못 치웁니다. 같은 프로그램을 내렸다 올리기를 되풀이하는 서버에서 메모리가 조금씩 차오르는 일이 대개 이 대목에서 생깁니다.
flowchart TD
subgraph G1["1세대 로더"]
L1["로더"] --> C1["Order"]
L1 --> C2["그 밖에 이 로더가 올린 클래스"]
end
subgraph G2["2세대 로더"]
L2["로더"] --> D1["Order"]
L2 --> D2["그 밖에 이 로더가 올린 클래스"]
end
G1 -->|내렸다 올릴 때마다 한 세대씩| G2
REF["바깥 어딘가에 남은 참조"] -.->|하나만 남아도| L1
G1 --- STUCK["가비지 컬렉터가 못 치운다"]
관련 항목
클래스 로더가 찾아 읽어 올리는 파일과 이름
클래스 파일 · 바이트코드 · JAR · 상수 풀 · 이진 이름 · 패키지
클래스 로더가 클래스를 찾을 때 뒤지는 탐색 경로
클래스패스 · 모듈 경로 · 부트 클래스패스 · 자바 모듈 시스템 · 리소스 파일
클래스 로더를 품고 돌리는 실행 환경
JVM · JRE · JDK · 자바 · 코틀린 · JVM 언어
클래스를 올리는 동안 지나는 처리 단계
클래스 로딩 · 링킹 · 바이트코드 검증 · 기호 참조 · 정적 초기화 · 동적 링킹
클래스 로더 계층을 이루는 로더 갈래
부트스트랩 클래스 로더 · 플랫폼 클래스 로더 · 애플리케이션 클래스 로더 · 사용자 정의 클래스 로더 · 클래스 로더 계층 · 부모 위임
클래스 로더에서 자주 나는 오류·장애
ClassNotFoundException · NoClassDefFoundError · ClassCastException · LinkageError · 클래스 로더 누수 · 의존성 충돌
클래스 로더를 갈아 끼워 세우는 실행 구조
서블릿 컨테이너 · 플러그인 구조 · 핫 리로드 · 리플렉션 · 동적 프록시 · 동적 로딩
다른 플랫폼에서 같은 몫을 맡는 적재 장치
다른 이름: class loader · classloader · 클래스로더 · ClassLoader