사전 클래스패스
개념

클래스패스

gabury1고친 사람 github-actions[bot]

클래스패스는 자바 프로그램을 컴파일하고 실행할 때 필요한 클래스를 어디서 찾을지 정해 줍니다. 컴파일러와 자바 가상 머신은 클래스 이름을 하나 만날 때마다 이 목록에 적힌 곳을 앞에서부터 뒤집니다. 목록에 없는 곳에 있는 파일은 디스크에 멀쩡히 있어도 없는 것으로 칩니다.

쉽고 빠른 이해

클래스패스는 클래스를 찾아볼 장소를 순서대로 적은 목록입니다. 「먼저 이 폴더, 없으면 저 묶음 파일」처럼 적어 두면 자바가 클래스 하나를 쓸 때마다 그 순서대로 뒤집니다.

이게 없으면 자바는 클래스 이름만 알고 그 코드가 담긴 파일이 어디 있는지는 모릅니다. 이름과 파일을 이어 주는 일을 이 목록이 맡습니다.

어떻게 도는가:

  1. 코드가 어떤 클래스를 처음 쓰는 순간 이름이 찾을 파일 경로로 바뀝니다
  2. 목록의 앞 항목부터 그 경로가 있는지 봅니다
  3. 처음 걸린 것을 읽어 씁니다. 끝까지 없으면 오류를 냅니다

대가가 있습니다. 뒤지는 때가 실행 도중이라 컴파일이 통과해도 실행에서 터질 수 있습니다. 같은 이름의 클래스가 두 곳에 있으면 앞에 적힌 쪽만 쓰이고 뒤쪽은 조용히 묻힙니다.

이 목록을 손으로 적는 것은 자바를 직접 굴려 볼 때뿐입니다. 실무에서는 빌드 도구가 대신 만들어 주므로 클래스패스를 직접 볼 일은 대개 무언가 안 될 때입니다.

상세

창고가 여러 동으로 나뉜 물류 센터에서 물건 하나를 찾는다고 해봅시다. 「A동 먼저, 없으면 B동, 그다음 C동」이라고 도는 순서를 정해 두면 직원은 물건 이름만 듣고도 그 순서대로 돌면 됩니다. 클래스패스가 그 순서표입니다.

다만 창고와 달리 목록에 없는 건물은 아예 들여다보지 않습니다. 바로 옆 건물에 물건이 쌓여 있어도 못 찾은 것으로 끝납니다.

클래스패스는 자바 컴파일러와 JVM(Java Virtual Machine, 자바 가상 머신)이 클래스 파일을 찾아볼 장소를 순서대로 적은 목록입니다. 주문을 다루는 프로그램이 com.example.Order 라는 클래스를 쓴다면, 그 클래스가 담긴 파일이 어느 폴더에 있고 어느 묶음 파일에 들어 있는지를 이 목록이 알려 줍니다.

목록이 따로 필요한 까닭은 자바 코드가 다른 클래스를 이름으로만 부르기 때문입니다. 코드에는 Order 라고만 적혀 있고 그 코드가 어느 파일에 있는지는 안 적혀 있습니다. 이름과 파일을 이어 주는 일은 누군가 해야 합니다. 그 일을 클래스패스가 맡습니다.

이름이 경로로 바뀌는 규칙

클래스 이름 하나는 이렇게 파일 하나로 이어집니다.

자바는 클래스를 패키지로 묶습니다. 패키지는 클래스 이름 앞에 붙는 점으로 이어진 이름입니다. 이름이 겹치는 것을 막으려고 씁니다. 클래스를 찾을 때는 그 점을 폴더 구분으로 바꾸고 뒤에 .class 를 붙입니다.

com.example.Order   →   com/example/Order.class

이제 이 경로를 클래스패스의 항목마다 대 봅니다. 항목이 폴더면 그 폴더 아래에서 이 경로를 찾고, 항목이 묶음 파일이면 그 묶음 안에서 같은 경로를 찾습니다.

flowchart TD
    N["클래스 이름 · com.example.Order"] --> P["찾을 경로 · com/example/Order.class"]
    subgraph 클래스패스
        D["항목 1 · 폴더 build/classes"]
        J["항목 2 · 묶음 libs/db.jar"]
    end
    P --> D
    D -->|없으면| J

앞 항목에서 걸리면 뒤 항목은 안 봅니다.

여기서 말하는 묶음 파일이 JAR(Java ARchive, 자바 아카이브)입니다. 클래스 파일 여러 개를 한 파일로 눌러 담은 것입니다. 남이 만든 라이브러리는 대개 이 꼴로 받아 씁니다. 안에 든 클래스를 꺼내 쓰려면 그 JAR 파일 자체를 클래스패스의 항목으로 적어야 합니다.

목록을 적는 법

그 목록을 사람이 넘기는 통로는 둘입니다.

첫째는 명령의 옵션입니다. -classpath 또는 짧게 -cp 뒤에 목록을 적습니다. 항목 사이는 유닉스 계열에서는 콜론 : 으로, 윈도우에서는 세미콜론 ; 으로 나눕니다. 같은 명령을 다른 운영체제로 옮길 때 이 글자 하나 때문에 안 도는 일이 자주 있습니다.

javac -cp libs/db.jar -d build src/com/example/Order.java
java  -cp build:libs/db.jar com.example.Main

첫 줄은 Order.java 를 컴파일해서 나온 클래스 파일을 build 폴더에 떨굽니다. -d 가 결과를 어디에 둘지 정하는 옵션입니다. 둘째 줄은 그 build 를 목록의 첫 항목으로 올려 실행합니다. 방금 만든 클래스 파일도 목록에 올려야 찾아집니다.

둘째는 CLASSPATH 환경 변수입니다. 옵션과 환경 변수가 둘 다 있으면 옵션이 이깁니다. 둘 다 없으면 자바는 지금 있는 폴더 하나만 봅니다.

항목이 많아지면 폴더 이름 뒤에 별표를 붙여 그 폴더의 JAR 파일들을 한꺼번에 올릴 수 있습니다. libs/* 라고 적으면 libs 안의 JAR 파일이 전부 항목으로 펼쳐집니다.

flowchart TD
    E["목록에 적은 항목 · libs/*"]
    subgraph libs
        A["a.jar"]
        B["b.jar"]
        subgraph sub
            C["c.jar"]
        end
    end
    E --> A
    E --> B

펼침은 한 겹입니다. 그림에서 c.jar 에만 화살표가 없는 것이 그 뜻입니다. libs/sub 처럼 그 아래 폴더에 든 JAR 은 항목이 되지 않습니다.

코틀린 컴파일러인 kotlinc 도 같은 이름의 옵션을 받습니다. 코틀린 코드도 결국 클래스 파일로 컴파일되므로 찾는 규칙이 자바와 같습니다.

뒤지는 때 — 실행 도중

클래스패스가 읽히는 때는 프로그램이 시작하는 때가 아닙니다. 그 클래스를 처음 쓰는 때입니다.

JVM은 클래스를 미리 다 읽어 두지 않습니다. 코드가 어떤 클래스를 처음 건드리는 순간 클래스 로더가 그때 클래스패스를 뒤집니다. 클래스 로더는 이름을 받아 클래스 파일을 찾아 읽고 메모리에 올려 주는 부품입니다.

flowchart TD
    A["클래스 이름을 처음 만난다"] --> B["이름을 파일 경로로 바꾼다"]
    B --> C["목록의 다음 항목에서 찾아본다"]
    C -->|있다| D["읽어서 메모리에 올린다"]
    C -->|없다| E{"남은 항목이 있나"}
    E -->|있다| C
    E -->|없다| F["못 찾았다고 오류를 낸다"]

그림에서 되돌아가는 화살표가 「앞에서부터 차례로」입니다. 목록의 끝까지 갔는데도 못 찾으면 그때 오류가 납니다. 컴파일할 때는 있던 클래스가 실행할 때 목록에 없으면 NoClassDefFoundError 가 옵니다.

이 때문에 클래스패스가 틀린 것은 컴파일에서 안 드러납니다. 그 클래스를 쓰는 코드가 실제로 불릴 때까지 조용하다가, 하루쯤 지나 어쩌다 타는 경로에서 터지기도 합니다.

앞선 항목이 이기는 규칙

같은 이름의 클래스가 두 항목에 함께 들었을 때 무엇이 쓰이는지를 libs/db-1.0.jar 와 libs/db-2.0.jar 로 봅니다. 둘이 같은 라이브러리의 옛 판과 새 판이고, 둘 다 안에 com.example.Db 를 담고 있다고 해봅시다.

목록을 앞에서부터 뒤진다는 말에는 뒤가 딸려 옵니다. 앞 항목인 libs/db-1.0.jar 에서 Db 가 걸리면 거기서 멈추므로, 뒤 항목의 Db 는 안 쓰입니다. 경고도 안 뜹니다.

flowchart TD
    S["com.example.Db 를 찾는다"]
    subgraph 클래스패스
        A["항목 1 · 묶음 libs/db-1.0.jar · 옛 판 Db"]
        B["항목 2 · 묶음 libs/db-2.0.jar · 새 판 Db"]
    end
    S --> A
    A --> H["여기서 멈춘다 · 옛 판 Db 를 쓴다"]
    B -.-> X["묻힘 · 경고 없음"]
    H --> M["새 판에만 있는 메서드를 부른다"]
    M --> R["NoSuchMethodError"]

그림의 마지막 갈래가 이 겹침이 골칫거리가 되는 대목입니다. 새 판에서 늘어난 메서드를 부르는 코드가 옛 판의 Db 를 잡으면 NoSuchMethodError 가 납니다. 클래스는 찾았는데 그 안에 부르려던 메서드가 없다는 뜻입니다.

고약한 대목은 항목의 순서만 바꿔도 결과가 달라진다는 것입니다. libs/db-2.0.jar 를 앞으로 옮기면 어제까지 옛 판을 잡던 코드가 오늘은 새 판을 잡습니다.

컴파일할 때와 실행할 때의 두 목록

클래스패스는 한 벌이 아닙니다. 앞에 적은 두 줄짜리 명령에서 javac 와 java 의 -cp 가 서로 달랐던 것이 그 때문입니다.

컴파일 명령에 준 목록과 실행 명령에 준 목록은 서로 남남입니다. 컴파일에 필요한 것만 챙기고 실행 명령에 같은 목록을 안 넘기면, 컴파일은 멀쩡히 끝났는데 실행에서 클래스를 못 찾습니다. 자바를 처음 손으로 굴려 볼 때 제일 자주 밟는 대목입니다.

그래서 실무에서는 이 목록을 손으로 안 적습니다. 빌드 도구가 프로젝트에 적힌 의존성 선언을 읽어 필요한 JAR 파일을 모으고, 컴파일과 실행 양쪽에 알맞은 목록을 만들어 넘깁니다. 클래스패스를 직접 볼 일은 대개 무언가 안 될 때입니다.

목록이 못 하는 것

클래스패스의 몫이 아닌 것이 있습니다. 목록이 하는 일은 찾아 주는 것뿐입니다. 무엇을 감출지도 무엇을 검사할지도 정하지 않습니다.

숨기는 힘이 없습니다. 클래스패스에 올린 JAR 안의 공개 클래스는 전부 다른 코드에서 보입니다. 라이브러리를 만든 쪽이 「이건 내부용이니 쓰지 마라」고 갈라 둘 방법이 목록에는 없습니다.

겹치는 것을 미리 알려 주지도 않습니다. 같은 이름의 클래스가 여러 항목에 들어 있어도 그대로 받아들이고 앞엣것을 씁니다. 빠진 것도 마찬가지입니다. 시작할 때 목록이 성한지 훑지 않으므로 빠진 클래스는 그것을 쓰는 코드가 실행될 때 드러납니다.

이 셋 때문에 자바에는 클래스패스 같은 탐색 경로가 하나 더 생겼습니다. 모듈 경로입니다. 모듈은 무엇을 내보내고 무엇을 감출지를 스스로 적어 두고, 그 선언을 프로그램이 시작할 때 맞춰 봅니다.

관련 항목

클래스패스를 읽어 클래스를 찾아 올리는 실행 장치

JVM · 클래스 로더 · 클래스 로딩 · 동적 링킹 · 리플렉션

클래스패스를 옵션으로 받는 도구와 배포판

javac · kotlinc · javap · JDK · JRE

클래스패스 항목 안에 놓이는 파일 형태

클래스 파일 · 바이트코드 · JAR · 패키지 · 상수 풀 · 리소스 파일

클래스패스에서 자주 나는 오류·장애

NoClassDefFoundError · ClassNotFoundException · NoSuchMethodError · 의존성 충돌 · 클래스 로더 누수

클래스패스를 대신하거나 보완하는 다른 탐색 경로

모듈 경로 · 자바 모듈 시스템 · 소스 경로 · 네이티브 라이브러리 경로 · 클래스 로더 계층

클래스패스를 대신 만들어 주는 빌드·의존성 도구

Maven · Gradle · 빌드 도구 · 의존성 · 전이 의존성 · 아티팩트 저장소

클래스패스 규칙을 그대로 따르는 JVM 언어

자바 · 코틀린 · 스칼라 · 그루비 · JVM 언어

클래스패스와 같은 일을 하는 다른 플랫폼의 탐색 경로

PATH · LD_LIBRARY_PATH · PYTHONPATH · 공유 라이브러리 · 모듈 해석

다른 이름: classpath · 클래스 경로