빌드노트
-cp를 줬는데 java -jar가 라이브러리 클래스를 못 찾는 이유
라이브러리 JAR이 있어도 java -jar는 명령행의 -cp를 쓰지 않는다. 같은 두 JAR의 실패·성공을 비교하고, 메인 클래스 직접 실행과 manifest Class-Path 중 무엇을 선택할지 확인한다. 상대 경로의 JAR 기준도 다른 작업 폴더에서 검증했다.
실행 JAR 옆에 라이브러리를 두고 java -cp "app.jar;lib/helper.jar" -jar app.jar로 실행했는데도 NoClassDefFoundError가 날 수 있다. -jar 실행에서는 명령행의 다른 사용자 classpath 설정을 무시하기 때문이다. 라이브러리 경로를 더 길게 적기 전에 실행 방식을 확인해야 한다. 이 규칙은 Java 17 실행 명령 문서 (새 탭에서 열림)와 Java 21의 같은 문서 (새 탭에서 열림)에 명시돼 있다.
해결 경로는 두 가지다. 실행 스크립트에서 라이브러리를 정하려면 -jar를 빼고 -cp와 메인 클래스명으로 실행한다. java -jar를 유지하려면 JAR의 manifest에 Class-Path를 넣는다. 여기서는 클래스가 JAR 루트의 패키지 경로에 들어 있는 일반 실행 JAR을 대상으로 두 방법을 비교한다.
파일은 있는데 검색하지 않는지 확인한다
실패한 배포 폴더의 실제 JAR을 확인한다. 아래 명령은 Windows PowerShell과 JDK의 jar, java를 사용한다. app.jar는 앱, lib/helper.jar는 외부 라이브러리라는 예시 이름이다. 자기 파일명과 오류에 나온 클래스명으로 바꾼다.
먼저 앱과 라이브러리의 클래스가 각각 어디에 있는지 목록을 본다.
jar tf .\app.jar
jar tf .\lib\helper.jar
각 명령이 정상 종료한 목록에서 원하는 항목을 찾아야 한다. 오류가 support/Helper를 가리킨다면 라이브러리 안에는 support/Helper.class가 있어야 한다. JAR을 읽지 못했거나 그 항목이 없다면 파일 경로·배포 내용부터 확인한다. 파일이 있다는 것과 필요한 클래스가 들어 있다는 것은 다른 조건이다.
이번 비교용으로 만든 앱 JAR에는 demo/Main.class가, 라이브러리에는 support/Helper.class가 있었다. 앱 JAR을 ZIP 형식으로 열어 본 META-INF/MANIFEST.MF에는 Main-Class: demo.Main이 있었고 Class-Path는 없었다. 메인 메서드는 main-started를 출력한 뒤 Helper.message()를 호출한다. 그 메서드의 반환값은 helper-ok다. 실제 서비스의 장애 기록이 아니라 이 실행 규칙을 확인하려고 만든 작은 예제다.
라이브러리를 명령행 classpath에 넣고 JAR 모드로 실행했다.
java -cp ".\app.jar;.\lib\helper.jar" -jar .\app.jar
main-started까지 출력됐지만 종료 코드는 1이었다. 스택 추적에서 클래스 검색 실패를 보여 주는 두 줄은 다음과 같았다.
java.lang.NoClassDefFoundError: support/Helper
Caused by: java.lang.ClassNotFoundException: support.Helper
메인 클래스는 실행됐지만 외부 라이브러리의 클래스를 찾지 못했다. 같은 JAR 경로를 절대 경로로 바꿔 -cp에 넣어도 실패했다. 이 조건에서 필요한 변경은 경로 표기보다 실행 모드에 있다.
스크립트에서 경로를 정한다면 메인 클래스명으로 실행한다
앱 JAR과 라이브러리 JAR은 그대로 두고, -jar app.jar 부분을 메인 클래스명으로 바꿨다.
java -cp ".\app.jar;.\lib\helper.jar" demo.Main
이번에는 다음 두 줄을 출력하고 종료 코드 0으로 끝났다.
main-started
helper-ok
일반 JAR도 -cp의 한 항목으로 지정할 수 있다. 라이브러리만 넣고 앱 JAR을 빼면 메인 클래스 자체를 못 찾을 수 있으므로 둘을 함께 적는다. demo.Main에는 .class를 붙이지 않는다. 자기 JAR의 META-INF/MANIFEST.MF에 적힌 Main-Class 값과 실제 클래스 위치를 확인해 바꾼다.
Windows의 classpath 구분자는 ;이고 Linux·macOS는 :다. PowerShell에서는 위처럼 전체 classpath를 따옴표로 묶어야 한다. 이 구분자 규칙과 JAR을 classpath 항목으로 쓰는 방법은 Java 실행 옵션 문서 (새 탭에서 열림)에 설명돼 있다. 위 상대 경로는 명령을 실행하는 현재 폴더 기준이므로, 실행 스크립트의 작업 폴더도 맞추거나 절대 경로를 사용한다.
이 방식으로도 같은 클래스 검색 오류가 난다면 오류에 나온 클래스가 그대로인지, 지정한 JAR 안에 그 클래스가 있는지 다시 본다. 반대로 성공한다면 같은 파일을 읽을 수 있다는 근거가 되므로, 처음 실패한 -jar 실행의 의존성 경로를 고치는 데 집중할 수 있다.
java -jar를 유지하려면 manifest에 경로를 담는다
JAR 모드에서 시작할 메인 클래스는 Main-Class가 정한다. 외부 라이브러리 검색 경로는 같은 manifest의 Class-Path로 연결할 수 있다. JAR 파일 명세의 Class-Path 설명 (새 탭에서 열림)에 따르면 상대 경로의 기준은 그 manifest가 들어 있는 JAR의 위치다.
예제에서는 앱 JAR과 같은 폴더 아래 lib/helper.jar가 있으므로 Class-Path: lib/helper.jar를 추가했다. 다음은 기존 Class-Path가 없는, 서명 없는 일반 JAR의 복사본에서 확인한 명령이다. 앞에서 사용한 app.jar와 lib/helper.jar가 있는 폴더에서 실행한다. 같은 이름의 app-with-cp.jar가 없는지 확인하고, 각 명령이 성공한 뒤 다음 명령으로 넘어간다.
Set-Content -LiteralPath '.\classpath.mf' -Encoding ascii -Value @('Class-Path: lib/helper.jar', '')
Copy-Item -LiteralPath '.\app.jar' -Destination '.\app-with-cp.jar'
jar --update --file .\app-with-cp.jar --manifest .\classpath.mf
java -jar .\app-with-cp.jar
첫 줄은 끝에 빈 줄이 있는 manifest 추가용 텍스트를 만든다. jar --update --manifest는 그 정보를 복사본에 반영한다. 사용한 옵션은 JDK 17 jar 명령 문서 (새 탭에서 열림)에 있다. 확인한 최종 manifest에는 기존 Main-Class: demo.Main과 새 Class-Path: lib/helper.jar가 함께 있었다. 마지막 실행은 main-started, helper-ok를 출력하고 종료 코드 0으로 끝났다.
Class-Path에는 URL 경로 형식으로 lib/helper.jar를 쓴다. 여러 JAR은 공백으로 나열하며, 명령행 -cp의 ;나 :로 이어 붙이지 않는다. manifest를 고쳤다고 라이브러리가 앱 JAR 안에 복사되는 것도 아니다. 실제로 예제의 앱 JAR에는 여전히 demo/Main.class만 있었고 외부 lib/helper.jar가 필요했다.
경로 기준도 따로 확인했다. 앱 JAR이 있는 폴더 바로 아래의 빈 elsewhere 폴더로 이동한 뒤, 부모 폴더의 JAR을 실행했다.
java -jar ..\app-with-cp.jar
현재 폴더에는 lib가 없었지만 실행은 성공했다. 부모 폴더의 app-with-cp.jar를 기준으로 부모 폴더 아래 lib/helper.jar를 찾았기 때문이다. 반대로 앱 JAR만 elsewhere로 복사하고 라이브러리는 따라 복사하지 않은 상태에서 그 복사본을 실행하자 support/Helper 검색이 다시 실패했다. 배포할 때는 JAR과 라이브러리의 상대 배치를 함께 유지해야 한다.
배포본을 만드는 빌드 설정에도 필요한 Main-Class와 Class-Path를 반영해야 다음 빌드에 같은 조건이 남는다. 실행 환경마다 라이브러리 목록을 바꿔야 한다면 앞의 메인 클래스 직접 실행 방식을, JAR과 라이브러리를 정해진 구조로 함께 배포한다면 manifest 방식을 선택할 수 있다.
직접 실행한 환경은 Windows 11, PowerShell 7.6.6, OpenJDK 17.0.2였다. 새 합성 소스로 두 일반 JAR을 만들고 각 실행을 새 JVM에서 비교했다. Java 21은 문서 규칙만 확인했으며 그 버전이나 Linux·macOS에서 실행하지 않았다. Spring Boot 중첩 JAR, 모듈 실행, 별도 클래스 로더의 동작은 이 비교로 검증하지 않았다. 오류 메시지와 cause가 다른 NoClassDefFoundError도 이 비교만으로 원인을 확정할 수는 없다.