방송대 Java프로그래밍 15강: JAR 라이브러리와 Java 모듈 의존성
클래스를 JAR로 묶는 것과 다른 프로젝트에 공개할 패키지를 정하는 것은 같은 일이 아니다. 라이브러리는 코드를 전달하는 묶음이고, 모듈은 그 묶음의 이름·공개 범위·의존 관계까지 선언한다. 이 글은 JAR의 제작과 연결에서 출발해 exports, requires, requires transitive, Java 표준 모듈이 하나의 의존성 지도에서 어떻게 맞물리는지 추적한다.
하나의 재사용 코드가 세 경계를 통과한다
다른 프로그램에서 공통 클래스를 사용하려면 세 가지 경계를 차례로 넘어야 한다. 먼저 클래스 파일이 배포 가능한 묶음에 들어 있어야 하고, 사용하는 프로젝트가 그 묶음을 찾을 수 있어야 하며, 모듈 시스템을 사용한다면 제공자와 사용자의 선언이 서로 맞아야 한다.
| 경계 | 확인할 질문 | 강의의 핵심 수단 |
|---|---|---|
| 파일 경계 | 여러 .class 파일을 무엇으로 전달하는가? | 패키지 구조를 포함한 JAR 라이브러리 |
| 검색 경계 | 사용 프로젝트가 그 JAR 또는 모듈을 어디서 찾는가? | 클래스패스 또는 모듈패스에 추가 |
| 접근 경계 | 어떤 모듈이 어느 패키지를 읽고 사용할 수 있는가? | module-info.java의 exports와 requires |
이 세 경계를 구분하면 “JAR을 추가했는데 import가 안 된다”는 상황을 진단할 수 있다. 파일이 없을 수도 있고, 빌드 경로가 잘못됐을 수도 있으며, 모듈이 패키지를 공개하지 않았거나 사용 측이 의존성을 선언하지 않았을 수도 있다.
판단 핵심: JAR은 클래스의 물리적 묶음이고, 모듈 기술자는 접근 관계의 명시적 계약이다. 파일 존재와 사용 권한을 같은 조건으로 보지 않는다.
라이브러리는 재사용할 클래스와 인터페이스를 JAR로 묶는다
라이브러리(library)는 여러 프로그램이 공통으로 사용할 수 있는 클래스와 인터페이스의 모음이다. 서로 관련된 클래스는 패키지로 구성되고, 컴파일된 여러 .class 파일과 패키지 구조는 압축된 .jar 파일로 배포할 수 있다.
강의의 비모듈 프로젝트 예에서는 my_lib 프로젝트에 pack_a.Member와 pack_b.Student를 작성하고, dist 폴더의 my_lib.jar로 내보낸다. 이때 프로젝트를 만들 때 module-info.java는 생성하지 않는다.
my_lib/
├─ src/
│ ├─ pack_a/Member.java
│ └─ pack_b/Student.java
└─ dist/my_lib.jar
├─ META-INF/
├─ pack_a/Member.class
└─ pack_b/Student.class
JAR 안에는 소스 파일이 아니라 실행에 필요한 클래스 파일이 포함된다. 패키지 디렉터리도 함께 유지되므로 사용 코드는 원래의 완전한 클래스 이름을 기준으로 import한다.
JAR을 사용하는 프로젝트는 위치에 맞는 추가 방법을 고른다
강의의 Eclipse 절차에서는 사용 프로젝트의 Java Build Path에서 라이브러리를 클래스패스에 연결한다. 작업 공간 바깥의 파일이면 Add External JARs, 현재 작업 공간 안의 파일이면 Add JARs를 선택한다.
| JAR 위치 | 선택 | 이유 |
|---|---|---|
| Eclipse 작업 공간 외부 | Add External JARs | 파일 시스템의 외부 경로에서 JAR을 선택 |
| Eclipse 작업 공간 내부 | Add JARs | 작업 공간에 포함된 자원에서 JAR을 선택 |
package LibraryExample;
import pack_a.Member;
import pack_b.Student;
public class Main {
public static void main(String[] args) {
Member member = new Member();
Student student = new Student();
}
}
빌드 경로 연결은 컴파일러가 클래스를 찾게 하고, import는 소스에서 완전한 이름을 짧게 쓰게 한다. 따라서 import만 추가하고 JAR을 연결하지 않으면 클래스 정의를 찾을 수 없다. 반대로 JAR을 연결해도 필요한 타입 이름을 잘못 import하면 사용할 수 없다.
모듈은 패키지 묶음에 이름과 접근 계약을 더한다
Java 9부터 도입된 모듈 시스템에서 모듈(module)은 관련 패키지를 하나의 기능 단위로 캡슐화한 묶음이다. 라이브러리가 파일을 재사용 가능하게 모으는 데 초점을 둔다면, 모듈은 어떤 패키지를 외부에 공개하고 어떤 다른 모듈에 의존하는지를 명시한다.
| 비교축 | 일반 JAR 라이브러리 | 명시적 모듈 |
|---|---|---|
| 주요 목적 | 클래스와 패키지를 하나의 파일로 배포 | 패키지 묶음의 경계와 의존 관계까지 선언 |
| 기술자 | 강의의 비모듈 예에는 없음 | module-info.java 사용 |
| 기본 외부 공개 | public 타입을 클래스패스에서 사용 | 패키지는 기본적으로 다른 모듈에 공개되지 않음 |
| 프로젝트 연결 | 클래스패스 | 모듈패스와 모듈 의존 선언 |
여기서 public의 범위가 중요하다. 클래스가 public이어도 그 패키지를 모듈이 exports하지 않으면 다른 모듈에서는 공개 API로 사용할 수 없다. 모듈은 패키지 단위의 접근 제한을 하나 더 만든다.
오개념 교정: “public 클래스면 어디서나 접근 가능하다”는 판단은 모듈 경계를 빠뜨린다. 다른 모듈에서 쓰려면 타입 자체의 접근 수준과 패키지의 exports, 사용 모듈의 requires를 함께 확인해야 한다.
module-info.java는 모듈 지도의 출발점이다
module-info.java는 모듈의 이름과 모듈 사이의 관계를 기록하는 모듈 기술자(module descriptor) 소스 파일이다. Java 프로젝트의 src 폴더에 두며, 배포된 JAR에서는 컴파일된 기술자가 루트에 위치한다.
module my_mod_a {
requires my_mod_b;
}
module my_mod_b {
exports package_a;
exports package_b;
}
첫 선언은 my_mod_a가 my_mod_b에 의존함을 나타낸다. 둘째 선언은 my_mod_b가 두 패키지를 다른 모듈에 공개한다는 뜻이다. 모듈 이름은 강의 예제에서 프로젝트 이름과 같게 사용하지만, 실제 관계를 결정하는 것은 기술자에 적힌 모듈 선언이다.
exports는 제공자의 문이고 requires는 사용자의 화살표다
모듈 간 사용 관계에는 제공자와 사용자가 각각 해야 할 선언이 있다. 제공 모듈은 공개할 패키지를 exports하고, 사용 모듈은 필요한 모듈을 requires한다.
| 선언 | 작성 위치 | 무엇을 결정하는가? | 누락하면 생기는 문제 |
|---|---|---|---|
exports pack_a; | 패키지를 가진 제공 모듈 | pack_a를 다른 모듈에 공개 | public 클래스라도 모듈 밖에서 접근 불가 |
requires my_mod_a; | 패키지를 사용할 소비 모듈 | my_mod_a를 읽는 의존성 선언 | 그 모듈이 공개한 패키지를 import할 수 없음 |
import pack_a.Member; | 타입을 쓰는 Java 소스 | 소스 코드의 타입 이름을 연결 | 타입을 짧은 이름으로 참조할 수 없음 |
exports, requires, import는 서로 대신할 수 없다. exports는 제공 범위, requires는 모듈 읽기 관계, import는 소스 수준의 타입 이름 사용을 담당한다.
세 모듈 예제로 공개와 의존을 동시에 추적한다
학습을 위해 강의의 구성을 다른 이름으로 바꿔 보자. catalog.core에는 공개할 catalog.api와 내부 구현용 catalog.internal이 있고, store.app이 API를 사용한다고 가정한다.
module catalog.core {
exports catalog.api;
}
module store.app {
requires catalog.core;
}
catalog.core는catalog.api만 외부에 공개한다.store.app은catalog.core를 읽겠다고 선언한다.store.app의 소스는catalog.api의 public 타입을 import할 수 있다.catalog.internal의 클래스가 public이어도 패키지를 exports하지 않았으므로 모듈 밖의 공개 API가 아니다.
이 예제의 검산 질문은 간단하다. “타입이 public인가?”만 묻지 말고, “그 타입의 패키지가 제공 모듈에서 exports됐는가?”와 “사용 모듈이 제공 모듈을 requires하는가?”를 연이어 묻는다.
requires transitive는 의존성 화살표를 다음 사용자에게 전달한다
일반 requires는 현재 모듈이 다른 모듈에 의존한다는 관계다. requires transitive는 그 의존성을 현재 모듈을 사용하는 모듈에도 전달한다.
module service.facade {
requires transitive service.model;
exports service.api;
}
client.app이 service.facade를 requires하면, 전이 선언 때문에 service.model도 읽을 수 있다. 단, 읽을 수 있다는 사실과 모든 패키지가 공개된다는 사실은 다르다. service.model이 실제로 외부에 제공할 패키지는 그 모듈 자신의 exports 선언으로 정한다.
| 관계 | client.app이 읽는 모듈 | 판단 근거 |
|---|---|---|
client.app → service.facade | service.facade | client의 직접 requires |
service.facade → service.model | service.model까지 전달 | facade의 requires transitive |
| model 내부 패키지 접근 | exports된 패키지만 가능 | 전이 의존성은 exports를 자동 생성하지 않음 |
경계 사례: requires transitive를 “하위 모듈의 모든 구현을 자동 공개한다”로 해석하면 틀린다. 전달되는 것은 읽기 의존성이고, 패키지 공개 범위는 각 제공 모듈의 exports가 별도로 통제한다.
Java 표준 라이브러리도 java.xxx 모듈로 나뉜다
Java 9부터 Java 표준 라이브러리도 모듈화되었다. java.xxx 이름의 표준 모듈을 플랫폼 모듈이라고 하며, 그중 java.base는 Java 플랫폼의 가장 기본이 되는 모듈이다.
java.base는 다른 모듈에 의존하지 않으며 다른 모든 모듈이 기본적으로 의존한다.java.base를 사용하기 위한requires선언은 생략한다.java.lang,java.math,java.net,java.io,java.nio,java.util등의 패키지가java.base에 포함된다.java.lang은 별도의 import 없이 사용할 수 있지만,java.util같은 다른 패키지는 필요한 타입을 import해야 한다.
따라서 requires 생략과 import 생략을 같은 규칙으로 외우면 안 된다. java.base 의존성은 암묵적이지만, 그 안의 모든 패키지 타입이 자동 import되는 것은 아니다.
java.sql 사용은 모듈 선언과 import가 함께 필요하다
java.sql 패키지는 같은 이름의 java.sql 표준 모듈이 exports한다. 모듈 프로젝트에서 JDBC 관련 타입을 사용하려면 모듈 기술자에 의존성을 선언하고, Java 소스에서는 필요한 타입을 import한다.
module com.example.app {
requires java.sql;
}
package jdbc_main;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Main {
public static void main(String[] args) throws SQLException {
Connection connection = DriverManager.getConnection(
"jdbc:example://localhost/sample", "user", "password");
connection.close();
}
}
위 코드는 선언 관계를 보여 주기 위한 학습용 예시다. 첫 코드의 requires java.sql은 모듈 읽기 관계를 만들고, 둘째 코드의 import는 Connection과 DriverManager라는 타입 이름을 소스에 연결한다. 어느 한쪽만으로 다른 쪽의 역할을 대신할 수 없다.
연결 실패는 파일·경로·공개·의존 순서로 찾는다
클래스나 패키지를 사용할 수 없을 때는 오류 메시지만 보고 import를 반복해서 고치기보다 관계망의 바깥쪽부터 확인한다.
- 파일: 필요한 클래스가 컴파일되어 JAR 또는 모듈 산출물에 포함됐는가?
- 경로: 일반 라이브러리는 클래스패스, 모듈 프로젝트는 모듈패스에 올바르게 연결됐는가?
- 공개: 제공 모듈이 사용할 패키지를
exports했는가? - 의존: 사용 모듈이 제공 모듈을
requires했는가? - 타입: Java 소스의 package와 import, 클래스 이름이 일치하는가?
- 전이: 간접 의존을 기대한다면 중간 모듈이
requires transitive로 전달하는가?
이 순서는 물리적 배포에서 논리적 접근 계약으로 좁혀 간다. 예를 들어 JAR 자체가 연결되지 않았는데 exports만 고치거나, 제공 모듈이 패키지를 감췄는데 import만 바꾸는 식의 헛수고를 줄일 수 있다.
핵심 개념 정리
- 라이브러리는 여러 프로그램이 재사용할 클래스와 인터페이스를 패키지 구조와 함께 JAR로 묶는다.
- 일반 JAR은 클래스패스에, 모듈 프로젝트의 의존 대상은 모듈패스에 연결하는 강의 흐름을 구분한다.
- 모듈은 관련 패키지의 묶음에 이름, 공개 범위와 의존 관계라는 계약을 더한다.
exports는 제공 모듈의 패키지 공개,requires는 사용 모듈의 읽기 의존,import는 소스의 타입 이름 연결을 담당한다.requires transitive는 읽기 의존성을 다음 사용자에게 전달하지만 패키지를 자동으로 exports하지 않는다.java.base의존성은 암묵적이고,java.sql같은 다른 표준 모듈은 명시적으로 requires한다.
라이브러리·모듈 문제는 배포할 클래스 묶음 확인 → 클래스패스·모듈패스 연결 → 제공자의 exports → 사용자의 requires → 소스의 import → 전이 의존성 추적 순서로 읽는다. 이 관계망을 그릴 수 있으면 파일이 존재하는데도 접근할 수 없는 이유와, 직접 의존과 전달된 의존의 차이를 설명할 수 있다.
예상문제 10선
1. Java 라이브러리에 관한 설명으로 가장 적절한 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
- ① 오답: 스레드 상태는 실행 제어의 개념이며 재사용 코드 묶음의 정의가 아니다.
- ② 오답: 모듈 의존 관계는
module-info.java가 기술하며 라이브러리 전체의 뜻보다 좁다. - ③ 오답: 라이브러리는 여러 관련 클래스를 패키지로 구성해 JAR로 묶을 수 있다.
- ④ 정답: 여러 프로그램에서 공통으로 재사용할 코드 집합이라는 목적을 정확히 나타낸다.
2. 강의의 Eclipse 절차에서 작업 공간 내부의 JAR을 추가할 때 선택할 항목은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
- ① 오답: 외부 JAR 선택은 Eclipse 작업 공간 밖의 파일을 연결할 때 사용한다.
- ② 정답: 작업 공간 안에 있는 JAR 자원을 선택하는 항목이다.
- ③ 오답: 컴파일된 클래스 폴더를 연결하는 선택으로 JAR 파일 조건과 다르다.
- ④ 오답: JRE 같은 라이브러리 컨테이너를 추가하는 항목이며 현재 JAR 위치 조건에 맞지 않는다.
3. module-info.java에 기록하는 정보의 조합으로 옳은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
- ① 정답:
module,requires,exports선언이 각각 이 관계를 기술한다. - ② 오답: 객체 상태와 스레드 설정은 모듈 기술자의 접근 계약이 아니다.
- ③ 오답: 개발 도구 환경과 압축 설정은 모듈 이름·의존·공개 범위와 다른 정보다.
- ④ 오답: 데이터베이스 스키마는 Java 모듈 기술자가 정의하는 대상이 아니다.
4. 다른 모듈의 public 클래스를 사용하기 위한 관계를 제공자에서 사용자 순으로 바르게 배열한 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
- ① 오답: 공개와 의존 선언의 작성 주체를 서로 바꾸고 import 위치도 잘못 연결했다.
- ② 오답: import는 패키지를 공개하지 않고 exports는 사용하는 쪽의 의존 선언이 아니다.
- ③ 정답: 제공 범위, 모듈 읽기 관계, 소스 타입 연결의 세 조건을 순서대로 충족한다.
- ④ 오답: 제공자가 사용자를 의존하도록 방향을 뒤집었고 사용자가 제공 패키지를 exports할 수도 없다.
5. catalog.core의 catalog.internal.Helper가 public이지만 패키지는 exports되지 않았다. store.app에서의 판단은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
- ① 오답: public 접근 수준만 확인하고 모듈의 패키지 공개 경계를 빠뜨렸다.
- ② 정답: 다른 모듈에서 사용하려면 제공 모듈이 그 패키지를
exports해야 한다. - ③ 오답: 사용 프로젝트의 패키지 이름은 제공 모듈의 공개 선언을 대신하지 않는다.
- ④ 오답: import 횟수는 모듈 접근 권한을 바꾸지 않는다.
6. requires와 requires transitive의 결정적 차이는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
- ① 오답: 패키지 공개는
exports, 압축은 JAR 생성의 역할이다. - ② 오답: 두 선언은 모듈 종류가 아니라 의존성 전달 여부로 구분한다.
- ③ 오답: 어느 선언도 import나 모듈 기술자 자체를 자동으로 없애지 않는다.
- ④ 정답: transitive가 붙으면 현재 모듈을 읽는 모듈에도 해당 의존성의 읽기 관계가 전파된다.
7. C가 A를 requires하고, A가 B를 requires transitive한다. B가 b.api를 exports했다면 C의 읽기 관계는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
- ① 오답: C의 직접 requires가 A를 읽게 하고 A의 전이 선언이 B 관계를 전달한다.
- ② 오답: 전이 의존성은 직접 의존 대상 A를 제거하지 않는다.
- ③ 정답: 직접 화살표 C→A와 전달된 화살표 C→B를 함께 추적한 결과다.
- ④ 오답: 읽기 관계가 전달돼도 B에서 exports하지 않은 패키지는 공개되지 않는다.
8. “java.base는 requires를 생략하므로 java.util.List도 import 없이 쓸 수 있다”는 설명의 오류는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
- ① 정답:
java.base의존은 암묵적이지만 자동 import되는 대표 패키지는java.lang이다. - ② 오답: 잘못된 문장은 java.base의 다른 모듈 의존성을 주장하지 않았다.
- ③ 오답: 문장은 java.util을 java.base와 연결했으며 그 소속 자체가 오류의 원인은 아니다.
- ④ 오답: List의 종류가 아니라 requires와 import의 역할을 혼동한 것이 핵심 오류다.
9. 모듈 프로젝트에서 java.sql.Connection을 사용할 때 필요한 구성은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
- ① 오답:
java.base는 SQL 패키지를 제공하지 않고 사용 프로젝트가 표준 모듈을 exports할 수도 없다. - ② 정답: 모듈 읽기 관계와 소스 타입 연결을 각각 필요한 선언으로 작성한다.
- ③ 오답:
requires뒤에는 클래스가 아니라 모듈 이름이 온다. - ④ 오답: exports는 제공 모듈의 기술자에서 패키지를 공개하는 선언이지 Java 소스의 사용 선언이 아니다.
10. JAR 안에 public 클래스가 있지만 모듈 프로젝트에서 import되지 않을 때 가장 체계적인 점검 순서는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
- ① 오답: import 반복과 실행 우선순위·압축률은 모듈 접근 계약을 진단하는 순서가 아니다.
- ② 오답: 데이터베이스 연결은 일반적인 클래스 탐색 실패의 필수 점검 항목이 아니다.
- ③ 오답: 필요한 공개·의존·경로를 제거하면 접근 가능성이 더 낮아진다.
- ④ 정답: 물리적 파일에서 검색 경로, 모듈 공개·의존, 소스 이름 연결로 범위를 좁힌다.
참고 자료와 작성 기준
이 글은 해당 차시 강의자료를 바탕으로 학습 목적에 맞게 재구성한 비공식 학습자료입니다. JAR의 물리적 배포와 모듈의 논리적 접근 계약을 구분하고, 의존성 전달과 표준 모듈 사용 과정을 단계별로 검토했습니다.
- 작성·편집: 올에이클래스 학습연구팀
- 주요 근거: 한국방송통신대학교 Java프로그래밍 15강 「라이브러리와 모듈」 강의록 :codex-file-citation{path="K1. 컴퓨터과학과/1. 재검토필요/2-1 Java프로그래밍/1. 강의록/java프로그래밍 강의록 15강.pdf" purpose="source"}
- 편집 원칙: 올에이클래스 편집 정책
- 최종 내용 검토: 2026-09-02
댓글
댓글 쓰기