
Java 프로젝트를 설정할 때 프로젝트 우클릭하면 Build Path가 나온다. 여기서 우리가 Oracle JDBC 드라이버(ojdbc8.jar) 같은 외부 라이브러리를 등록할 수 있다. 또 MySQL Connection(mysql-connector-java.jar)이라든지 Getter와 Setter을 자동생성하는 Lombok(lombok.jar) 라이브러리를 등록 해 본 적이 있을 것이다.
필자는 항상 이 라이브러리들을 추가할때 Classpath, Module Path 중 어디에 추가해야 하는지 헷갈렸기 때문에 이 글을 작성한다. Buildpath는 자바에서 중요한 개념이며, 어떤 것을 등록해야 하는지는 프로젝트 환경과 사용 기술에 따라 달라진다.
이론적인 설명은 건너 뛰고 언제 ModulePath vs Classpath를 사용하는지만 알고 싶은 사람은 6.언제 ModulePath vs Classpath를 사용할까? 로 바로 이동하기 바란다.
1. Classpath와 ModulePath의 개념
- Classpath: 사용자 정의 클래스(Class) 또는 패키지(Package)의 위치 (Java8 까지 기본 방식)
- ModulePath: 사용자 정의 모듈(Module)이 위치하는 곳
- 모듈패스는 한 마디로 모듈 개념을 도입하여 패키지를 그룹화한 위치이다. Java 9부터 도입되었다.
- ➡ 한 마디로 Java 8 이하에서는 Classpath 사용, Java 9 이상에서는 ModulePath 사용 권장
- ModulePath를 사용하면 exports 키워드로 원하는 패키지만 명확하게 공개할 수 있고, 나머지는 감출 수 있다.
- 즉, **캡슐화(Encapsulation)**가 가능해지고, 불필요한 코드 의존성을 막을 수 있다. 하지만 개인적으로 연습하는거면 classpath를 사용하는 것이 좋다. 라이브러리를 ModulePath에 추가하면, module-info.java에서 requires oracle.jdbc 같은 식으로 의존성을 직접 설정해주어야 한다. 자바의 캡슐화 원칙때문에 그렇다.
그림을 보면서 설명을 하자면, ModulePath는 패키지가 하나의 모듈만 액세스가 가능하고, 여러 패키지를 종속시킬 수 있다. 위치적인 개념이라고 생각하면 된다. JRE는 무조건 Module에 위치한다. 마치 메서드를 호출하는것 처럼 모듈을 만들어 호출하여 사용한다. (interface를 통해 접근하는것과 유사하다.)

📌 모듈(Module)
module 은 package들의 모음이며 packages의 top에 설치된다.
즉, module은 최상위 패키지라고 생각하면 된다. (패키지들의 상위 개념)
module-info.java 파일을 포함하기 때문에 접근제어(캡슐화) 가능 하다.
이게 무슨 말이냐면 모듈은 내부가 숨겨져 있다. 공개 설정하지 않는 한,
또한 모듈은 계약관계를 가진다. (코드가 예상대로 작동할 것이라는 보증이 있다) 이건 아래에서 자세히 살펴보겠다.
2. 모듈(Module)의 계약관계
모듈의 계약관계라는 말이 추상적이고 애매해서 따로 조사해 보았다.
모듈 시스템에서는 모듈이 제공하는 기능과 그것을 사용하는 다른 모듈 간의 관계가 명확하게 정의된다. 즉, 모듈은 개발자가 공개하도록 결정한 패키지만 외부에서 사용할 수 있도록 보장하며, 이를 통해 코드가 예상대로 작동할 수 있도록 한다. 반면, 클래스패스에서는 모든 패키지가 공개되므로 의도하지 않은 코드 접근이 가능할 수 있다. 이해를 돕기 위해 아래 예시를 살펴보자.
2-1. Modulepath 방식
모듈이 제공하는 패키지 (module-info.java)
module mymodule {
exports com.example.utils; // 이 패키지만 외부에 공개
}
com.example.utils패키지는 공개(export) 되므로 다른 모듈이 사용할 수 있음
하지만, 다른 패키지는 감춰져 있어서 예상치 못한 코드 변경이 영향을 주지 않음
2-2. Classpath 방식 (Java8 이하) 문제점
패키지를 가져다 쓰면 보증(guarantee)할 수 없는 이유에 대해서 설명하자면 일반적인 Classpath 방식(Java 8 이하)에서는 패키지를 직접 가져다 써야 한다. 이 경우, 해당 패키지의 개발자가 내부 코드를 수정할 가능성이 있다.
import com.example.utils.StringUtils; // 다른 개발자가 만든 유틸리티 클래스
public class Main {
public static void main(String[] args) {
System.out.println(StringUtils.capitalize("hello"));
}
}
🚨 여기서 만약에 StringUtils의 내부 코드가 변경되거나 삭제되면? 패키지를 가져다 쓰는 코드가 깨질 수 있다. 즉, 보증(guarantee)할 수 없다.
→ 결론: 모듈 시스템은 특정 패키지만 노출시키고, 내부 구조는 감춰서 코드가 의도치 않게 깨지는 것을 방지한다.
반면, 패키지를 직접 가져다 쓰면, 패키지 내부 코드가 바뀔 경우 의존하는 코드가 영향을 받을 수 있어 보장(guarantee)할 수 없다는 것이다.
여기서 자세히 들어가면 모듈에는 Unnamed Module 과 Named Module이 있다.
- Unnamed Module: Classpath의 코드들은 모두 unnamed module에 속한다.
- Named Module: ModulePath의 코드들은 각자 이름을 가진다.
3. Classpath
📌 Classpath (클래스패스)
- 모든 클래스는 "Unnamed Module"에 속함
- 클래스 간 의존성이 명확하지 않음 (충돌 가능성 높음)
- 모든 패키지는 기본적으로 공개(public)
- 실행할 때 -cp 옵션으로 설정
java -cp myapp.jar com.example.Main
4. Modulepath
- 각 모듈은 고유한 이름을 가짐
- module-info.java를 통해 의존성을 명확하게 선언
- exports 키워드를 사용하여 공개할 패키지를 제한할 수 있음
- 실행할 때 --module-path 옵션을 사용
java --module-path mods -m mymodule/com.example.Main
module mymodule {
exports com.example.utils; // 외부에서 접근 가능
requires anothermodule; // anothermodule이 필요함
}
5. 모듈 시스템이 제공하는 기능 & 용어 정리 설명
이건 코딩을 좀 배운 사람이라면 자바나 정보처리기사에서 자주 나오는 개념이기 때문에 알고 있겠지만
모르시는 분들을 위해 개념 설명을 하도록 하겠다.
5-1. 캡슐화(Encapsulation)
위에서 설명했지만 캡슐화를 한 마디로 요약하면 exports 키워드로 외부에서 접근 가능한 패키지를 제한할 수 있다는 개념이다.
Classpath에서는 모든 패키지가 공개되었지만, ModulePath에서는 패키지의 공개/비공개를 명확하게 설정할 수 있다고 했다.
Classpath(Java 8 이하)에서는 모든 패키지가 자동으로 공개된다. 그럼 다른 코드에서 마음대로 import 가능하겠지?
원하는 패키지만 외부에 공개할 방법이 없고 내부 구현을 보호할 수 없고, 의도치 않은 접근도 가능하다는 것이다.
반면 ModulePath(Java 9 이상, 모듈 시스템)에서는 exports 키워드를 사용하여 특정 패키지만 명확하게 공개 가능하다.
나머지 패키지는 기본적으로 비공개(다른 모듈에서 import 불가)이다. 따라서 내부 코드 보호가 가능하고, 의도된 방식으로만 접근이 허용된다.
모듈 정의 (module-info.java)
module mymodule {
exports com.example.utils; // ✅ 외부에서 사용 가능
}
패키지 구조
mymodule/
├── com/example/utils/StringUtils.java ✅ 공개됨
├── com/example/internal/SecretHelper.java ❌ 비공개 (외부에서 import 불가)
└── module-info.java
외부 모듈에서 사용 가능
import com.example.utils.StringUtils; // ✅ 가능
import com.example.internal.SecretHelper; // ❌ 오류! (비공개)
5-2. 계약 관계(Contract)
module-info.java에서 requires로 모듈 간 의존성을 명확하게 선언한다. 이 말은 풀어서 설명하자면 어떤 모듈이 다른 모듈을 필요로 하는지 선언하는 역할을 한다. Classpath에서는 의존성이 자동으로 인식되지 않지만, ModulePath에서는 명확한 선언이 필요 하다.
두 개의 모듈 (app과 utils)이 있다고 가정해보자.
여기서 app 모듈이 utils 모듈을 사용하려면 의존성을 명확하게 선언해야 한다.
📂 프로젝트 구조
myproject/
├── app/
│ ├── module-info.java
│ ├── com/example/app/Main.java
├── utils/
│ ├── module-info.java
│ ├── com/example/utils/StringUtils.java
utils 모듈은 유틸리티 기능을 제공하는 모듈의 집합체이다. app 모듈은 메인 애플리케이션 모듈이다.
requires 키워드를 쓰면 의존성을 명확하게 관리할 수 있다. 예를 들어 app 모듈이 utils 모듈을 필요로 한다고 선언하면, utils 모듈이 없으면 app 모듈이 아예 빌드가 안 되도록 막을 수 있다. 덕분에 불필요한 모듈 참조를 방지할 수 있고, 필요한 것만 딱딱 가져다 쓸 수 있다.
또한 requires는 캡슐화랑 보안 측면에서도 도움이 된다. utils 모듈에서 exports를 써서 특정 패키지만 공개할 수 있는데, 이렇게 하면 app 모듈은 utils에서 허용된 패키지만 접근할 수 있다. 덕분에 모듈 내부 코드가 보호되고, 외부에서는 꼭 필요한 기능만 사용할 수 있게 되는 것이다.
그리고 유지보수할 때도 requires 덕을 많이 본다. 필요한 모듈을 명확하게 정리해 두면 코드가 어디서 왔는지 쉽게 추적할 수 있고, module-info.java 파일만 보면 어떤 모듈이 어떤 의존성을 가지고 있는지 한눈에 알 수 있다. 덕분에 코드 관리가 훨씬 체계적으로 가능해지는 것이다.
utils/module-info.java
module utils {
exports com.example.utils; // ✅ `utils` 모듈에서 `com.example.utils` 패키지를 공개
}
utils/com/example/utils/StringUtils.java
package com.example.utils;
public class StringUtils {
public static String toUpper(String input) {
return input.toUpperCase();
}
}
app/module-info.java
module app {
requires utils; // ✅ `app` 모듈이 `utils` 모듈을 필요로 함 (의존성 선언)
}
app/com/example/app/Main.java
package com.example.app;
import com.example.utils.StringUtils; // ✅ utils 모듈에서 제공하는 클래스 사용
public class Main {
public static void main(String[] args) {
String message = "hello, world!";
System.out.println(StringUtils.toUpper(message)); // "HELLO, WORLD!" 출력
}
}
6. 언제 ModulePath vs Classpath를 사용할까?
일단 Java 8 이하에서는 Classpath를 사용한다. JAR 파일을 Add External JARs로 추가하는 것이 그 예시이다.
그리고 Java 9 이상 (모듈 시스템 사용 시)은 라이브러리릘 ModulePath에 추가한다. ( module-info.java에서 requires 설정 필수)
예를 들면
ojdbc8.jar을 Classpath에 추가하면 모든 패키지를 자유롭게 참조할 수 있다.
하지만, ModulePath에 추가하면 아래처럼 module-info.java에서 requires를 설정해야 한다.
module mymodule {
requires java.sql; // JDBC 사용
requires oracle.jdbc; // Oracle JDBC 사용
}
이건 자동으로 설정되지 않는다. ojdbc8.jar 같은 라이브러리를 ModulePath에 추가하면, module-info.java에서 직접 requires를 선언해줘야 한다.
왜? 이유는 자바 모듈 시스템이 강제하는 캡슐화 원칙 때문이야. Classpath에서는 모든 패키지를 자유롭게 참조할 수 있었지만, ModulePath에서는 모듈들이 명시적으로 공개(exports)된 패키지만 접근할 수 있기 때문에 그래서 ojdbc8.jar을 ModulePath에 추가하면, module-info.java에서 requires oracle.jdbc 같은 식으로 의존성을 명확하게 직접 설정한다.
만약 module-info.java를 추가하기 귀찮거나 기존처럼 자유롭게 참조하고 싶다면, ojdbc8.jar을 Classpath에 추가하는 방법도 가능하다. ModulePath를 사용하려면 수동으로 설정하는 게 필수이기 때문에 그냥 Classpath를 쓰자.
참고 자료 & 포스팅에 도움을 주신 분들
1. https://velog.io/@c4fiber/eclipse-modulepath-vs-classpath
2. https://whitekeyboard.tistory.com/849
'Java' 카테고리의 다른 글
| [Eclipse] 이클립스에서 메서드 구현부를 바로 찾아가도록 설정하는 방법 (4) | 2025.03.17 |
|---|---|
| [Java] 자바 주석 종류: Javadoc 주석(/** */)과 멀티라인 주석(/* */의 차이점 (7) | 2025.03.06 |
| [Eclipse] 이클립스 제이유닛 Junit java.lang.NoClassDefFoundError 해결법 (11) | 2025.02.27 |
| [Eclipse] restore 명령어로 복구한 파일이 이클립스 패키지 익스플로러에는 복구 안 되는 문제 해결법 (10) | 2025.02.14 |
| [Java] 자바 소켓 통신 Gson + BufferedWriter 사용중 host 에러 해결법 (11) | 2025.02.10 |