Java 9 와 Project Jigsaw 소개 1

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

This post is a translation of this original article [https://blog.codecentric.de/en/2015/11/first-steps-with-java9-jigsaw-part-1] 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


Module (Project Jigsaw)

Java 플랫폼의 모듈화및 일반 library의 모듈화 시스템 Project Jigsaw는 첫 발표후 8년이 지난후에서야 Java9에 포함될 예정입니다.
적용 대상 릴리스는 Java7, Java8, Java9로 수년에 걸쳐 변경 되었고 또한 적용 범위도 몇번이나 바뀌 었습니다.
2015년 JavaOne keynote에서 대대적으로 다뤘고 또한 많은 세션에서 포커싱 했던 Project Jigsaw의 주요한 토픽들에 대해 어느정도 정리가 된듯 합니다.

Project Jigsaw 란 무엇이며 우리가 어떻게 사용할 수 있을까요?

이 두개의 파트로 구성된 이 블로그 시리즈는 Module system에 대한 간단한 소개와 Project Jigsaw의 동작을 보여주기 위한 실제 예제 코드를 제공하는 것을 목표로합니다.
첫번째 부분에서는 Module system이 무엇인지, JDK가 어떻게 모듈화 되었는지에 대해 조금 이야기하고 특정 상황에서 컴파일러와 런타임이 어떻게 작동하는지 설명합니다.

Module 이란 뭘까요?

Module을 설명하는건 간단합니다. module-info.java라는 파일에 아래의 세 가지 질문에 대한 답을 선언하는 소프트웨어적인 단위일 뿐입니다.

  • 이름이 무엇인가? (name)
  • 어떤것을 제공하는가? (export)
  • 어떤것들이 필요한가 ? (require)

a simple module

Module

첫 번째 질문에 대한 대답은 간단합니다. (대부분의) 각 Module에는 이름이 있습니다. 그리고 이름은 충돌을 피하기 위해 패키지 명명 규칙과 유사해야 합니다 (예: de.codecentric.mymodule)
두번째 질문에 대한 답은, 우리의 Module은 다른 외부 모듈에서 사용할 수 있도록 공개 API로 간주되는 모든 패키지 목록을 제공합니다.
또한 만약 어떤 클래스가 public이라 할지라도 export된 패키지에 없으면 모듈 외부의 어떤것도 이 클래스에 접근 할 수 없습니다.
세번째 질문에 대한 답은, 우리의 모듈과 의존 관계가 있는 다른 모듈 목록(우리 모듈은 다른 모듈의 export된 모든 public type에 접근 가능합니다.)으로 얘기할 수 있습니다.
Jigsaw팀은 이것을 다른 모듈 읽기(reading another module)라고 합니다.

이것은 매우 큰 변화입니다. Java 8까지 classpath에 있는 모든 public type은 다른 어떤 type에서도 접근이 가능 했습니다.
Jigsaw를 사용하면 Java의 type들에 대한 기존의 접근 방법이

  • public
  • private
  • default
  • protected

에서

  • 외부에 모두 Public (public to everyone who reads this module (exports))
  • 특정 모듈에만 Public (public to some modules that read this module (exports to))
  • 모듈 내부만 Public (public to every other class within the module itself)
  • private
  • default
  • protected

로 변한다는 것입니다.

JDK 모듈화

모듈의 종속성은 순환 종속성(상호 참조)을 금지하는 비순환 그래프를 형성해야합니다. 이러한 원칙을 유지하기 위해 Jigsaw 팀의 주요한 작업중 하나는 기존 Java Runtime의 순환 참조(상호 참조)및 직관적이지 않은 종속성을 모듈화하는 것이었습니다. 그들은 아래의 그래프와 같이 모듈을 새로 그렸습니다.

아직 개발이 완료된 상황이 아니기 때문에 계속 바뀔수 있습니다.

그래프 하단에 java.base가 있습니다. 외부 참조가 없는 (inbound 만 있음)유일한 모듈입니다.
여러분이 생성하는 모든 모듈이 (Java의 모든 클래스가) java.lang.Object를 암시적으로 상속 하는 것과 유사하게 모듈내부의 선언 여부에 관계없이 java.base를 참조합니다. java.base는 java.lang, java.util, java.math 등과 같은 패키지를 export합니다.

JDK의 모듈화는 사용하려는 Java Runtime의 모듈을 따로 지정할 수 있음을 의미합니다.
이제는 java.desktop 또는 java.corba를 사용하지 않는 경우, Swing 또는 Corba 모듈이 포함된 환경에서 애플리케이션을 실행할 필요가 없습니다.
(애플리케이션에서 사용하는 모듈만 모아 Runtime 이미지를 만들수 있습니다.)

이제 체험 해봅시다.

아래의 모든 예제들은 JDK9가 필요합니다.
JDK 9 Early Access with Project Jigsaw에서 다운받아 실행 가능합니다.
JDK9의 설치는 기존 개발환경에 영향을 줄수 있기 때문에 신중히 설치하세요.

컴파일, 패키징 및 예제 실행을 위한 셸 스크립트를 비롯하여 다음에 나오는 모든 코드는 여기에서 확인할 수 있습니다.

우리의 기본적인 Use case는 매우 간단합니다.
우편번호 유효성 검사를 수행하는 de.codecentric.zipvalidator라는 모듈이 있습니다. 이 zipvalidator 모듈은 de.codecentric.addresschecker 모듈이 사용합니다. (예제에서 zipvalidator 모듈이 우편 번호보다 더 많은 것을 검사 할 수 있지만 그렇게 하지 않습니다.)

zipvalidator는 다음 module-info.java에 다음과 같이 선언됩니다.

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

이 모듈은 de.codecentric.zipvalidator.api 패키지를 export하고 다른 모듈은 사용하지 않습니다 (java.base 제외). 그리고 이 모듈은 addresschecker에 의해 사용됩니다.

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

전체 파일 시스템 구조는 다음과 같습니다.

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

기존 패키지의 관례에 따라 모듈도 모듈과 동일한 이름의 폴더에 위치 합니다.
우리의 첫번째 예제는 잘 동작합니다.(AddressCheckerImpl 클래스는 de.codecentric.zipvalidator에서 export된 패키지 ZipCodeValidatorZipCodeValidatorFactory에만 액세스 할 수 있습니다.)

1
2
3
4
5
6
public class AddressCheckerImpl implements AddressChecker {
@Override
public boolean checkZipCode(String zipCode) {
return ZipCodeValidatorFactory.getInstance().zipCodeIsValid(zipCode);
}
}

이제 javac를 이용하여 컴파일 하고 bytecode를 생성해 봅시다.
zipvalidator를 컴파일 하려면 일반적으로 아래의 예제와 같이 실행을 하지만, addresscheckerzipvalidator를 사용하려면 우리가 먼저 해야할 일이 있습니다.

1
2
javac -d de.codecentric.zipvalidator \
$(find de.codecentric.zipvalidator -name "*.java")

보는 바와 같이 매우 익숙한 명령어입니다. zipvalidator는 어떤 커스텀 모듈에도 의존하지 않기 때문에 다른 모듈에 대해 언급할 필요는 아직 없습니다.
find 명렁어는 폴더 안에 있는 .java 파일들을 리스팅하도록 합니다.
하지만 컴파일 할 때 javac 명령어에 모듈 구조에 대해 어떻게 설명할까요?
이를 위해 Jigsaw-modulepath 또는 -mp 옵션을 적용했습니다.

addresschecker를 컴파일하기 위해 다음과 같이 수정하여 사용합니다.

1
2
javac -modulepath . -d de.codecentric.addresschecker \
$(find de.codecentric.addresschecker -name "*.java")

classpath 옵션과 유사하게 -modulepath를 이용하여 javac가 기존 컴파일된 모듈을 어디에서 찾을수 있는지 알려 줄 수 있습니다.(위의 예제의 경우 .입니다.)
여러개의 모듈을 각각 컴파일하는건 번거럽기 때문에, 아래와 같이 -modulesourcepath 옵션을 이용하여 한꺼번에 컴파일 할 수 있습니다.

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

위 예제는 . 하위의 모든 서브 디렉토리의 모듈을 검색하고 모든 Java 파일을 컴파일합니다.
모든 컴파일이 완료되면 이제 실행을 해볼 수 있습니다.

1
java -mp . -m de.codecentric.addresschecker/de.codecentric.addresschecker.api.Run 76185

위 예제와 같이 java명령어를 실행하면서 Main 클래스 및 매개 변수도 지정 할 수있고, 또한 JVM이 어디에서 컴파일 된 모듈을 찾아야 하는지도 지정할 수 있습니다.
위 예제의 실행 결과는 아래와 같이 나옵니다.

1
76185 is a valid zip code

Modular Jars

기존 Java 프로그램에서 jar파일을 library로 매우 많이 사용하고 있습니다.
Jigsaw에서는 modular jar라는 개념을 도입했습니다. modular jar는 일반 jar와 매우 유사하지만 컴파일 된 module-info.class을 포함 하고 있습니다.
그리고 특정 JVM 버전용으로 컴파일 된 경우 이 jar는 하위 버전과 호환됩니다.
module-info.java는 유효한 타입 이름이 아니기 때문에 컴파일 된 module-info.class는 이전 JVM에서 무시됩니다.

zipvalidator 용 jar파일을 만들기 위해 아래와 같이 실행하면 됩니다.

1
2
jar --create --file bin/zipvalidator.jar \
--module-version=1.0 -C de.codecentric.zipvalidator .

위 예제에서 결과 파일, 버전(런타임에 Jigsaw에서 여러버전의 Module을 사용한다는 지시는 표현 되어 있지 않지만), 모듈을 패키징 합니다.

zipvalidator에는 Main 클래스가 있으므로 다음과 같이 지정할 수도 있습니다.

1
2
3
jar --create --file=bin/addresschecker.jar --module-version=1.0 \
--main-class=de.codecentric.addresschecker.api.Run \
-C de.codecentric.addresschecker .

Main 클래스는 Jigsaw 팀에서 처음 계획 한대로 module-info.java에 설정되지 않고 평소대로 Manifest에 작성되었습니다.

다음 명령어로 실행해 볼수 있습니다.

1
java -mp bin -m de.codecentric.addresschecker 76185

이전에 실행한 결과와 같습니다.
위 예제에서 jar 파일이 생성된 bin 폴더를 modulepath에 다시 설정합니다.
addresschecker.jarManifest에는 이 정보가 이미 포함되어 있으므로 Main 클래스를 지정할 필요가 없습니다. 단지 -m 옵션을 이용하여 모듈 이름을 제공하면 충분합니다.

Module 사용시 유의할 점

지금까지의 예제는 모두 잘 만들어지고 동작하는 코드들이었습니다. 이제 우리는 정상적이지 않는 경우에 Jigsaw가 컴파일과 런타임에서 어떻게 동작하는지 살펴 보기 위해 Module을 조금 수정해 보도록 하겠습니다.

1.5.1 Export하지 않은 타입 사용

이번 예제에서 사용해서는 안되는 다른 모듈을 액세스 할때 어떤 일이 발생하는지 확인해 보겠습니다.
AddressCheckerImpl에서 ZipCodeValidatorFactory를 이용하는 부분을 다음과 같이 변경합니다.

1
2
3
4
5
6
7
public class AddressCheckerImpl implements AddressChecker {
@Override
public boolean checkZipCode(String zipCode) {
//return ZipCodeValidatorFactory.getInstance().zipCodeIsValid(zipCode);
return new ZipCodeValidatorImpl().zipCodeIsValid(zipCode);
}
}

위 예제를 컴파일하면 아래와 같은 결과가 나옵니다.

1
2
error: ZipCodeValidatorImpl is not visible because
package de.codecentric.zipvalidator.internal is not visible

따라서 직접 export하지 않은 타입을 사용하는 것은 컴파일 할 때 에러가 발생합니다.
하지만 만약 아래와 같이 reflection을 이용하도록 코드를 수정하면 어떻게 될까요?

1
2
3
4
5
6
7
ClassLoader classLoader = AddressCheckerImpl.class.getClassLoader();
try {
Class aClass = classLoader.loadClass("de.[..].internal.ZipCodeValidatorImpl");
return ((ZipCodeValidator)aClass.newInstance()).zipCodeIsValid(zipCode);
} catch (Exception e) {
throw new RuntimeException(e);
}

위의 코드는 정상적으로 컴파일되고 실행할 수 있습니다.
그럼 실행해 볼까요?. 하지만 Jigsaw는 쉽게 속지 않습니다.

1
2
3
4
5
6
7
java.lang.IllegalAccessException:
class de.codecentric.addresschecker.internal.AddressCheckerImpl
(in module de.codecentric.addresschecker) cannot access class [..].internal.ZipCodeValidatorImpl
(in module de.codecentric.zipvalidator) because module
de.codecentric.zipvalidator does not export package
de.codecentric.zipvalidator.internal to module
de.codecentric.addresschecker

Jigsaw는 컴파일시 체크 뿐만 아니라 런타임시에도 체크가 포함되어 있습니다!. 그래서 우리가 잘못한 것을 매우 분명하게 알아 냅니다.

순환 참조 (Circular dependencies)

다음의 경우를 가정해 봅시다.
개발하는 도중에 어느 순간 zipvalidator가 사용하고자 하는 API 클래스를 addresschecker 모듈이 포함하고 있음을 깨닫게 되었습니다.
하지만 귀찮거나 다른 이유로 해당 클래스를 다른 모듈로 리팩토링하는 대신 아래의 코드처럼 addresschecker에 그냥 의존성을 선언할 수 있습니다.

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

그렇지만 Jigsaw에서 순환 참조는 허용되지 않기 때문에 컴파일러는 아래와 같은 에러 메시지를 보여줍니다.

1
2
./de.codecentric.zipvalidator/module-info.java:2:
error: cyclic dependence involving de.codecentric.addresschecker

우리는 이러한 문제들을 컴파일 타임에 바로 알수 있게 됩니다.

묵시적인 접근(Implied readability)

좀 더 많은 기능을 제공하기 위해 우리는 리턴 타입으로 boolean을 사용하는 대신 유효성 검사 결과를 위한 일종의 모델을 포함하는 새로운 모듈 de.codecentric.zipvalidator.model을 도입하여 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
three-modules-ok/
├── de.codecentric.addresschecker
│ ├── de
│ │ └── codecentric
│ │ └── addresschecker
│ │ ├── api
│ │ │ ├── AddressChecker.java
│ │ │ └── Run.java
│ │ └── internal
│ │ └── AddressCheckerImpl.java
│ └── module-info.java
├── de.codecentric.zipvalidator
│ ├── de
│ │ └── codecentric
│ │ └── zipvalidator
│ │ ├── api
│ │ │ ├── ZipCodeValidator.java
│ │ │ └── ZipCodeValidatorFactory.java
│ │ └── internal
│ │ └── ZipCodeValidatorImpl.java
│ └── module-info.java
├── de.codecentric.zipvalidator.model
│ ├── de
│ │ └── codecentric
│ │ └── zipvalidator
│ │ └── model
│ │ └── api
│ │ └── ZipCodeValidationResult.java
│ └── module-info.java

ZipCodeValidationResult는 “too short”, “too long” 등과 같은 인스턴스가 있는 간단한 enum 타입입니다.
module-info.java는 아래와 같이 수정되었습니다.

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

그리고 ZipCodeValidator의 구현체 (ZipCodeValidatorImpl.java)는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public ZipCodeValidationResult zipCodeIsValid(String zipCode) {
if (zipCode == null) {
return ZipCodeValidationResult.ZIP_CODE_NULL;
} else if (zipCode.length() < 5) {
return ZipCodeValidationResult.ZIP_CODE_TOO_SHORT;
} else if (zipCode.length() > 5) {
return ZipCodeValidationResult.ZIP_CODE_TOO_LONG;
} else {
return ZipCodeValidationResult.OK;
}
}

addresschecker 모듈이 ZipCodeValidationResult를 리턴 타입으로 사용하도록 수정되었기 때문에 컴파일이 잘 되어야 합니다.
그럴까요? 아래는 컴파일 결과입니다.

1
2
3
./de.codecentric.addresschecker/de/[..]/internal/AddressCheckerImpl.java:5:
error: ZipCodeValidationResult is not visible because package
de.codecentric.zipvalidator.model.api is not visible

addresschecker의 컴파일에 에러가 발생합니다. zipvalidatorzipvalidator.model에서 exportpublic API타입을 사용합니다.
하지만 addresscheckermodule-info.java에서 requires를 하지 않았기 때문에 zipvalidator.model 모듈을 액세스 할 수 없습니다.

이 문제를 해결하기 위한 두가지 방법이 있습니다.
확실한 방법은 addresschecker에서 zipvalidator.model에 대한 read edge를 추가하는 것입니다.
그러나 이것은 그리 좋은 방법이 아닙니다.

addresscheckerzipvalidator를 사용하기 위해 zipvalidator.model에 대한 dependency를 추가해야 할까요?
그리고 zipvalidator를 사용하는 모든 곳에서 zipvalidator에서 참조하는 모든 모듈에 액세스 할 수 있어야 할까요?

하지만 그렇게 해야만하고 그렇게 할 수 있습니다.

Implied readability에 오신 것을 환영합니다.
zipvalidatorrequires definition에 public 키워드를 추가하여 zipvalidator의 모든 클라이언트 모듈에 또 다른 모듈을 읽을 필요가 있음을 알립니다.

아래 코드는 zipvalidator의 업데이트 된 module-info.java입니다.

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

public 키워드는 zipvalidator 모듈을 사용하는 모든 모듈에게 zipvalidator.model모듈을 읽을 필요가 있다고 알려줍니다.
이것은 classpath를 사용하던 익숙한 방식으로 부터 변화입니다. 더이상 Maven POM에 의존 해서는 안됩니다.
그렇기 때문에 public API의 일부인 경우 모든 client가 여러분 모듈의 dependency를 사용할 수 있도록 하려면 명시적으로 지정해야 합니다.

이것은 매우 좋은 모델입니다.
만약 의존성이 있는 모듈을 내부에서만 사용하는 경우 client를 귀찮게 하지 않아도 됩니다.
그리고 모듈을 대외적으로 사용한다면 자신이 사용하는 모듈에 대해 자신의 클라이언트에 알려줘야 합니다.


관련 문서

공유하기