Java 9의 새로운 기능 - 모듈(1)
1. 개요
Java 9는 공식적으로 Java Platform Module System (JPMS) 또는 줄여서 '모듈'이라고 알려진 새로운 수준의 추상화를 도입한다. 모듈에는 종속성(dependency)의 개념이 있으며, Public API를 내보내고 구현 세부 정보를 숨김/비공개 상태로 유지할 수 있다.
모듈형 시스템을 고안하게된 주된 동기 중 하나는, 가용 메모리가 훨씬 적은 장치에서 실행할 수 있는 모듈형 JVM을 제공하는 것이었다. JVM은 애플리케이션에 필요한 모듈 및 API로만 실행될 수 있다.
2. 모듈이란?
우선, 모듈의 사용법을 이해하기 전에 모듈이 무엇인지 이해해야 한다. 모듈이란 서로 밀접하게 연관된 패키지들과 리소스들의 그룹이다. 즉, "자바 패키지들의 패키지(package of Java Packages)"라고도 볼 수 있는데, 이는 코드의 재사용성을 높이기 위해 추상화 단계를 하나 더 추가한 것이라고 볼 수 있다.
2.1 패키지
모듈 내의 패키지들은 Java가 시작된 이후 사용해 온 Java 패키지와 동일하다. 모듈을 만들 때, 우리는 이전에 다른 프로젝트에서 했던 것처럼 똑같이 패키지를 구성한다. 코드 구성 외에도 패키지는 모듈 외부에서 공개적으로 접근할 수 있는 코드를 결정하는 데 사용된다. 이 부분에 대해서는 이 글의 뒷부분에서 추가적으로 설명하도록 하겠다.
2.2 리소스
각 모듈은 미디어 또는 configuration 파일과 같은 리소스들을 담당한다. 이전에는 모든 리소스를 프로젝트의 루트 레벨에 넣고 애플리케이션의 각기 다른 부분들에 속한 리소스들을 수동으로 관리했다. 모듈을 사용하면, 모듈과 함께 필요한 이미지와 XML 파일 등을 함께 모아놓을 수 있으므로, 프로젝트를 훨씬 쉽게 관리할 수 있다.
2.3 모듈 설명자(Module Descriptor)
모듈을 만들 때, 모듈의 여러 가지 설정을 정의하는 설명자 파일(module-info.java)을 추가한다. 설정값들에는 다음과 같은 것들이 있다.
- 이름 : 모듈 이름
- 종속성 : 이 모듈이 의존하는 다른 모듈 목록
- 공용 패키지 : 모듈 외부에서 접근할 수 있는 모든 패키지 목록
- 제공되는 서비스 : 다른 모듈에서 사용할 수 있는 서비스 구현을 제공할 수 있다. (Service Provider)
- 사용된 서비스 : 현재 모듈이 서비스의 소비자가 될 수 있도록 한다. (Service Consumer)
- 리플렉션 허용 : 다른 클래스가 리플렉션을 사용하여 패키지의 private 멤버에 접근할 수 있는지의 여부를 명시
모듈 네이밍 규칙은 패키지의 이름을 지정하는 방법과 유사하다(도트는 허용되고 대시는 허용되지 않음). 프로젝트 스타일(my.module) 또는 리버스 DNS(com.baeldung.mymodule) 스타일 이름을 사용하는 것은 매우 일반적이다.
디폴트로 모든 패키지는 모듈 전용(module private)이므로 공개하려는 모든 패키지들은 명시적으로 나열되어야 한다. 리플랙션 또한 마찬가지로, 디폴트로 다른 모듈에서 모듈의 클래스에 대한 리플랙션을 사용할 수 없다.
2.4 모듈 타입
새 모듈 시스템에는 4가지 유형의 모듈이 있다.
- 시스템 모듈(System Modules) : list-modules 명령어를 입력했을 때 조회되는 모듈들이다. 여기에는 Java SE 및 JDK 모듈이 포함된다.
- 애플리케이션 모듈(Application Modules) : 이 모듈은 일반적으로 우리가 모듈을 사용하기로 결정했을 때 빌드하고자 하는 모듈이다. 이들은 JAR에 포함된 컴파일된 module-info.class 파일에 명명되고 정의된다.
- 자동 모듈(Automatic Modules) : 기존 JAR 파일을 모듈 경로(module path)에 추가하여 비공식 모듈을 포함시킬 수 있다. 모듈 이름은 JAR의 이름에서 파생된다. 자동 모듈은 모듈 경로에 로드된 다른 모든 모듈들에 대한 전체 읽기 액세스 권한을 가지게 된다.
- 이름 없는 모듈(Unnamed Module) : 클래스 또는 JAR가 모듈 경로(module path)가 아닌 클래스 경로(classpath)에 로드되면, 이름이 지정되지 않은 모듈에 자동으로 추가된다. 이것은 이전에 작성된 Java 코드와의 하위 호환성을 유지하기 위함이다.
2.5 배포
모듈은 JAR 파일 또는 컴파일된 여러 파일들로 이루어진 프로젝트 두 가지 방법 중 하나로 배포될 수 있다. 물론 이는 다른 Java 프로젝트와 동일한 방법이다.
우리는 "주 애플리케이션"과 여러 라이브러리 모듈로 구성된 다중 모듈 프로젝트를 만들 수 있다. 이때 JAR 파일당 모듈을 하나만 가질 수 있기 때문에 주의해야 한다. 빌드 파일을 설정할 때 프로젝트의 각 모듈을 별도의 JAR로 묶어야 한다.
3. 디폴트 모듈
Java 9를 설치하면 JDK가 새로운 구조를 가지고 있음을 알 수 있다. 원래 패키지들이 모두 새 모듈 시스템으로 옮겨졌다. 커멘드 라인에 아래의 명령어를 입력하면 이러한 모듈들이 무엇인지 알 수 있다.
java --list-modules
이러한 모듈들은 Java, javafx, jdk 및 Oracle의 네 가지 주요 그룹으로 나뉜다.
java 모듈은 핵심 Java SE 언어에 대한 구현 클래스이다.
javafx 모듈은 FX UI 라이브러리이다.
JDK 자체에 필요한 모든 것은 JDK 모듈에 보관된다.
마지막으로, 오라클에 특화된(Oracle-specific) 것들은 오라클 모듈에 있다.
4. 모듈 선언
모듈을 설정하려면 module-info.java라는 모듈 설명자 파일을 패키지 루트 레벨에 생성해야 한다. 이 파일에는 모듈을 생성하는데 필요한 모든 데이터가 들어있다. 어떤 설정값도 없는 빈 모듈 설명자 파일을 만들어보자.
module myModuleName {
// 설정값들은 모두 Optional
}
'module' 키워드로 모듈 선언을 시작하고, 그다음 모듈 이름을 쓴다. 이러한 선언만으로도 모듈은 동작하지만, 일반적으로 더 많은 정보(설정값들)가 필요하다.
4.1 Requires
첫 번째 키워드는 'requires'이다. 이 키워드는 이 모듈의 종속성을 나타낸다.
module my.module {
requires module.name;
}
이제 'my.module'은 'module.name'에 대한 종속성을 갖게 된다(런타임, 컴파일 타임 둘 다). 이제 'my.module'는 'module.name'에서 외부로 공개(exports)하기로 한 모든 public type들에 대한 접근이 가능하다.
4.2 Requires Static
때때로 우리는 다른 모듈을 참조하는 코드를 작성하지만, 우리 라이브러리의 사용자는 결코 사용을 원하지 않을 수 있다. 예를 들어 다른 로깅 모듈을 사용하여, 내부 상태를 예쁘게 나타내는 유틸리티 함수를 작성할 수 있다. 그러나 우리 라이브러리의 모든 소비자들이 이 기능을 원하는 것은 아니며 추가 로깅 라이브러리가 포함되기를 원치 않을 수 있다.
이러한 경우 우리는 선택적 종속성(optional dependency)을 사용할 수 있다. 'requires static' 키워드를 사용하여 오직 컴파일 타임에 대한 종속성(compile-time-only dependency)을 갖게 할 수 있다.
module my.module {
requires static module.name;
}
4.3 Requires Transitive
우리는 어떤 모듈을 우리의 코드에서 사용하려 할 때, 그 모듈이 다른 전이되는 종속성(transitive dependency)을 필요로 할 수 있다는 것을 알아야 한다. 이 종속성을 추가하지 않을 경우, 코드는 정상 동작하지 않을 것이다. 이때 사용할 수 있는 키워드가 'requires transitive'인데, 이를 사용하면 우리 모듈의 사용자들이 전이된 종속성에 대한 접근이 가능하도록 강제할 수 있다.
module my.module {
requires transitive module.name;
}
이제 어떤 개발자가 자신의 모듈에 'requires my.module'을 추가한다면, 'requires module.name'은 추가하지 않아도 이에 대한 접근이 가능하다.
4.4 Exports
디폴트로, 모듈은 자신의 API를 다른 모듈에 노출시키지 않는다. 이러한 강력한 캡슐화가 모듈 시스템이 만들어진 주요 동기 중 하나였다. API가 사용될 수 있게 하려면 명시적으로 공개 범위를 나타내야 한다. 모듈 내의 모든 public 멤버들을 외부로 노출시키기 위해 'exports' 키워드를 사용한다.
module my.module {
exports com.my.package.name;
}
이제 어떤 개발자가 자신의 모듈에 'requires my.module'을 추가한다면, com.my.package.name 패키지 내의 모든 public 타입에 접근할 수 있다. 하지만 다른 패키지에는 접근할 수 없다.
4.5. Exports … To
특정 모듈에만 API를 노출시키고 싶을 수 있다. 'exports…to' 키워드를 이용하여 모듈의 API를 특정 모듈에만 공개하도록 설정할 수 있다.
module my.module {
export com.my.package.name to com.specific.package;
}
4.6. Uses
서비스란 다른 클래스에서 사용할 수 있는 특정 인터페이스 또는 추상 클래스의 구현을 말한다. 'uses' 키워드를 통해 모듈이 사용할 서비스를 지정할 수 있다. 이때 주의할 점은, 'uses' 키워드 뒤에 추가될 것은 구현 클래스가 아니라 서비스의 인터페이스 혹은 추상 클래스여야 한다는 것이다.
module my.module {
uses class.name.MyInterface;
}
여기서 우리는 'requires'와 'uses'의 차이점에 대해 유의해야 한다.
요구된(requires) 모듈의 경우, 우리가 사용할 서비스가 전이된 종속성(transitive dependencies)으로부터 구현된 것일 수도 있다. 이 경우, 모든 전이된 종속성을 모듈에 추가해야 한다. 그렇게 하는 대신, 'uses' 키워드를 이용하여 인터페이스나 추상 클래스만 모듈 경로(module path)에 추가할 수 있다.
4.7. Provides … With
모듈은 다른 모듈이 소비(uses)할 수 있는 서비스 공급자가 될 수 있다. 'provides' 키워드 뒤에는 인터페이스 또는 추상 클래스 이름을 넣는다. 그리고 'with' 키워드 뒤에는 인터페이스를 구현하거나 추상 클래스를 확장한 구현 클래스 이름을 넣는다.
module my.module {
provides class.name.MyInterface with class.name.MyInterfaceImpl;
}
4.8. Open
앞에서 캡슐화가 모듈 시스템이 생기게 된 주된 원동력이라고 언급했다. 자바 9 이전에는 리플렉션을 사용하여 패키지의 모든 종류와 멤버들에 접근하는 것이 가능했으며, 심지어 private 멤버에도 접근이 가능했다. 실질적으로 캡슐화된 것은 아무것도 없었으며, 이는 많은 문제를 야기시킬 수 있었다.
Java 9에서는 강력한 캡슐화를 강제하므로, 우리는 다른 모듈들이 우리의 클래스에 대한 리플랙션을 사용할 수 있는지의 여부를 명시적으로 표현해야 한다. 만약 이전 버전의 Java처럼 리플렉션을 모두 허용하려면 전체 모듈을 열면(open) 된다.
open module my.module {
}
4.9 Opens
만약 특정 패키지에 대해서만 리플렉션을 허용하고 싶을 경우, 'opens' 키워드를 아래와 같이 사용한다.
module my.module {
opens com.my.package;
}
4.10. Opens … To
만약 특정 패키지에 대한 리플랙션을 특정 모듈들에 대해서만 허용하고 싶을 경우, 아래와 같이 모듈들의 목록을 쓰면 된다.
module my.module {
opens com.my.package to moduleOne, moduleTwo, etc.;
}
5. 커멘드 라인 옵션
Java 9 모듈 시스템에 대한 지원이 Maven과 Gradle에 추가되었으므로, 사실 프로젝트를 수동으로 빌드할 일은 없을 것이다. 그러나 전체 시스템이 어떻게 동작하는지 정확히 이해하기 위해서, 커멘드 라인 옵션에 대한 이해는 필요하다.
1) module-path : -module-path 옵션을 사용하여 모듈 경로를 지정합니다. 모듈이 포함된 하나 이상의 디렉터리 목록이다.
2) add-reads : 모듈 설명자 파일(module-info.java)에 의존하지 않고, 커멘드 라인을 통해 'requires'와 동일한 역할을 할 수 있다. : -add-reads
3) add-exports : 모듈 설명자 파일(module-info.java)에 의존하지 않고, 커멘드 라인을 통해 'exports'와 동일한 역할을 할 수 있다.
4) add-opens : 모듈 설명자 파일(module-info.java)에 'open' 절을 대체한다.
5) add-modules : 디폴트 모듈 집합에 모듈 목록을 추가한다.
6) list-modules : 모든 모듈 및 해당 버전 문자열 목록을 보여준다.
7) patch-module : 모듈을 추가하거나 오버라이드 한다.
8) illegal-access=permit|warn|deny : 하나의 글로벌 경고문만을 보여주거나(permit) 혹은 모든 경고문을 보여주면서(warn) 강력한 캡슐화(리플랙션에 대한 제한)를 완화시킬 수 있다. deny의 경우 illegal-access에 대해 에러를 발생시킨다. 디폴트는 permit이다.
6. 가시성
가시성, 혹은 캡슐화에 대한 얘기를 좀 더 해보자. 많은 라이브러리들(JUnit and Spring 등)이 리플랙션에 의존한다. 그러나 Java 9에서는 기본적으로 리플랙션을 사용하여 private 요소들에 접근하고 setAccessible(true)을 호출하더라도 이것들에 대해 접근할 수 없다. 대신 우리는 'open, opens, and opens…to' 를 이용하여 리플렉션에 대한 권한을 런타임 전용으로 부여할 수 있다.
만약 우리가 특정 모듈에 대한 리플랙션을 사용해야 하는데 그 모듈의 소유자가 아니라면, 커멘드 라인 -add-opens 옵션을 사용하여 리플랙션이 허용되도록 할 수 있다. 여기서 주의해야 할 유일한 점은 모듈을 실행하는 데 사용되는 커멘드 라인 인수에 접근할 수 있어야 한다는 것이다.
7. 모듈을 Unnamed Module에 추가하기
Unnamed Module은 디폴트 패키지와 유사한 개념을 갖는다. 따라서 진짜 모듈이라기보단, 디폴트 모듈로 간주된다.
만약 어떤 클래스가 named module의 멤버가 아닐 경우, 그것은 자동으로 Unnamed Module의 일부로 간주된다.
때때로 특정 플랫폼, 라이브러리 또는 서비스 프로바이더 모듈의 동작을 위해 모듈들을 디폴트 루트 셋에 추가해야할 때가 있다. 예를 들면, Java 9 컴파일러로 Java 8 프로그램을 그대로 실행하려고 할 때 우리는 모듈을 추가해야할 수 있다.
일반적으로 named module을 디폴트 루트 셋에 추가하는 옵션은 '-add-modules <module>' 이다(<module>은 모듈명이다.). 예를 들어 모든 java.xml.bind 모듈에 대한 접근을 제공해야한다고 가정할 때, 아래와 같이 커멘드 라인 옵션을 추가할 수 있다.
--add-modules java.xml.bind
메이븐을 사용할 경우, 아래와 같이 플러그인을 사용할 수 있다.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>9</source>
<target>9</target>
<compilerArgs>
<arg>--add-modules</arg>
<arg>java.xml.bind</arg>
</compilerArgs>
</configuration>
</plugin>
※ 참조