Java 9 와 Project Jigsaw 소개 2

의역, 오역, 직역이 있을 수 있음을 알려드립니다.
이 포스트는 원저자의 동의를 얻어 한글로 번역한 내용입니다.

This post is a translation of this original article [https://blog.codecentric.de/en/2015/11/first-steps-with-java9-jigsaw-part-2] by Florian Troßbach from codecentric
Author: Florian Troßbach

참고 자료
http://openjdk.java.net/projects/jigsaw/spec/sotms/
http://openjdk.java.net/projects/jigsaw/
http://openjdk.java.net/projects/jdk9/
http://greatkim91.tistory.com/197
Modularity in Java 9
JDK 9 Early Access with Project Jigsaw
JEP

JDK9 Schedule
2016/05/26 Feature Complete
2016/12/22 Feature Extension Complete
2017/01/05 Rampdown Start
2017/02/09 All Tests Run
2017/02/16 Zero Bug Bounce
2017/03/16 Rampdown Phase 2
2017/07/06 Final Release Candidate
2017/07/27 General Availability


이 포스트는 Project Jigsaw 시작을 돕는 시리즈 중에 두번째 파트 입니다. 첫번째 파트에서는 Module의 정의와 Java Runtime이 어떻게 모듈화 되는지에 대해 간략히 이야기했습니다. 그런 다음 modular application을 컴파일(또는 안되게), 패키징, 실행하는 등의 방법을 보여주는 간단한 예제로 진행했습니다.

이번 포스트에서 우리는 아래의 질문에 답하려고 합니다.

  • export된 패키지에 대한 접근(읽기)을 제한 할 수 있습니까?
  • 다른 버전들의 Module을 modulepath에 적용 할 수 있습니까?
  • Jigsaw에서 기존 모듈화되지 않은 레거시 코드를 어떻게 사용 할 수 있습니까?
  • 내 자신만의 Java Runtime 이미지를 어떻게 작성 할 수 있습니까?

우리는 첫번째 파트의 예제를 기본으로 삼아 계속해서 진행할 예정입니다. 코드는 여기에서 사용할 수 있습니다.

특정 모듈에 대한 접근(읽기) 제한

첫번째 파트에서 Jigsaw가 Java 접근성(accessiblity)을 어떻게 확장 시킬수 있는지 얘기 했습니다. 첫번째 파트에서 언급된 정교하지 않은 접근성 레벨 중 하나는 “특정 모듈에만 Public“ 입니다. 이 경우 우리는 export된 패키지를 읽을 수 있는 모듈을 제한 할 수 있습니다. 따라서 de.codecentric.zipvalidator의 개발자가 de.codecentric.nastymodule을 개발한 팀을 싫어 한다면 module-info.java를 다음과 같이 변경할 수 있습니다.

1
2
3
4
module de.codecentric.zipvalidator{
exports de.codecentric.zipvalidator.api
to de.codecentric.addresschecker;
}

이렇게 하면 de.codecentric.addresschecker만 zipvalidator API를 액세스 할 수 있습니다. 하지만 접근 제한은 패키지 수준에서 이뤄지기 때문에 일부 패키지의 액세스를 완벽하게 제한 할 수 있지만, 제한 하지 않는 다른 패키지는 모든 권한을 허용합니다. 이것이 Qualified export 입니다.

de.codecentric.nastymodulede.codecentric.zipvalidator.api의 모든 타입에 액세스 하려고 하면 아래와 같이 컴파일 오류가 발생합니다.

1
2
3
./de.cc.nastymodule/de/cc/nastymodule/internal/AddressCheckerImpl.java:4:
error: ZipCodeValidatorFactory is not visible
because package de.cc.zipvalidator.api is not visible

zipvalidator는 실제 nastymodule에 보여지는 package(visible package)를 따로 export할 수 있기 때문에 module-info.java는 아무런 문제가 없습니다. 그리고 Qualified export는 application을 외부 client와 공유하지 않고 내부적으로 모듈화하려는 경우에 사용할 수 있습니다.

모듈 버전 충돌

일반적인 버전 충돌 시나리오 중 하나는 추이 종속성 (transitive dependencies)을 통해 동일한 애플리케이션에서 서로 다른 버전의 라이브러리를 갖게되어 결국 모듈이 modulepath에 두번 있게 되는 상황입니다.

아래의 두가지 시나리오를 생각해 보겠습니다.

  • 동일한 이름을 사용하는 모듈이 다른 폴더 또는 다른 modular jar에 있고 컴파일 타임에 사용하는 경우.
  • 모듈의 버전별로 서로 다른 이름을 가지게 하는 경우.

zipvalidator를 복사하여 첫번째 시나리오에 따라 컴파일 해봅시다.
복사한 애플리케이션은 아래와 같은 구조를 가집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
two-modules-multiple-versions
├── de.codecentric.addresschecker
│ ├── de
│ │ └── codecentric
│ │ └── addresschecker
│ │ ├── api
│ │ │ ├── AddressChecker.java
│ │ │ └── Run.java
│ │ └── internal
│ │ └── AddressCheckerImpl.java
│ └── module-info.java
├── de.codecentric.zipvalidator.v1
│ ├── de
│ │ └── codecentric
│ │ └── zipvalidator
│ │ ├── api
│ │ │ ├── ZipCodeValidator.java
│ │ │ └── ZipCodeValidatorFactory.java
│ │ ├── internal
│ │ │ └── ZipCodeValidatorImpl.java
│ │ └── model
│ └── module-info.java
├── de.codecentric.zipvalidator.v2
│ ├── de
│ │ └── codecentric
│ │ └── zipvalidator
│ │ ├── api
│ │ │ ├── ZipCodeValidator.java
│ │ │ └── ZipCodeValidatorFactory.java
│ │ ├── internal
│ │ │ └── ZipCodeValidatorImpl.java
│ │ └── model
│ └── module-info.java

중복된 모듈을 다른 폴더에 넣었습니다. 하지만 모듈 이름을 바꾸지는 않았습니다. 그럼 컴파일 할때 Jigsaw가 어떤 메시지를 보여줄까요?

1
2
./de.codecentric.zipvalidator.v2/module-info.java:1:
error: duplicate module: de.codecentric.zipvalidator

좋습니다. 결과를 확인 했습니다. Jigsaw는 컴파일 할때 modulepath에 있는 두 모듈이 같은 이름을 가지면 컴파일 할때 에러를 발생 시킵니다.

두번째 경우는 어떨까요?
Directory 구조는 그대로 둔 상태에서 두개의 zipvalidator를 서로 다른 이름 (de.codecentric.zipvalidator.v{1|2})으로 변경하고, addresschecker에서 아래의 코드와 같이 두개의 모듈을 모두 읽도록 합니다.

1
2
3
4
5
module de.codecentric.addresschecker{
exports de.codecentric.addresschecker.api;
requires de.codecentric.zipvalidator.v1;
requires de.codecentric.zipvalidator.v2;
}

이건 확실히 컴파일이 될까요? 동일한 패키지를 내보내는 두 개의 모듈을 읽을 수 있을까요?

네. 확실히 가능합니다. 컴파일러는 이상황을 인정(허용)하지만 아래와 같이 경고를 보냅니다.

1
2
./de.cc.zipvalidator.v1/de/codecentric/zipvalidator/api/ZipCodeValidator.java:1:
warning: package exists in another module: de.codecentric.zipvalidator.v2

Jigsaw는 이런 상황을 좋아하지 않지만, 만약 개발자가 위의 메시지를 봤음에도 불구하고 애플리케이션을 실행시키면 Runtime에 아래와 같은 메시지를 보냅니다.

1
2
3
java.lang.module.ResolutionException:
Modules de.codecentric.zipvalidator.v2 and de.codecentric.zipvalidator.v1 export
package de.codecentric.zipvalidator.api to module de.codecentric.addresschecker

개인적으로 이런 방식은 직관적이지 않기 때문에 컴파일시 오류가 더 좋을 수도 있다고 생각합니다. 필자는 메일 링리스트로 이러한 선택의 동기에 대해 질문했지만 글을 쓸 때 까지 아직 답변을 받지 못했습니다.

Automatic moduleUnnamed module

지금까지 우리는 모두 모듈로 구성된 환경에서 작업해 왔습니다. 그러나 모듈화되지 않은 Jar 파일을 사용해야하는 경우는 어떨까요? 이제 우리는 Automatic moduleUnnamed module을 이용해서 작업해 보도록 하겠습니다.

Automatic module

Automatic module부터 시작해보도록 하겠습니다. Automatic module은 modulepath에 있는 jar 파일입니다. 일단 modulepath에 jar를 추가할려면 다음 세 가지 질문에 답을 해야합니다.

Q: 이름은 어떻게 되나요? (name)

A: jar 파일의 이름입니다. 만약 guava.jar를 modulepath에 추가한다면 guava라는 Automatic module이 생깁니다. 이것은 또한 guava-18.0이 유효한 Java 식별자가 아니기 때문에 Maven 저장소에서 바로 Jar을 사용할 수 없다는 것을 의미합니다.

Q: 어떤것을 제공하게 되나요? (export)

A: automatic module은 모든 패키지를 export 합니다. 그렇기 때문에 모든 public type은 Automatic module을 이용하는 모든 모듈에서 사용할 수 있습니다.

Q: 어떤것이 필요하나요? (require)

A: Automatic module은 사용 가능한 다른 모든 모듈 (다음에서 설명할 Unnamed module 포함 해서)을 액세스 할수 있습니다. 이것은 매우 중요한 내용입니다.! 어느 곳에서도 따로 지정할 필요없이 암묵적(imply)으로 Automatic module에서는 다른 모듈이 export한 모든 타입에 액세스 할 수 있습니다.

예를 들어 보겠습니다. 우리의 zipvalidator에서 com.google.common.base.Strings를 사용하기 시작했습니다. 이 액세스를 허용하려면 아래의 예제와 같이 Guava의 Automatic module에 대한 읽기 접점(read edge)을 정의해야합니다.

1
2
3
4
5
module de.codecentric.zipvalidator{
exports de.codecentric.zipvalidator.api;
requires public de.codecentric.zipvalidator.model;
requires guava;
}

컴파일을 하려면 guava.jar을 아래와 같이 modulepath에 추가해야 합니다. (./jars 에 있다고 가정합니다.)

1
javac -d . -modulepath ../jars -modulesourcepath . $(find . -name "*.java")

이 예제는 잘 컴파일 되고 실행이 됩니다.

(기록을 위해 남깁니다. 이 예제를 잘 동작 하기까지 쉽지 않았습니다. Jigsaw 빌드 86을 사용하면 몇 가지 문제가 발생합니다. jdk.management.resource라는 모듈에 대한 종속성에 문제가 있다고 나왔기 때문입니다. mailing list에 질문을 남겼고 내용은 여기에서 확인 하실수 있습니다.
기본적인 해결책은 초기 버전을 사용하지 않고 직접 JDK를 빌드해서 사용하는 것이었습니다. 위의 메일 스레드에서 볼 수 있듯이 OSX Mavericks에서 더 많은 문제가 있었고, makefile을 변경해서 정상 작동하도록 했습니다.
하지만 여러분은 이후 Release 버전에 따라 다를수 있습니다.)

Jigsaw로 전환할때 가장 유요한 도구를 소개하도록 하겠습니다.
모듈화되지 않은 코드를 살펴보고 종속성에 대해 알려주는 jdeps라는 도구가 있습니다. guava.jar를 살펴보도록 하겠습니다.

1
jdeps -s ../jars/guava.jar

아래는 위 명령어를 실행했을 때의 결과입니다.

1
2
3
guava.jar -> java.base
guava.jar -> java.logging
guava.jar -> not found

이 결과는 Automatic guava 모듈은 java.base, java.logging이 필요하고… “찾을수 없다”고?.
이건 무었이죠? jdeps-s 옵션을 추가하여 모듈 레벨에서 패키지 레벨 관점으로 이동해서 살펴보도록 하겠습니다. (guava에는 많은 패키지가 있지만 짧게 표시합니다.)

1
2
3
4
com.google.common.xml (guava.jar)
-> com.google.common.escape guava.jar
-> java.lang
-> javax.annotation not found

위 내용을 보면 com.google.common.xml 패키지는 모듈 내부에 있는 com.google.common.escape, 이미 잘 알려진 java.lang, 그리고 여기서 찾을 수 없는 javax.annotation 패키지에 종속되어 있음을 알수 있습니다. 또한 이것은 javax.annotation이 포함되어 있는(JSR-305 타입이 같이 있는) jar가 필요하다는 것을 말해줍니다. (이 예제에서는 jar파일을 추가하는 작업을 수행하지 않습니다. 예제에서 이 패키지가 필요하지 않으며 컴파일러도 런타임에도 사용하지 않습니다..)

Unnamed module

그렇다면 Unnamed module은 무엇일까요? 역시 아래의 세 가지 질문에 다시 답해보겠습니다.

Q: 이름은 어떻게 되나요? (name)

A: 아직 추측하지 못했나요? Unnamed module에는 이름이 없습니다.

Q: 어떤것을 제공하게 되나요? (export)

A: Unnamed module 은 자신의 모든 패키지를 다른 모듈로 export 합니다. 그렇다고 다른 모듈에서 읽을 수있는 것은 아닙니다. 이름이 없으므로 require 할 수 없습니다! requires unnamed;는 쓸수 없습니다.

Q: 어떤것이 필요하나요? (require)

A: Unnamed module은 사용 가능한 다른 모든 모듈을 읽습니다.

그럼 여러분의 모듈 중 하나에서 Unnamed module을 읽을 수 없다면 다른 중요한 점은 무었일까요? 이것에 대한 대답하기 위해 우리는 옛 친구를 (classpath) 만나봐야 합니다. classpath (modulepath 대신)에서 읽은 모든 타입은 Unnamed module로 자동 배치됩니다. 또는 다르게 설명하면 Unnamed module의 모든 타입은 classpath를 통해 로드됩니다.
Unnamed module은 다른 모든 모듈을 읽을 수 있으므로 classpath에 로드된 모든 exported된 타입을 읽을수 있습니다. Java9는 classpath와 modulepath를 단독으로 사용하거나 하위 호환성을 위해 혼합하여 사용할 수 있도록 지원합니다. 몇가지 예제를 살펴보도록 하겠습니다.

우리는 여전히 잘 작동하는 zipvalidator 모듈을 가지고 있고, addresschecker는 여전히 모듈화된 상태가 아니며 module-info.java가 없다고 가정해 보겠습니다.
아래는 우리 소스의 구조입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
one-module-with-unnamed-ok/
├── classpath
│ └── de.codecentric.legacy.addresschecker
│ └── de
│ └── codecentric
│ └── legacy
│ └── addresschecker
│ ├── api
│ │ ├── AddressChecker.java
│ │ └── Run.java
│ └── internal
│ └── AddressCheckerImpl.java
├── modulepath
│ └── de.codecentric.zipvalidator
│ ├── de
│ │ └── codecentric
│ │ └── zipvalidator
│ │ ├── api
│ │ │ ├── ZipCodeValidator.java
│ │ │ └── ZipCodeValidatorFactory.java
│ │ └── internal
│ │ └── ZipCodeValidatorImpl.java
│ └── module-info.java

위 소스의 구조에는 레거시 코드가 포함된 zipvalidator에 액세스하려는 classpath 폴더와 zipvalidator 모듈이 포함된 modulepath라는 폴더가 있습니다. 일반적인 방법으로 우리의 모듈은 컴파일 할수 있습니다. 그리고 레거시 코드를 컴파일하려면 modular 코드에 대한 정보를 제공해야 합니다. 이러한 정보는 아래의 예제와 같이 classpath에 포함하면 됩니다.

1
2
javac -d classpath/de.codecentric.legacy.addresschecker
-classpath modulepath/de.codecentric.zipvalidator/ $(find classpath -name "*.java")

이런 방식은 평소대로 잘 작동합니다.

런타임에는 아래의 두가지 옵션을 사용할 수 있습니다.

  • 모듈을 classpath에 추가 하는 방법
  • classpath와 modulepath를 혼합하는 방법

첫 번째 옵션을 사용하면 모듈 시스템을 사용하지 않는다는 것을 의미합니다. 모든 타입은 자유롭게 서로 액세스 할 수있는 Unnamed module로 처리 됩니다.

1
2
java -cp modulepath/de.cc.zipvalidator/:classpath/de.cc.legacy.addresschecker/
de.codecentric.legacy.addresschecker.api.Run 76185

위와 같이 할 경우 현재 사용중인 Java 응용 프로그램과 정확히 동일하게 작동합니다.

classpath와 modulepath를 섞어서 사용하면 다음과 같이됩니다.

1
2
3
java -modulepath modulepath -addmods de.codecentric.zipvalidator
-classpath classpath/de.codecentric.legacy.addresschecker/
de.codecentric.legacy.addresschecker.api.Run

-classpath-modulepath 옵션을 모두 사용합니다. 새로운 추가 기능은 -addmods 옵션입니다. classpath와 modulepath를 혼합하여 사용 할 때 modulepath 폴더에있는 모든 모듈을 액세스 할 수있는 것은 아니며, 어떤 모듈을 사용할지 구체적으로 명시해야합니다.

이 접근법은 잘 작동하지만 주의 할 점이 있습니다! “Unnamed module이 요구하는 것(require)”에 대한 대답은 “다른 모든 모듈”입니다. modulepath를 통해 zipvalidator 모듈을 사용할때는 zipvalidator 모듈의 exported된 패키지 만 사용할 수 있습니다. 그러지 않을 경우 런타임시 IllegalAccessError가 발생합니다. 그렇기 때문에 이러한 경우 모듈 시스템의 규칙을 준수해야 합니다.

jlink를 이용한 runtime image

모듈에 대한 예제는 이걸로 끝입니다. 하지만 우리가 관심을 가질 또 다른 새로운 도구가 있습니다. jlink는 여러분 만의 자체 JVM 배포판을 만들수 있는 Java9 의 유틸리티입니다. 멋진 점은 새로 모듈화 된 JDK 특성으로 인해 이 배포판에 포함 할 모듈만을 선택할 수 있다는 것입니다. 예제를 한번 보겠습니다. 우리의 addresschecker가 포함된 런타임 이미지를 만들고자 한다면 다음 명령어를 실행하면 됩니다.

1
2
jlink --modulepath $JAVA9_BIN/../../images/jmods/:two-modules-ok/
--addmods de.codecentric.addresschecker --output linkedjdk

여러분은 아래의 세 가지만을 지정하면 됩니다.

  • module path (여러분의 모듈이 포함되고, Java의 standard 모듈이 포함되어 있는 JDK의 jmods 폴더)
  • 여러분의 배포판에 포함되길 원하는 모듈
  • 배포판이 저장될 폴더

위의 명령어는 아래와 같은 폴더를 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
linkedjdk/
├── bin
│ ├── java
│ └── keytool
├── conf
│ ├── net.properties
│ └── security
│ ├── java.policy
│ └── java.security
└── lib
├── classlist
├── jli
│ └── libjli.dylib
├── jspawnhelper
├── jvm.cfg
├── libjava.dylib
├── libjimage.dylib
├── libjsig.diz
├── libjsig.dylib
├── libnet.dylib
├── libnio.dylib
├── libosxsecurity.dylib
├── libverify.dylib
├── libzip.dylib
├── modules
│ └── bootmodules.jimage
├── security
│ ├── US_export_policy.jar
│ ├── blacklisted.certs
│ ├── cacerts
│ └── local_policy.jar
├── server
│ ├── Xusage.txt
│ ├── libjsig.diz
│ ├── libjsig.dylib
│ ├── libjvm.diz
│ └── libjvm.dylib
└── tzdb.dat

이것이 끝입니다. OSX Mavericks의 경우 약 47MB정도 크기입니다. 또한 압축 기능을 사용하거나, 프로덕션 시스템에서 필요하지 않은 일부 디버깅 기능을 제거 할 수 있습니다. 지금까지 작업한 예제의 가장 작은 배포판은 다음 명령을 사용하여 만들었습니다.

1
2
3
jlink --modulepath $JAVA9_BIN/../../images/jmods/:two-modules-ok/bin
--addmods de.codecentric.addresschecker --output linkedjdk --exclude-files *.diz
--compress-resources on --strip-java-debug on --compress-resources-level 2

이 방식은 배포판을 약 18MB로 줄여줘 즐겁게 합니다. 아마도 Linux에서는 13MB까지 줄일수 있을 것입니다.

1
/bin/java --listmods

위 명령어를 실행하면 배포판에 포함된 모듈을 보여줍니다.

1
2
3
de.codecentric.addresschecker
de.codecentric.zipvalidator
java.base@9.0

따라서 위의 모듈중 하나 또는 모든 모듈에 의존성이 있는 모든 응용프로그램은 이 JVM에서 실행 할 수 있습니다.

하지만 저는 이 시나리오를 이용해서 main class를 실행할 수 없었습니다. 다른 방법을 사용해야 했습니다.
예리한 눈을 가진 분은 jlink의 두번째 예제에서 첫번째 예제와 다른 modulepath를 지정한것을 발견했을 것입니다. 두 번째 예제에서 bin 폴더까지 경로를 지정했습니다. 이 폴더는 modular jar들을 포함하고, addresschecker jar는 Manifest에 메인 클래스 정보를 포함합니다. jlink는 이 정보를 사용하여 JVM bin 폴더에 추가 내용을 포함합니다.

1
2
3
4
5
6
7
linkedjdk/
├── bin
│ ├── de.codecentric.addresschecker
│ ├── java
│ └── keytool
...

이러한 방법을 이용하여 우리의 애플리케이션을 직접 호출 할 수 있습니다.

1
./linkedjdk/bin/de.codecentric.addresschecker 76185

실행 결과

1
76185 is a valid zip code

결론

이것으로 Jigsaw 소개를 마칩니다. 우리는 Jigsaw 및 Java 9로 할 수있는 것과 할 수없는 것을 보여주는 몇 가지 예를 살펴 보았습니다. Jigsaw는 쉽게 이해 및 적용할 수 있었던 Lambda 또는 try-with-resources와 다르게 다소 혼란스러운 변화일 수 있습니다. Maven 또는 Gradle과 같은 빌드 도구 부터 IDE까지 전체 툴 체인이 모듈 시스템을 적용해야 합니다. JavaOne에서 Gradle Inc.의 Hans Dockter는 Java9 이하 버전에서 모듈러 코드를 작성하는 방법을 보여주는 세션을 진행했습니다. Gradle은 컴파일 타임에 검사를 수행하고 모듈 무결성을 위반하면 실패합니다. 이 (실험적) 기능은 최근 출시 된 Gradle 2.9에 포함되었습니다.


관련 문서

공유하기