Java9 이전까지 순수하게 Java의 컴파일러와 JVM의 기능만으론 라이브러리 버전의 종속성이나 import할 Class 혹은 Package에 대한 정보를 명시 할 방법이 존재하지 않았다. Java9에 추가된 module기능은 완벽하다고 하긴 힘들지만 이를 가능하게 해주었다. 그러면 간단한 예제들과 함께 module에 대해 알아보자
Java9 이전까지 흔히 이야기 하는 캡슐화를 위한 범위는 간단하게 package와 class 그리고 class 내부의 private, public, protected가 전부였다.
Java9부터 추가된 module은 여러개의 패키지를 한번더 캡슐화 하여 package에 대한 접근권한을 부여하고 해당 module에서 외부에 노출할 패키지를 정의할 수 있게 되었다.
예를 들어 특정 Project에 com.sql이라는 패키지와 com.sql.communicate이라는 패키지가 존재하고 외부에서 해당 패키지에서 사용하는 함수나 클래스는 모두 com.sql에 존재한고 com.sql.communicate은 module내부에서만 사용하는 패키지라고 가정하면 module은 외부에 com.sql이라는 패키지만 노출하도록 정의할 수 있다는 이야기 이다.
간단한 module을 작성해보면서 확인해보도록 하자.
우선 main이 될 module을 생성해 보자. Java의 어플리케이션을 module로 구성하기 위해선 소스코드의 최상단 디렉토리에 module-info.java파일이 필요하다.
src/
├── com.hello
│ ├── com
│ │ └── hello
│ │ └── Main.java
│ └── module-info.java
우선 디렉토리 구조는 위와 같이 구성하고 진행해 보자. 기본적인 구조는 일반적인 자바프로젝트와 동일하고 최상단에 module-info.java가 추가되었다는 것만 차이점이다.
//com.hello/module-info.java
module com.hello {
}
module은 위 코드와 같이 정의할 수 있다. 이 예제에선 module의 이름은 가장 상위의 패키지명으로 사용하겠다.(전혀 다른 module명을 사용해도 된다)
package com.hello;
import com.module.World;
public class Main {
public static void main(String[] args) {
System.out.format("Hello %s!%n", World.world());
}
}
그리고 Main함수는 위와 같이 작성해주었다. 해당 함수는 com.module.World라는 class를 import한다. 그렇다면 이에 해당하는 module을 추가로 작성해보도록 하자.
src
└── com.module
├── com
│ └── module
│ └── World.java
└── module-info.java
import할 module은 위와 같이 작성해주었다.
package com.module;
public class World {
public static String world() {
return "world";
}
}
해당 module은 내부에 "world"라는 문자열을 return하는 함수를 갖고 있다. 그러면 해당 module은 외부로 접근이 가능한 패키지를 갖는 module로 작성을 해야할 것이다.
module com.module {
exports com.module;
}
해당 예제에선 패키지가 하나뿐이라 외부로 노출할 패키지는 하나뿐이다. com.module을 노출시켜야 한다. 이렇게 노출하기 원하는 경우 exports {패키지명}을 module-info.java 내부에 정의해주면 된다.
그러면 이제 컴파일을 해보자.
$ javac -d mods\com.module --module-path .\mods\ .\src\com.module\com\module\World.java .\src\com.module\module-info.java
$ javac -d mods\com.hello --module-path .\mods\ .\src\com.hello\com\hello\Main.java .\src\com.hello\module-info.java
컴파일할 targer의 폴더는 mods이고 mods내부에 각 패키지명의 폴더에 컴파일을 해주었다. 여기서 module-path라는 옵션은 컴파일에 참조할 module이 위치하는 디렉토리 이다. 하지만 이렇게 진행하면 다음과 같은 에러가 발생한다.
.\src\com.hello\com\hello\Main.java:3: error: package com.module is not visible
import com.module.World; // import
^
(package com.module is declared in module com.module, but module com.hello does not read it)
1 error
com.module이 정의가 되어있지만 com.hello에서 읽지못한다고 한다. 이게 무슨말인가? Java의 module은 노출하는 쪽과 사용할 쪽 모두에게 정의를 해주어야 한다. 그러면 com.hello의 module-info.java를 다음과 같이 수정하고 다시 컴파일 해보자
//com.hello/module-info.java
module com.hello {
requires com.module;
}
이제 컴파일이 정상적으로 진행될 것이다. 특정 module에서 exports한 패키지를 사용하려면 사용하는 쪽의 module에서도 requires 로 명시해주어야 한다. 여기서 확인할 수 있는 것은 module의 의족관계는 컴파일 타임에 확인이 가능하다는 것이다. 물론 런타임에서도 체크한다.
$ java --module-path mods -m com.hello/com.hello.Main
Hello world!
그러면 위와같이 정상적으로 실행이 되는 것을 확인할 수 있다.
그러면 module은 어떤 식으로 구성이 되어있는 것일까? 우선 컴파일된 class파일을 확인해보자
mods/
├── com.hello
│ ├── com
│ │ └── hello
│ │ └── Main.class
│ └── module-info.class
└── com.module
├── com
│ └── module
│ └── World.class
└── module-info.class
컴파일 된 내용도 기존 Java의 class파일들과 크게 달라보이는 부분은 없어보인다. 최상단에 module-info.class이 존재할 뿐이다. 그렇다면 해당 파일을 살펴보도록 하다.
$ javap .\mods\com.hello\module-info.class
Compiled from "module-info.java"
module com.hello {
requires java.base;
requires com.module;
}
여기에 한가지 우리가 추가하지 않았던 java.base라는 패키지가 있는 것을 볼수있다. Java9이후에도 여전히 기본적으로 import가 가능한 패키지들이 있다. java.util이나 java,lang 같은 패키지들은 특별한 작업을 하지 않아도 사용이 가능한데 이들이 포함된 module이 java.base이다. java.base는 명시를 해주지 않더라고 무조건 추가된다. java.base에 포함된 패키지들은 해당 링크에서 확인할 수 있다.
Java9로 넘어오면서 JDK에서 기본적으로 제공하는 패키지들도 module로 세분화 되었는데 java.base는 Java를 사용하는데 필수적인 패키지들만 모아둔 module이라고 생각하면 된다. 그리고 module로 분할되면서 우리는 JDK에서 필요한 모듈만을 담아 어플리케이션을 릴리즈 하는 것이 가능해진 것이다.
그러면 위 예제를 기준으로 필요한 모듈만을 담아 배포를 해보자.
$ jlink --module-path .\mods\ --output myjre --add-modules com.hello
위와같은 명령어를 사용하면 내가 만든 module과 이 module을 실행하기 위해 필요한 모듈만을 담아서 jre환경을 만들어준다.
$ .\myjre\bin\java -m com.hello/com.hello.Main
Hello world!
다음과 같이 실행이 되는 것을 확인할 수 있다. 해당 jre폴더의 release라는 폴더에 들어가 보면
JAVA_VERSION="11.0.11"
MODULES="java.base com.module com.hello"
위와 같이 적혀있는 것을 볼수있는데 해당 jre는 위의 3개의 모듈만 포함되어 있다는 것을 알 수있다. 용량을 확인해보면 40MB정도로 모든 JDK의 모듈을 포함하여 릴리즈한 200MB정도 보다 훨씬 줄어든 것을 확인할 수 있다.
java의 module에 관해서 간략하게 알아보았다. module기능은 java에서 패키지 단위의 캡슐화를 가능하게 해주었고 module간의 의존관계를 정의하여 최적화 된 어플리케이션 배포를 가능하게 해주었다.