WEB BE Repository/JAVA

Java - classpath

조금씩 차근차근 2025. 12. 11. 00:24

우리가 특정 데이터를 JVM에 올리고 싶을 때, 이를 안정적으로 업로드할 "마법"이 필요해진다.

이 예로는 키 파일과 같은 것들이 있다.
만약 키를 평범한 상대경로/절대경로로 가져오고 있다면, 이는 현재 환경에 종속적인 빌드 결과가 된다.


이때, 클래스패스라는 용어를 곧잘 접하게 된다.

 

클래스패스는 자바의 JVM 이 클래스를 로드할 때 사용하는 클래스로더와도 곧잘 얽히는데, 지금부터 이 클래스패스에 대해 빠르게 알아보자.

1. 클래스패스란 무엇인가

간단히 말하면

JVM이나 컴파일러가 .class 파일과 리소스(예: .properties, .json, .xml)를 찾기 위해 검색하는 “경로 목록”

 

이다.

  • Java 소스: *.java
  • 컴파일 결과: *.class
  • 실행 시, JVM은 필요한 클래스를 어디에서 읽을지 알아야 함 → class path에 등록된 경로들을 순서대로 탐색.

클래스패스에는 다음과 같은 것들이 들어갈 수 있다.

  • 디렉터리 경로 (예: build/classes/java/main, out/production/classes)
  • JAR 파일 경로 (예: libs/some-lib.jar)
  • 와일드카드 (예: lib/* → 해당 디렉터리의 모든 JAR)

이걸 어떻게 쓰는지는, 3번부터 알아볼 것이다.


2. 클래스패스는 어디서 설정되는가

위에서 설명한 "마법"을 부리려면, 클래스 패스에서 올리고 싶은 파일을 찾아야 한다고 한다.

클래스패스에 대한 깊은 이해를 하기 전에, 이 클래스패스는 어디서 설정되는지부터 알아보자.

2-1. 명령행에서 직접 지정

java -cp .:libs/* com.example.Main
# 또는
java -classpath .:libs/* com.example.Main
  • -cp 또는 -classpath 옵션으로 클래스패스를 지정.
  • . 는 현재 디렉터리(대개 컴파일된 클래스가 들어 있는 디렉터리).
  • libs/*libs 폴더 내의 모든 JAR.

2-2. 환경 변수 CLASSPATH

예전 방식으로 환경 변수 CLASSPATH를 설정해둘 수도 있지만, 실무에서는 거의 사용하지 않거나, IDE/빌드 도구가 자동으로 관리한다.

2-3. JAR의 MANIFEST.MF 내부의 Class-Path

jar로 패키징할 때, META-INF/MANIFEST.MFClass-Path 항목을 넣어 다른 JAR 의존성을 기술할 수도 있다.

그러나 일반적으로는 Maven/Gradle이 전체 런처 스크립트를 만들어 주므로 직접 사용할 일은 많지 않다.

2-4. IDE / 빌드 도구(Maven, Gradle)에서 자동 설정

IntelliJ, Eclipse, VS Code 등 IDE나 Maven/Gradle이 각종

  • src/main/java → 컴파일 후 target/classes / build/classes/...
  • src/main/resources → 빌드 후 target/classes / build/resources/main
  • 의존성 JAR (~/.m2/repository/..., ~/.gradle/caches/...)

들을 모아서 실행 시점 classpath를 자동으로 구성해준다.

개발자는 보통 “어떤 소스/리소스가 어느 위치에 있어야 classpath에 포함되는가”만 알고 있으면 충분하다.


3. 클래스패스와 resources의 관계

src/main/resources가 중요한 이유는

빌드 후 src/main/resources 안의 모든 파일이 classpath 루트에 복사되기 때문이다.

스프링부트는 build 시, .jar 파일을 만들게 된다.
이 안에는 중요한 2가지 폴더가 존재한다.

  • BOOT-INF
    • classes
      • 우리가 구현한 클래스 코드들이 들어있다.
      • resources 폴더 내에 존재하는 파일들도 해당 폴더에 들어있다.
      • 즉, 여기가 classpath 루트이다.
    • lib
      • 외부 의존성 라이브러리가 들어있다.
  • META-INF
    • 실행에 필요한 메타데이터가 들어있다.

예시 (Maven/Gradle 공통적인 느낌)

src/main/java          → 컴파일 후: target/classes
src/main/resources     → 빌드 후: target/classes (리소스 파일)

따라서 다음 구조라면

src/main/resources/
  ├─ application.properties
  └─ config/
      └─ firebase-service-account.json

빌드 결과 classpath 상에서는

  • application.properties
  • config/firebase-service-account.json

이 두 파일이 classpath 기준 루트에 위치하게 된다.


4. 클래스패스와 파일 시스템 경로의 차이

여기에서 많이 헷갈리는 부분이 나온다.

  • 파일 시스템 경로: OS에서 보는 실제 디렉터리 경로
    • 예: /home/user/project/src/main/resources/config/firebase-service-account.json
    • Java에서 Paths.get(...), new File(...)로 접근
  • 클래스패스 경로: JVM이 “논리적으로” 보는 리소스 위치
    • 예: config/firebase-service-account.json
    • ClassLoader#getResourceAsStream, ClassPathResource에서 사용하는 경로

중요한 점

  1. 개발 환경에서는 src/main/resources가 실제 디렉터리로 존재하지만,
  2. 배포 시 JAR/War 안에 패키징되면 더 이상 파일 시스템 상의 폴더가 아니다.
  3. 대신, JAR 내부의 엔트리로 존재하고, JVM은 이를 classpath 엔트리로 취급.

따라서

Files.newInputStream(Paths.get("src/main/resources/config/..."));

이런 코드는 개발 환경에서는 동작할 수 있지만,
jar 패키징 후 서버에서 실행할 때는 src/main/resources 자체가 없으므로 실패한다.

반면에,

getClass().getClassLoader()
         .getResourceAsStream("config/firebase-service-account.json");

는 JAR 내부에 있든, 디렉터리 상태이든 상관 없이 classpath만 올바르게 구성되어 있으면 항상 동작한다.


5. 클래스패스와 ClassLoader

JVM은 클래스를 로딩할 때 ClassLoader를 사용한다.

대표적인 것들

  • Bootstrap ClassLoader
    • JDK 기본 라이브러리 (rt.jar / java.base) 등
  • Extension / Platform ClassLoader
  • Application ClassLoader
    • 우리가 지정한 classpath에 있는 것들을 로딩

ClassLoader는 위임(delegation) 모델로 동작하고 있다.

  1. 먼저 부모 ClassLoader에게 “이 클래스 있어?”라고 물어봄.
  2. 부모가 못 찾았을 때만 자신이 담당하는 classpath 영역에서 탐색.

this.getClass().getClassLoader()는 보통 Application ClassLoader를 반환하고,
이 ClassLoader가 관리하는 classpath 영역에서 우리가 원하는 파일을 찾을 수 있다.


6. 클래스패스 관련 에러와 증상

클래스패스를 이해하면 다음 예외의 원인이 명확해진다.

6-1. ClassNotFoundException

  • 런타임에 특정 클래스를 로딩하려고 했는데, classpath 어디에도 .class가 없는 경우.
  • 예: JDBC 드라이버 로딩 시를 호출했는데 MySQL 드라이버 JAR이 classpath에 없으면 ClassNotFoundException.
  • Class.forName("com.mysql.cj.jdbc.Driver");

6-2. NoClassDefFoundError

  • 컴파일 시에는 존재했던 클래스가, 실행 시점 classpath에는 없는 경우.
  • 예: A 클래스를 컴파일할 때 B에 의존했는데, 배포 시 B를 누락.

6-3. 리소스 로딩 실패

getResourceAsStream("...")null을 반환할 때

  • 리소스를 잘못된 경로로 참조
  • 리소스 파일이 빌드 결과에 포함되지 않음 (예: resources가 아닌 다른 디렉터리에 있음)
  • classpath에 해당 리소스를 포함하고 있는 모듈/JAR가 빠져 있음

7. Spring 관점에서의 클래스패스

Spring에서 자주 쓰는 classpath: 접두사는 전부 클래스패스를 통해 리소스를 찾겠다는 의미이다.

예시

@PropertySource("classpath:application.properties")
spring:
  config:
    import: "classpath:some-extra-config.yml"

또는 Java 코드:

Resource resource = new ClassPathResource("config/firebase-service-account.json");
try (InputStream is = resource.getInputStream()) {
    ...
}

이들은 모두 현재 애플리케이션의 classpath를 기준으로 리소스를 검색한다.


요약

  1. 클래스패스 = JVM이 클래스/리소스를 찾는 검색 경로 목록
  2. src/main/resources 안의 파일은 빌드 후 classpath 루트에 놓인다.
  3. classpath 기준 경로로 리소스를 읽을 때는
    • Java 표준
      • getClass().getClassLoader().getResourceAsStream("config/xxx.json");
    • Spring
      • new ClassPathResource("config/xxx.json").getInputStream();
  4. 파일 시스템 경로(src/main/resources/...)로 직접 접근하는 방식은
    배포 후 깨질 수 있으므로, 리소스는 되도록 항상 classpath를 통해 읽는다.
  5. ClassNotFoundException, NoClassDefFoundError, null 리소스는 대부분 classpath 문제다.

 

그러면 이렇게 원하는 파일을 탐색할 수 있다!

@Configuration
public class FirebaseConfig {
    @Bean
    public FirebaseApp firebaseApp() throws IOException {
        try (InputStream serviceAccount =
                     this.getClass().getClassLoader()
                             .getResourceAsStream("firebase/key-pair.json")) {

            if (serviceAccount == null) {
                throw new IllegalStateException("firebase/key-pair.json 파일을 찾을 수 없습니다.");
            }

            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                    .build();

            if (FirebaseApp.getApps().isEmpty()) {
                return FirebaseApp.initializeApp(options);
            } else {
                return FirebaseApp.getInstance();
            }
        }
    }
}

'WEB BE Repository > JAVA' 카테고리의 다른 글

가독성이 좋은 코드의 기준?  (0) 2025.06.06
자바 패키지 네이밍 컨벤션  (0) 2025.04.28
스레드 풀 생성 전략과 작업 거절 정책  (0) 2025.03.09
Java 에 대하여  (0) 2025.03.07
Java Thread Model 의 역사  (0) 2025.01.06