Java 9의 모듈 시스템(Java Platform Module System). 무엇을 공개하고 무엇에 의존하는지를 선언한다.
// module-info.java
module com.example.order {
requires com.example.payment; // 이 모듈에 의존한다
requires transitive java.sql; // 나를 쓰는 쪽에도 전파된다
exports com.example.order.api; // 이 패키지만 공개한다
opens com.example.order.entity; // 리플렉션만 허용 (JPA·Jackson 용)
}
무엇이 달라지나
public 의 의미가 바뀐다
기존 public = 클래스패스의 누구나 접근 가능
모듈 public + exports 된 패키지 = 접근 가능
exports 안 된 패키지의 public 은 밖에서 못 쓴다
- "내부 구현" 을 언어 차원에서 숨길 수 있게 됐다
- 그전에는 impl · internal 같은 패키지명 관례가 전부였다
해결하려던 문제
-
① 클래스패스 지옥
- 같은 클래스가 여러 jar 에 있으면 앞선 것이 이긴다 (조용히)
- 빠진 의존성은 실행 중 NoClassDefFoundError 로 드러난다
- 모듈은 시작 시점에 그래프를 검증한다
-
② 강한 캡슐화
- sun.misc.Unsafe 같은 내부 API 를 아무나 썼다
- JDK 를 못 바꾸게 만드는 족쇄가 됐다
-
③ 런타임 축소
- jlink 로 필요한 모듈만 담은 런타임 이미지를 만든다
- 컨테이너 이미지가 작아진다
실무에서의 현실
-
애플리케이션이 module-info 를 쓰는 비율은 낮다
- (Spring Boot 생태계는 대체로 클래스패스로 동작한다)
-
그러나 JDK 자체는 모듈화됐다
- 리플렉션으로 내부 API 를 건드리면 경고·차단이 난다
- InaccessibleObjectException / --add-opens 요구
- 리플렉션으로 내부 API 를 건드리면 경고·차단이 난다
-
Java 17 부터 강한 캡슐화가 기본이라 낡은 라이브러리가 여기서 깨진다
-
마이그레이션에서 만나는 대부분의 문제가 이 지점이다
자동 모듈
module-info 가 없는 jar 를 모듈 경로에 두면 jar 이름으로 자동 모듈이 만들어지고 모든 패키지가 exports 된다
- 점진적 이행을 위한 장치지만, 이름이 불안정해 배포에는 권장되지 않는다
마이그레이션에서 실제로 부딪히는 것
InaccessibleObjectException: Unable to make field ... accessible
- 원인 — 라이브러리가 리플렉션으로 JDK 내부에 접근한다
- 임시 — --add-opens java.base/java.lang=ALL-UNNAMED
- 근본 — 라이브러리를 최신으로 올린다
Java 16 까지는 경고만, 17 부터는 오류다 그래서 8/11 → 17 이행에서 이 오류가 몰려 나온다
split package 문제
같은 패키지가 두 모듈/jar 에 나뉘어 있으면 모듈 경로에서 거부된다 클래스패스에서는 조용히 동작하던 것이 오류가 된다
- 대표 사례: javax.annotation 이 여러 jar 에 흩어져 있던 상황
- Java EE 패키지들이 JDK 에서 분리되며 정리된 배경이기도 하다
jlink 로 얻는 것
필요한 모듈만 담은 런타임 이미지를 만든다
- JDK 전체 ~300MB → 필요한 것만 ~40MB
컨테이너 이미지 크기와 기동 시간에 직접 영향을 준다 다만 애플리케이션이 모듈화돼 있지 않으면 효과가 제한적이다
면접 함정
- ❌ "Java 9부터 모듈을 써야 한다" → 클래스패스로도 계속 동작한다.
- ❌ "exports와 opens는 같다" → opens는 리플렉션 전용 허용이다.