1. module-info.java란 무엇이며 어디에 있나요?
Java에서 모듈은 항상 파일 module-info.java로 시작합니다. 이는 모듈의 이름, 내보낼 패키지, 다른 모듈에 대한 의존성을 선언하는 일종의 “여권”입니다.
어디에서 찾을까요?
파일 module-info.java는 모듈 소스 폴더의 루트에 있어야 합니다. 예를 들어, 프로젝트 구조가 다음과 같다면:
project-root/
└── src/
└── my.module.name/
├── module-info.java
└── com/
└── example/
└── api/
└── MyClass.java
여기서 my.module.name은 모듈 이름입니다(명명 규칙은 뒤에서 다룹니다).
이 파일이 없으면 모듈로 인정되지 않습니다!
이 파일이 없다면, 폴더와 클래스가 아무리 많아도 그냥 “구식” Java 프로젝트일 뿐입니다.
2. module-info.java 문법: 핵심 요소
가장 단순한 파일 예시부터 바로 살펴보죠 — 전혀 어렵지 않습니다, 정말로!
module my.module.name {
exports com.example.api;
requires java.sql;
}
부분별로 살펴보기:
module my.module.name { ... }
이는 my.module.name이라는 이름의 모듈 선언입니다. 모듈 이름은 고유 식별자이며, 보통 루트 패키지와 일치합니다(예: com.example.app).
재미있는 사실: 모듈을 java.base라고 이름 붙이면 컴파일러가 좋아하지 않습니다. 표준 모듈을 바꾸려는 시도는 하지 마세요.
exports com.example.api;
이 줄의 의미는 “나는 com.example.api 패키지에 있는 내용을 공개하겠다”입니다. 해당 패키지 내부에서 public로 표시된 것은 다른 모듈에서도 볼 수 있습니다. 그 외는 철저히 내부 전용입니다.
requires java.sql;
여기서는 컴파일러에게 솔직히 말합니다: “작동하려면 표준 모듈 java.sql이 필요합니다.” 이것을 선언하지 않으면 그 모듈의 클래스를 사용할 수 없습니다.
추가 키워드(참고용)
- opens <package>; — 리플렉션을 위해 패키지를 개방합니다(예: Jackson 같은 직렬화 라이브러리).
- uses <service-interface>; — 모듈이 어떤 서비스(인터페이스)를 사용함을 나타냅니다.
- provides <service-interface> with <implementation-class>; — 모듈이 해당 서비스의 구현을 제공함을 알립니다.
이번 강의에서는 exports와 requires에 집중합니다 — 학습용과 실무 프로젝트의 대다수에서 이 둘이면 충분합니다.
3. 예시 module-info.java
예제 1. 최소 모듈
module com.example.hello {
exports com.example.hello.api;
}
- 이 모듈은 com.example.hello.api 패키지만 내보냅니다.
- 예를 들어 com.example.hello.internal에 있는 것들은, public 클래스가 있더라도 다른 모듈에서 보이지 않습니다.
예제 2. 의존성이 있는 모듈
module com.example.dbclient {
exports com.example.db.api;
requires java.sql;
}
우리는 JDBC를 사용할 수 있습니다. 단, java.sql에 대한 의존성을 정직하게 선언했기 때문입니다.
예제 3. 여러 패키지 내보내기
module com.example.library {
exports com.example.library.api;
exports com.example.library.utils;
}
원한다면 여러 패키지를 내보낼 수 있습니다(하지만 무분별하게 전부 내보내지는 마세요 — 그것이 바로 모듈의 존재 이유입니다!).
4. 제약과 규칙
모듈 이름
- 보통 루트 패키지와 일치합니다(예: com.example.app).
- 표준 모듈 이름(java.base, java.sql 등)과 같아서는 안 됩니다.
- 공백, 특수문자, 숫자로 시작하는 이름 등을 포함해서는 안 됩니다.
- 권장: 충돌을 피하려면 조직 또는 프로젝트의 역도메인 이름을 사용하세요.
모듈 하나당 module-info.java 는 하나
하나의 모듈에는 이러한 파일이 딱 하나만 있을 수 있습니다. 두 개 이상이면 컴파일러가 “모듈 스캔들”을 일으킬 겁니다.
하나의 패키지는 하나의 모듈에서만 export
동일한 패키지를 서로 다른 두 모듈에서 동시에 내보낼 수는 없습니다. 같은 이름의 여권을 두 개 갖는 것과 같아서, 국가는 용납하지 않습니다.
모듈 안의 패키지
실제로 이 모듈의 소스 구조에 존재하는 패키지만 내보낼 수 있습니다. 존재하지 않는 패키지를 내보내려 하면 컴파일 오류가 발생합니다.
5. 실습: 프로젝트에 module-info.java 만들기
다음과 같은 간단한 프로젝트가 있다고 가정해봅시다:
project-root/
└── src/
└── com.example.greetings/
├── module-info.java
└── com/
└── example/
└── greetings/
├── api/
│ └── Greeter.java
└── internal/
└── SecretSauce.java
1단계. module-info.java 생성
module com.example.greetings {
exports com.example.greetings.api;
}
2단계. 다른 모듈에서 internal 패키지의 클래스를 사용해 보기
두 번째 모듈 com.example.app이 있고, 이 모듈이 SecretSauce에 접근하려 한다고 해봅시다:
module com.example.app {
requires com.example.greetings;
}
import com.example.greetings.internal.SecretSauce; // 오류!
결과:
컴파일러는 “패키지 com.example.greetings.internal은 모듈 com.example.greetings에서 내보내지 않았다”고 말할 것입니다. 클래스 SecretSauce가 public이라 해도 다른 모듈에서는 접근할 수 없습니다.
이것이 바로 모듈 수준의 진정한 캡슐화입니다!
3단계. requires 를 선언하지 않으면?
com.example.app에서 requires com.example.greetings;를 작성하지 않고 com.example.greetings.api의 클래스를 사용하려 하면, 컴파일러는 다음과 같은 오류를 냅니다:
package com.example.greetings.api is not visible
6. module-info.java 작업 시 흔한 실수
오류 №1: 모듈 이름과 프로젝트 구조 불일치.
모듈을 com.example.app이라고 이름 붙였는데 폴더 구조가 src/main/java/app라면, 컴파일러는 무엇을 원하는지 이해하지 못합니다. 모듈 이름은 보통 루트 패키지와 일치하며, 폴더 구조도 이를 반영해야 합니다.
오류 №2: 무분별한 전체 export.
정말로 다른 모듈에서 보여야 하는 것만 export해야 합니다. 단지 “간단해서”라는 이유로 exports com.example;처럼 통째로 내보내지 마세요. 이는 캡슐화를 해칩니다.
오류 №3: requires 를 빼먹음.
다른 모듈 또는 표준 라이브러리(예: java.sql)의 클래스를 사용하면서 의존성을 선언하지 않으면 컴파일 오류가 발생합니다.
오류 №4: exports 에 존재하지 않는 패키지.
exports com.example.foo;라고 썼는데 그런 패키지가 없다면 — 컴파일러가 오류를 보고합니다.
오류 №5: export되지 않은 패키지의 public 클래스.
클래스가 public으로 선언되었더라도, 그 클래스가 export되지 않은 패키지에 있다면 해당 클래스는 모듈 내부에서만 보입니다. 오류는 아니지만 초보자에게는 종종 놀라운 지점입니다.
오류 №6: 하나의 모듈에 여러 module-info.java.
하나의 모듈에는 module-info.java 파일이 정확히 하나만 있어야 합니다. 두 개 이상이면 컴파일러가 프로젝트를 빌드하지 못합니다.
GO TO FULL VERSION