Java 파일이 하나일 때는 컴파일과 실행이 어렵지 않다. 하지만 클래스가 여러 개이고, 패키지 폴더까지 나뉘면 ClassNotFoundException이나 NoClassDefFoundError가 등장하기 시작한다. 코드의 이름을 정하는 규칙(패키지)과 클래스를 찾는 위치(클래스패스)를 구분하면 이런 문제를 이해하기 쉽다.
다음과 같이 주문과 관련된 클래스를 구분한다고 가정하자.
my-app/
└── src/
└── com/
└── example/
├── app/
│ └── Main.java
└── order/
└── Order.java
Order.java:
package com.example.order;
public class Order {
private final String id;
public Order(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
Main.java:
package com.example.app;
import com.example.order.Order;
public class Main {
public static void main(String[] args) {
Order order = new Order("ORD-205");
System.out.println(order.getId());
}
}
패키지는 클래스를 논리적으로 그룹화하고, 같은 짧은 클래스 이름이 충돌하지 않도록 돕는다. Order의 전체 이름은 com.example.order.Order다.
프로젝트 최상위 폴더에서 다음 명령을 실행한다.
javac -d out src/com/example/order/Order.java src/com/example/app/Main.java
java -cp out com.example.app.Main
ORD-205
javac -d out: 컴파일 결과를 out 폴더 아래 패키지 경로에 배치한다.java -cp out: out을 클래스 검색 경로의 시작점으로 지정한다.com.example.app.Main: 실행할 클래스의 전체 이름(fully qualified name)이다.컴파일 후 구조는 다음처럼 된다.
out/
└── com/
└── example/
├── app/Main.class
└── order/Order.class
패키지와 맞는 경로를 만들었더라도 실행 시 java Main을 사용하면 JVM이 올바른 클래스를 찾지 못한다. 클래스패스의 루트와 패키지의 상대 경로를 함께 생각해야 한다.
package와 import의 역할은 다르다package com.example.app;
import com.example.order.Order;
package는 현재 소스가 속한 패키지를 선언하고, import는 다른 패키지에 있는 타입을 간단한 이름으로 사용하도록 한다. import는 실행 시 라이브러리를 다운로드하는 기능도 아니고 클래스파일 검색 경로를 설정하는 기능도 아니다.
java.lang.String, java.lang.System 같은 java.lang의 타입은 별도 import 없이 사용할 수 있다.
.class 파일을 하나씩 나눠 배포하는 대신 보통 여러 클래스를 JAR 파일로 묶는다. 직접 만든 라이브러리가 libs/common.jar에 있다면, 실행 시 해당 JAR를 클래스패스에 포함해야 할 수 있다.
# Linux / macOS
java -cp 'out:libs/common.jar' com.example.app.Main
# Windows PowerShell
java -cp 'out;libs/common.jar' com.example.app.Main
경로 구분자는 운영체제에 따라 다르다. 여기서 실제 예제의 Main은 외부 JAR가 없어도 실행되므로 위 명령은 의존 라이브러리가 추가된 경우를 설명하는 예시다.
실무에서는 Maven이나 Gradle이 의존성, 컴파일, 실행 설정을 관리하도록 하는 경우가 많다. 그러나 직접 컴파일해 보면 오류 메시지가 의미하는 바를 이해하기 쉬워진다.
ClassNotFoundException주로 프로그램이 이름으로 클래스를 동적으로 찾으려 했는데 발견하지 못한 상황에서 발생한다. 예를 들면 Class.forName("com.example.order.Order")에서 해당 클래스를 찾을 수 없는 경우다.
NoClassDefFoundError컴파일 시 참조했던 클래스의 정의를 실행 과정에서 찾거나 로드할 수 없는 경우 등에 나타난다. 라이브러리 누락, 다른 배포 버전, 초기화 실패 등을 점검할 수 있다.
package ... does not exist대개 컴파일 단계에서 패키지나 타입을 못 찾은 경우다. 소스 파일 포함 여부, 라이브러리 클래스패스, 오타부터 확인한다.
Java는 표준 API를 풍부하게 제공한다. 처음 보는 메서드를 만나면 검색 결과의 코드만 복사하기보다 공식 문서에서 다음을 확인하는 습관을 들이자.
null을 허용하는지 등예를 들어 String.substring(beginIndex, endIndex)는 시작 인덱스는 포함하고, 끝 인덱스는 포함하지 않는다. 이런 규칙은 공식 API 문서를 통해 확인하는 것이 가장 정확하다.
package는 소속을, import는 간단한 이름 사용을 지정한다.-d는 컴파일 결과 경로, -cp는 클래스 검색 경로를 설정한다.