1. 기존 방식의 문제
“Classpath party”: 모두가 한 공간에 뒤섞인 상황
모듈이 등장하기 전(Java 9 이전)에는 애플리케이션 코드, 라이브러리, 의존성이 모두 하나의 큰 더미인 classpath에 섞였습니다. 라이브러리 사이의 경계가 사실상 없었죠. classpath에 들어오기만 하면 어떤 클래스든 누구나 찾고 사용할 수 있었습니다.
이름 충돌도 빈번했습니다. 두 라이브러리가 같은 완전한 이름을 가진 클래스를 포함하는 경우(예: com.example.Util) “어느 한쪽”이 골라져 버리고, 여러분은 런타임의 이상한 동작으로 문제를 뒤늦게 알게 됩니다.
장르의 고전 — dependency hell: 한 라이브러리는 로거 1.2 버전을, 다른 하나는 1.3을 요구하는데, classpath에는 둘 다 올라와 있는 경우입니다. JVM은 무엇을 집어올릴지 알 수 없고, 여러분은 수수께끼 같은 NoSuchMethodError를 마주치게 됩니다.
또 하나: 클래스가 public이라도 라이브러리 내부용으로 의도된 것이라면, 다른 코드가 그것을 여전히 사용할 수 있고 — 그 과정에서 뭔가를 망가뜨릴 수도 있습니다. 대규모 프로젝트는 지뢰밭이 되었고, 유지보수는 모험이 되곤 했습니다.
2. Java에서 모듈이란?
모듈: 구멍 난 상자에 비유
Java 9에는 모듈 시스템(JPMS)이 도입되었습니다. 모듈은 클래스와 패키지를 담은 상자이며, 여러분이 스스로 “구멍”을 냅니다. 즉, 바깥에 무엇을 보여줄지(exports)와 무엇을 내부에 숨길지 명시적으로 지정합니다. 그리고 맞습니다. 어떤 클래스가 public이라도, 그 패키지가 export되지 않았다면 다른 모듈에서는 보이지 않습니다.
형식적 정의
모듈은 관련된 패키지와 클래스를 묶는 코드 분할의 논리적 단위입니다. 각 모듈은 다음을 명시적으로 선언합니다:
- 무엇을 export하는지 — 다른 모듈에 공개하는 것(exports);
- 무엇을 import하는지 — 어떤 다른 모듈에 의존하는지(requires).
모듈의 핵심 속성:
- 명확한 경계. 무엇이 외부에 노출되는지, 무엇이 아닌지가 분명합니다.
- 명시적 의존성. 명시적인 requires 없이는 다른 모듈을 “볼” 수 없습니다.
- 격리. 내부 구현 세부는 외부 세계로부터 완전히 숨길 수 있습니다.
3. 모듈화의 이점
향상된 캡슐화
모듈 수준의 새로운 가시성이 생겼습니다. 클래스가 public이라도 그 패키지가 export되지 않으면 모듈 내부에서만 보입니다. 라이브러리 내부가 더 이상 밖으로 “새지” 않습니다.
의존성의 명시적 기술
필요한 의존성은 module-info.java에 나열합니다. 컴파일러와 IDE는 여러분이 의존하는 모듈을 빠뜨렸다면 미리 경고해 줍니다.
보안성과 신뢰성의 향상
내부 API에 대한 접근 제한은 우발적이거나 의도적인 개입을 어렵게 만듭니다. 이는 대형 라이브러리와 플랫폼 모듈에 특히 중요합니다.
“축소된” JRE 생성 가능
jlink를 사용하면 필요한 모듈로만 최소 런타임을 구성할 수 있습니다. 클라우드나 임베디드 시나리오에서 디스크 공간과 메모리를 절약합니다.
보너스: 시작 속도 향상과 크기 감소
모든 모듈을 로드하지 않고 실제로 사용하는 모듈만 로드하므로 애플리케이션이 더 빨리 시작하고 더 적은 자원을 사용합니다.
4. 모듈은 어디에 쓰이나
Java 표준 라이브러리에서
Java 9부터 플랫폼 자체가 모듈화되었습니다. 예:
- java.base — 기본 모듈(모든 프로그램에 필수).
- java.sql — 데이터베이스 작업.
- java.xml — XML 작업.
XML을 사용하지 않는다면, java.xml 모듈은 여러분의 런타임에 포함되지도 않습니다.
대규모 애플리케이션과 라이브러리에서
여러 팀이 모노레포의 각 부분을 개발하는 엔터프라이즈 시스템에서는 사실상 필수입니다. 모듈성은 충돌을 줄이고 유지보수를 단순화합니다.
개인 프로젝트에서
펫 프로젝트에서도 모듈은 아키텍처 사고를 훈련하고 코드를 정돈하는 데 도움이 되며, “스파게티”를 피하게 해 줍니다.
5. 문법 요약: module-info.java
가장 중요한 파일 — module-info.java
module-info.java 파일은 모듈 소스의 루트에 위치하며 그 경계와 의존성을 선언합니다.
module my.awesome.module {
exports com.example.api; // 내보내는 패키지
requires java.sql; // 표준 모듈에 대한 의존성
}
주요 키워드:
- module <이름> — 모듈을 선언합니다.
- exports <패키지> — 패키지를 다른 모듈에서 사용할 수 있게 합니다.
- requires <모듈> — 다른 모듈에 대한 의존성을 선언합니다.
최소 예시:
module com.myproject.core {
exports com.myproject.core.api;
}
의존성이 있는 예시:
module com.myproject.app {
requires com.myproject.core;
requires java.sql;
}
추가 기능(간단히): 리플렉션을 위해서는 opens, 서비스에는 uses와 provides ... with ...를 사용합니다(자세한 내용은 고급 강의에서 다룹니다).
6. 유용한 뉘앙스
모듈은 클래스의 “여권”과 같습니다. 예전에는 public 비자를 가진 클래스라면 애플리케이션 전역을 “여행”할 수 있었습니다. 이제는 모듈 “여권”, 즉 패키지 export(exports)도 필요합니다. 이것이 없으면 클래스는 “집”에 머뭅니다.
Dependency hell은 실제 용어입니다. ClassNotFoundException: com.google.common.base.Strings를 본 적이 있다면 이미 겪어본 것입니다. 모듈은 엄격한 경계와 의존성으로 이런 골칫거리를 몰아내기 위해 고안되었습니다.
이제 Java 전체가 모듈식입니다. 직접 모듈을 작성하지 않더라도 플랫폼은 이미 수십 개로 나뉘어 있습니다. 다음 명령을 실행해 보세요:
java --list-modules
개발에 어떤 영향이 있을까
여러분은 코드의 가시성을 명시적으로 관리할 수 있고, 내부 패키지가 “우연히” 모두에게 공개되는 일이 사라집니다. IDE와 컴파일러가 오류를 더 일찍 잡아냅니다. 의존성 선언을 잊으면 프로젝트는 빌드되지 않습니다. 대규모 시스템의 유지보수가 쉬워집니다. 누가 누구에게 의존하는지, 변경 시 무엇이 깨질지를 파악하기가 더 수월해집니다.
7. 모듈로 전환할 때 흔한 실수
오류 1: 패키지를 export하지 않았는데 클래스는 public인 경우. 클래스를 public으로 선언했지만 그 패키지를 export하지 않았다면 다른 모듈은 사용할 수 없습니다. 컴파일러는 “패키지가 모듈에서 export되지 않았습니다”라고 알려줄 것입니다.
오류 2: requires로 의존성을 선언하지 않은 경우. 다른 모듈의 타입을 사용하면서 requires를 module-info.java에 추가하지 않았다면, 결과는 “모듈이 필요한 클래스를 찾을 수 없습니다”라는 컴파일 오류입니다.
오류 3: 모듈 이름이 중복되는 경우. 대규모 프로젝트에서 두 모듈이 우연히 같은 이름을 갖는 문제가 생길 수 있습니다. JVM은 이를 허용하지 않습니다. 이름을 변경하고 일관된 네이밍 규칙을 지키세요.
오류 4: 표준 모듈을 잊은 경우. 예를 들어 JDBC를 사용하면서 requires java.sql;을 추가하지 않은 경우입니다. Java 8에서는 “그냥 보였지만”, 9+에서는 아닙니다.
오류 5: 다른 모듈의 내부 클래스를 사용하려는 경우. 패키지가 export되지 않았다면, public 클래스라도 외부에서는 보이지 않습니다. 패키지를 export하거나 API를 “public” 레이어로 분리하세요.
GO TO FULL VERSION