예제로 배우는 JAVA9
1. Java Modular System
Java9의 가장 큰 변화는 Module System의 도입이라 할 수 있다. jdk만 보더라도 기존의 구조와 많이 달라졌으며, Java runtime의 근간이 되는 library에도 큰 변화가 있었다. Java8까지는 java runtime에 필요한 대부분의 library가 rt.jar와 tool.jar에 포함되어 있었다.rt.jar 의 구성
├─com
│ ├─oracle
│ │ ├─net
│ │ ├─nio
│ │ ├─util
│ │ ├─webservices
│ │ │ └─internal
│ │ │ ├─api
│ │ │ │ ├─databinding
│ │ │ │ └─message
│ │ │ └─impl
│ │ │ ├─encoding
│ │ │ └─internalspi
│ │ │ └─encoding
│ │ └─xmlns
│ │ └─internal
│ │ └─webservices
│ │ └─jaxws_databinding
│ └─sun
│ ├─accessibility
│ │ └─internal
│ │ └─resources
│ ├─activation
│ │ └─registries
│ ├─awt
│ ├─beans
│ │ ├─decoder
│ │ ├─editors
│ │ ├─finder
│ │ ├─infos
│ │ └─util
......
├─java
│ ├─applet
│ ├─awt
│ │ ├─color
│ │ ├─datatransfer
│ │ ├─dnd
│ │ │ └─peer
│ │ ├─event
│ │ ├─font
│ │ ├─geom
│ │ ├─im
│ │ │ └─spi
│ │ ├─image
│ │ │ └─renderable
│ │ ├─peer
│ │ └─print
│ ├─beans
│ │ └─beancontext
│ ├─io
│ ├─lang
│ │ ├─annotation
│ │ ├─instrument
│ │ ├─invoke
│ │ ├─management
│ │ ├─ref
│ │ └─reflect
│ ├─math
│ ├─net
│ ├─nio
│ │ ├─channels
│ │ │ └─spi
│ │ ├─charset
│ │ │ └─spi
│ │ └─file
│ │ ├─attribute
│ │ └─spi
│ ├─rmi
│ │ ├─activation
│ │ ├─dgc
│ │ ├─registry
│ │ └─server
│ ├─security
│ │ ├─acl
│ │ ├─cert
│ │ ├─interfaces
│ │ └─spec
│ ├─sql
│ ├─text
│ │ └─spi
│ ├─time
│ │ ├─chrono
│ │ ├─format
│ │ ├─temporal
│ │ └─zone
│ └─util
│ ├─concurrent
│ │ ├─atomic
│ │ └─locks
│ ├─function
│ ├─jar
│ ├─logging
│ ├─prefs
│ ├─regex
│ ├─spi
│ ├─stream
│ └─zip
├─javax
│ ├─accessibility
│ ├─activation
........
│ ├─ws
│ │ ├─handler
│ │ │ └─soap
│ │ ├─http
│ │ ├─soap
│ │ ├─spi
│ │ │ └─http
│ │ └─wsaddressing
│ └─xpath
├─jdk
│ ├─internal
│ │ ├─cmm
│ │ ├─instrumentation
.......
├─org
│ ├─ietf
│ │ └─jgss
│ ├─jcp
│ │ └─xml
│ │ └─dsig
│ │ └─internal
│ │ └─dom
.......
│ ├─w3c
│ │ └─dom
│ │ ├─bootstrap
│ │ ├─css
│ │ ├─events
│ │ ├─html
│ │ ├─ls
│ │ ├─ranges
│ │ ├─stylesheets
│ │ ├─traversal
│ │ ├─views
│ │ └─xpath
│ └─xml
│ └─sax
│ ├─ext
│ └─helpers
└─sun
├─applet
│ └─resources
├─audio
├─awt
.......
├─tracing
│ └─dtrace
├─usagetracker
└─util
├─calendar
├─cldr
├─locale
│ └─provider
├─logging
│ └─resources
├─resources
│ └─en
├─spi
└─xml
1.1 Jmods
jdk 9부터는 rt.jar와 tool.jar가 사라지고 .jmod 의 확장자를 가지는 새로운 java archive 파일 형태의 library가 jdk에 포함된다. 파일명도 기존의 형식을 탈피하고, package name을 연상시키는 형태로 바뀌었다. (ex. java.base.jmod) 실제로 jdk9의 구조를 살펴보면 기존의 jar가 수많은 jmod 파일들로 대체되었다는 것을 알 수가 있다.D:\work\java\jdk-9.0.4\jmods>dir /w
D:\work\java\jdk-9.0.4\jmods 디렉터리
[.] [..]
java.activation.jmod java.base.jmod
java.compiler.jmod java.corba.jmod
java.datatransfer.jmod java.desktop.jmod
java.instrument.jmod java.jnlp.jmod
java.logging.jmod java.management.jmod
java.management.rmi.jmod java.naming.jmod
java.prefs.jmod java.rmi.jmod
java.scripting.jmod java.se.ee.jmod
java.se.jmod java.security.jgss.jmod
java.security.sasl.jmod java.smartcardio.jmod
java.sql.jmod java.sql.rowset.jmod
java.transaction.jmod java.xml.bind.jmod
java.xml.crypto.jmod java.xml.jmod
java.xml.ws.annotation.jmod java.xml.ws.jmod
javafx.base.jmod javafx.controls.jmod
javafx.deploy.jmod javafx.fxml.jmod
javafx.graphics.jmod javafx.media.jmod
javafx.swing.jmod javafx.web.jmod
jdk.accessibility.jmod jdk.attach.jmod
jdk.charsets.jmod jdk.compiler.jmod
jdk.crypto.cryptoki.jmod jdk.crypto.ec.jmod
jdk.crypto.mscapi.jmod jdk.deploy.controlpanel.jmod
jdk.deploy.jmod jdk.dynalink.jmod
jdk.editpad.jmod jdk.hotspot.agent.jmod
jdk.httpserver.jmod jdk.incubator.httpclient.jmod
jdk.internal.ed.jmod jdk.internal.jvmstat.jmod
jdk.internal.le.jmod jdk.internal.opt.jmod
jdk.internal.vm.ci.jmod jdk.jartool.jmod
jdk.javadoc.jmod jdk.javaws.jmod
jdk.jcmd.jmod jdk.jconsole.jmod
jdk.jdeps.jmod jdk.jdi.jmod
jdk.jdwp.agent.jmod jdk.jfr.jmod
jdk.jlink.jmod jdk.jshell.jmod
jdk.jsobject.jmod jdk.jstatd.jmod
jdk.localedata.jmod jdk.management.agent.jmod
jdk.management.cmm.jmod jdk.management.jfr.jmod
jdk.management.jmod jdk.management.resource.jmod
jdk.naming.dns.jmod jdk.naming.rmi.jmod
jdk.net.jmod jdk.pack.jmod
jdk.packager.jmod jdk.packager.services.jmod
jdk.plugin.dom.jmod jdk.plugin.jmod
jdk.plugin.server.jmod jdk.policytool.jmod
jdk.rmic.jmod jdk.scripting.nashorn.jmod
jdk.scripting.nashorn.shell.jmod jdk.sctp.jmod
jdk.security.auth.jmod jdk.security.jgss.jmod
jdk.snmp.jmod jdk.unsupported.jmod
jdk.xml.bind.jmod jdk.xml.dom.jmod
jdk.xml.ws.jmod jdk.zipfs.jmod
oracle.desktop.jmod oracle.net.jmod
98개 파일 127,847,193 바이트
jdk에 포함되어 있는 jmod 파일들이 각각 Java9의 모듈이라 할 수 있는데, 그 중에서 하나를 선택하여(여기에서는 java.base.jmod) 파일의 압축을 풀어보면 java module의 구조를 대략적으로 이해할 수 있다. jar 파일의 생성 및 extract에 'jar' tool을 이용하였듯이, Java9에서는 jmod 파일의 생성 및 압축해제를 위해 'jmod' tool을 제공한다.
Usage: jmod (create|extract|list|describe|hash) (OPTIONS) jmod-file
java.base.jmod 파일의 압축을 풀어야 하므로 다음과 같이 실행한다.
jmod extract --dir java.base java.base.jmod
압축해제 경로인 java.base 디렉토리로 이동해보면 기존의 jar 와는 조금 다른 부분을 확인할 수 있는데 jar가 주로 클래스 파일들의 집합이었다면 jmod 는 클래스파일외에도 binary, native code, config 파일 등을 포함하고 있음을 알 수 있다.
java.base
├─bin
├─classes
│ ├─com
│ ├─java
│ ├─javax
│ ├─jdk
│ └─sun
......
├─conf
│ └─security
│ └─policy
│ ├─limited
│ └─unlimited
├─include
│ └─win32
├─legal
└─lib
├─security
└─server
1.2 Module in Java
좀 더 주의깊게 볼 필요가 있는 파일이 classes 폴더 안에 있는 module-info.java (class) 이다. 또다른 jmod 파일 중의 하나인 java.sql.jmod 파일의 module-info.java 파일의 내용은 아래와 같다.module java.sql {
requires transitive java.logging;
requires transitive java.xml;
exports java.sql;
exports javax.sql;
exports javax.transaction.xa;
uses java.sql.Driver;
}
Module은 code와 data를 포함하고 있으며, 일반적으로 root에 module-info.java를 가진다. java9에서 module 개념을 도입하게된 이유는 크게 2가지인데, 하나는 reliable configuration, 다른 하나는 strong encapsulation 이다. 위에서 설명하였지만 일반적인 module-info.java 의 형태는 다음과 같다.module com.mine {
requires com.yours;
}
module com.yours {
exports son.been;
exports dau.been;
}
com.mine과 com.yours은 module name을 의미하고, requires 뒤의 내용은 해당 모듈이 참조하는 다른 모듈명을 의미한다. exports 키워드를 통해 모듈내에서 다른 모듈에 public 하게 제공할 package를 선언할 수 있는데, com.mine 모듈이 com.yours 모듈을 requires 하고 있고 com.yours 모듈이 son.been, dau.been 패키지를 exports 하고 있으므로 com.mine은 이것을 이용할 수 있다. 후술하겠지만 A모듈이 B모듈을 access하기 위해서는 A모듈이 B모듈을 'requires' 하고 B모듈은 자신을 'exports' 할 때 가능해진다. 위의 example의 구조와 코드내용은 아래와 같다. (.iml 파일은 IDE(여기에서는 intellij)에서 프로젝트 관리를 위해 생성하는 파일이며 java module system과 관련이 없다.)├── com.mine
│ ├── com.mine.iml
│ └── src
│ ├── me
│ │ └── dhyun
│ │ └── MyWriter.java
│ └── module-info.java
├── com.yours
│ ├── com.yours.iml
│ └── src
│ ├── dau
│ │ └── been
│ │ └── MyStringBuilder.java
│ ├── module-info.java
│ └── son
│ └── been
│ └── MyFileReader.java
├── file.txt
com.mine 모듈의 MyWriter.java
package me.dhyun;
import dau.been.MyStringBuilder;
import son.been.MyFileReader;
public class MyWriter {
public static void main(String[] args) throws Exception {
MyFileReader fileReader = new MyFileReader("file.txt");
MyStringBuilder stringBuilder = new MyStringBuilder();
try (fileReader) {
String line;
while ((line = fileReader.readLine()) != null) {
stringBuilder.append(line);
}
}
System.out.println(stringBuilder.toString());
}
}
com.yours 모듈의 FileReader.java
package son.been;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class MyFileReader implements AutoCloseable {
private BufferedReader br;
public MyFileReader(String filePath) {
try {
br = new BufferedReader(new FileReader(new File(filePath)));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public String readLine() {
try {
return br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
@Override
public void close() throws Exception {
br.close();
}
}
com.yours 모듈의 MyStringBuilder.java
package dau.been;
public class MyStringBuilder {
private StringBuilder sb;
public MyStringBuilder() {
sb = new StringBuilder();
}
public StringBuilder append(Integer i) {
return sb.append(String.valueOf(i));
}
public StringBuilder append(String s) {
return sb.append(s);
}
public String toString() {
return sb.toString();
}
}
file.txt 파일
java9
modular
system
main() 함수가 정의되어 있는 MyWriter.java는 com.yours가 exports하고 있는 dau.been.MyStringBuilder와 son.been.MyFileReader를 import해서 사용하고 있으며 이를 실행해보면 아래와 같은 결과를 확인할 수 있다.
java9modularsystem
1.2.2 Implied Readability
추가적으로 com.theirs 모듈을 아래와 같이 정의하도록 하자.com.theirs 모듈의 module-info.java
module com.theirs {
exports spa.ko;
}
com.thiers 모듈의 구조
├── com.theirs
│ ├── com.theirs.iml
│ └── src
│ ├── module-info.java
│ └── spa
│ └── ko
│ └── MyStringReplacer.java
com.theirs 모듈의 MyStringReplacer.java
package spa.ko;
public class MyStringReplacer {
public String replace(String source, String from, String to) {
return source.replace(from, to);
}
}
com.theirs 모듈이 spa.ko 패키지를 exports 하고 있으므로, com.mine 모듈에서 MyStringReplacer 클래스를 사용하고 싶다면 당연히 아래와 같이 requires 항목을 추가해주면 된다.com.mine 모듈의 수정된 module-info.java
module com.mine {
requires com.yours;
requires com.theirs;
}
하지만 com.yours 모듈이 com.thiers 모듈을 requires 하고 있다면, com.yours 모듈의 moule-info.java를 transitive 키워드를 이용하여 아래와 같이 수정해도 같은 효과를 볼 수 있다.module com.yours {
exports son.been;
exports dau.been;
requires transitive com.theirs;
}
java module system에서는 명시적인 requires 항목 없이도 위와 같은 암묵적인 access가 가능한 방법을 제공하는데, 이를 implied readability라고 한다.com.mine 모듈의 수정된 MyWriter.java
package me.dhyun;
import dau.been.MyStringBuilder;
import son.been.MyFileReader;
import spa.ko.MyStringReplacer;
public class MyWriter {
public static void main(String[] args) throws Exception {
MyFileReader fileReader = new MyFileReader("file.txt");
MyStringBuilder stringBuilder = new MyStringBuilder();
try (fileReader) {
String line;
while ((line = fileReader.readLine()) != null) {
stringBuilder.append(line);
}
}
System.out.println(stringBuilder.toString());
MyStringReplacer stringReplacer = new MyStringReplacer();
System.out.println(stringReplacer.replace(stringBuilder.toString(), "a", "_"));
}
}
=> 결과
java9modularsystem
j_v_9modul_rsystem
1.2.3 Qualified Exports
Module system을 정의하다보면, 특정 모듈에만 exports를 허용하고 싶은 경우가 있다. 이 경우 to 키워드를 이용하여 exports 할 모듈을 정의할 수 있는데, 이를 Qualified Exports 라고 한다. 위의 예제에서 com.yours 모듈의 패키지를 com.mine 모듈에만 exports 하고 싶다면 아래와 같이 module-info.java를 정의할 수 있다.module com.yours {
exports son.been to com.mine;
exports dau.been to com.mine;
}
아래의 경우와 같이 여러개의 모듈을 지정할 수도 있다.
module com.yours {
exports son.been to com.mine, com.theirs, com.another...;
}
1.2.4 Defining as a Service Consumer
아래와 같이 uses 키워드를 사용하여 모듈이 implements 또는 extends 하는 Service Provider를 정의할 수 있다.module com.mine {
requires com.yours;
uses java.security.Provider;
}
여기에서 uses 뒤에 오는 항목은 Service Provider가 되며 interface나 abstract class가 될 수 있다. 이 때, com.mine은 Service Consumer로서 abstract class인 java.security.Provider 를 extends 하는 Service 구현체를 가지게 된다.1.2.5 Defining as a Service Provider
모듈은 Service Consumer가 되는 동시에 Service Provider가 될 수도 있다. 이 때 쓰이는 키워드가 provides/with 이며 아래와 같이 선언한다.
module com.mine {
requires com.yours;
uses java.nio.file.spi.FileSystemProvider;
provides java.nio.file.spi.FileSystemProvider with jdk.internal.jrtfs.JrtFileSystemProvider;
}
provides 키워드 뒤에는 uses 키워드 다음에 선언한 interface 또는 abstract class가 위치한다. with 키워드 뒤에는 이를 구현한 class를 선언함으로써 Service Provider로서의 기능을 가지는 모듈을 정의할 수 있다.1.2.6 Strong Encapsulation
Java의 information hiding 정책은 기본적으로 다른 class의 private method 및 field로의 접근을 허용하지 않는다. 하지만 java9 이전에는 아래와 같이 reflection을 이용한 우회적인 접근 방법이 존재했다.public class TargetClass {
private String targetString = "encapsulated string";
}
import java.lang.reflect.Field;
public class PrivateFieldGetter {
public static void main(String[] args) throws Exception {
TargetClass targetClass = new TargetClass();
Field f = targetClass.getClass().getDeclaredField("targetString");
f.setAccessible(true);
System.out.println(f.get(targetClass));
}
}
=> 결과
encapsulated string
실질적인 information hiding이 runtime에서는 엄격하게 지켜지지는 않았던 것이다. 하지만 java9의 module system은 strong encapsulation을 표방하며 위와 같은 접근을 허용하지 않는다. PrivateFieldGetter 클래스는 com.mine 모듈에 속해있고, com.yours 모듈의 dau.been패키지에 TargetClass가 속해 있으며, 두 모듈의 module-info.java는 아래와 같다고 가정한다.
module com.mine {
requires com.yours;
}
module com.yours {
exports dau.been;
}
package me.dhyun;
import java.lang.reflect.Field;
import dau.been.TargetClass;
public class PrivateFieldGetter {
public static void main(String[] args) throws Exception {
TargetClass targetClass = new TargetClass();
Field f = targetClass.getClass().getDeclaredField("targetString");
f.setAccessible(true);
System.out.println(f.get(targetClass));
}
}
=> 결과
Exception in thread "main" java.lang.reflect.InaccessibleObjectException
: Unable to make field private java.lang.String dau.been.TargetClass.targetString accessible
: module com.yours does not "opens dau.been" to module com.mine
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:176)
at java.base/java.lang.reflect.Field.setAccessible(Field.java:170)
at com.mine/me.dhyun.PrivateFieldGetter.main(PrivateFieldGetter.java:11)
위와 같이 InaccessibleObjectException을 발생시켜 허용되지 않는 접근임을 알려준다. 그런데 Exception의 메세지를 통해 알 수 있듯이 아래와 같이 opens 키워드를 사용하여 com.yours 모듈의 module-info.java를 수정하면 접근이 가능하게 할 수 있다. module com.yours {
exports dau.been;
opens dau.been; // 또는 opens dau.been to com.mine;
}
opens 키워드는 해당 패키지를 runtime 시에 public하게 공개(open)하겠다는 의미를 가진다. 하지만 이 역시 java9 이전의 완전치 않은 object encapsulation의 되풀이가 아니냐는 의문을 가질 수도 있지만, java9에서는 의도적(명시적)으로 module-info.java에 opens 키워드를 이용한 정책을 정의해야만 위와 같은 접근이 가능하다는 점을 생각할 때 java9이전의 encapsulation 정책과는 구분되어야 할 것이다.패키지 단위가 아닌 전체 모듈을 runtime에서 공개하고 싶다면 아래와 같이 모듈명 앞에 open 키워드를 붙여주면 된다.
open module com.yours {
exports dau.been;
}
1.2.7 Unnamed Module
일반적으로 모듈간의 access는 한 모듈이 다른 모듈을 명시적으로 필요로(requires) 하고, 다른 모듈이 자신의 package를 명시적으로 노출(exports)할 때 가능해진다. 그렇다면 java8 이하 버전에서 존재하는 클래스들과 java9에서 작성된 module들간의 accessibility는 어떻게 될까? 일반적으로 그들은 module system에서 정의하는 module-info.java는 물론 module 이름조차 가지고 있지 않다. module system에서는 이러한 모듈을 'unnamed module'로 정의한다. unnamed module은 특별한 선언 없이도 다른 모듈(named module)의 export된 package들을 읽을 수 있다. 이러한 방식으로 unnamed module을 정의함으로써, java8이하의 환경에서 작성된 코드가 java9 환경에서도 compile과 실행이 가능해진다(backward-compatibility). 아래와 같이 named module(com.yours)와 unnamed module(UnnamedModuleClass)이 같이 존재하는 경우를 생각해보자.|--java9_project
├── com.yours
│ └── src
│ ├── dau
│ │ └── been
│ │ ├── MyStringBuilder.java
│ ├── module-info.java
└── src
└── unamed
└── module
└── UnnamedModuleClass.java
UnnamedModuleClass.java의 내용은 아래와 같다.
package unamed.module;
import dau.been.MyStringBuilder;
public class UnnamedModuleClass {
public static void main(String[] args) {
MyStringBuilder sb = new MyStringBuilder();
sb.append(7777).append("=").append("FourCard");
System.out.println(sb.toString());
}
}
=> 결과
7777=FourCard
위에서 unnamed module은 named module의 모든 exports된 package를 읽을 수 있다고 하였듯이 unnamed module인 UnnamedModuleClass에서 com.yours 모듈의 MyStringBuilder를 아무 문제없이 import하여 사용하고 있으며 결과도 정상적으로 출력함을 확인할 수 있다.또한 unnamed module은 자신이 가지는 모든 package들을 export한다. 하지만 named module은 이 unnamed module을 읽을 수(requires) 없다. 이것은 module system에서 의도한 것으로 모듈간의 상호 정의된 규약(requires, exports)을 통해서만 접근을 허용함으로써 reliable configuration을 보장하기 위한 것이다.
아래와 같이 named module인 com.yours와 unnamed module을 재 정의해보자.
|-java9_project
├─com.yours
│ └─src
│ └─dau
│ └─been
│ └─ModuleTest.java
└─src
└─unnamed
└─module
└──UnnamedModuleClass2.java
UnnamedModuleClass2.java
package unnamed.module;
public class UnnamedModuleClass2 {
private boolean unnamed = true;
public boolean isUnnamed() {
return unnamed;
}
}
아래와 같이 ModuleTest.java를 작성하면 compile error가 발생한다. named module은 unnamed module을 읽을 수 없기 때문이다.
package dau.been;
public class ModuleTest {
public static void main(String[] args) {
UnnamedModuleClass2 unnamedClass = new UnnamedModuleClass2();
System.out.println(unnamedClass.isUnnamed());
}
}
=> 결과
Compile Error!
만일 어떤 모듈이 읽으려고 선언한(requires) 모듈의 특정 package가 unnamed module, named module 모두에 존재한다면 unnamed module의 package 내용은 무시된다.1.2.8 Automatic Module
위에서 java8이하에서 작성된 unnamed module이 가지는 package들을 java9 모듈들은 읽을 수 없다고 했다. 하지만 이 package들을 java9 모듈들이 접근할 수 없다면 java9에서는 java8이하에서 생성된 library를 전혀 사용할 수 없을까? 당연히 그렇지 않다. Module System에서는 java8이하의 library(일반적으로 jar, 사실 이들은 최소한 이름은 가지고 있으므로 unnamed module은 아니다.)들을 별다른 작업 없이도 java9 환경에서 사용할 수 있도록(자동으로 참조할 수 있도록) automatic module로 정의하고 있다.com.yours 모듈을 포함하는 java project의 lib 디렉토리에 아래와 같이 java8 이하에서 생성된 jar 파일들을 추가해보자.
├─ java9_module
├── com.yours
│ └── src
│ ├── dau
│ │ └── been
│ │ ├── AutomaticModuleTest.java
│ └── module-info.java
└── lib
├── jackson-annotations-2.8.11.jar
├── jackson-core-2.2.3.jar
└── jackson-databind-2.1.4.jar
module-info.java
module com.yours {
requires jackson.databind; // jackson-databind-2.1.4.jar의 ObjectMapper 클래스를 이용하기 위함
requires java.sql;
}
AutomaticModuleTest.java
package dau.been;
import com.fasterxml.jackson.databind.ObjectMapper;
public class AutomaticModuleTest {
public static void main(String[] args) throws Exception {
com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
Student student = new Student("Jason", "dhyun", "A");
System.out.println(mapper.writeValueAsString(student));
}
static class Student {
private String name;
private String id;
private String grade;
Student(String name, String id, String grade) {
this.name = name;
this.id = id;
this.grade = grade;
}
public String getName() {
return name;
}
public String getId() {
return id;
}
public String getGrade() {
return grade;
}
}
}
=> 결과
{"name":"Jason","id":"dhyun","grade":"A"}
java9의 module과 automatic module간의 작업이 문제없이 수행됨을 알 수 있다.automatic module은 자신의 모든 package를 exports하며, 또한 module path에 있는 모든 모듈들을 requires 한다. 이러한 정책을 가져감으로써 java9은 java8 이하의 모듈들의 java9으로의 migration을 보장하고 있다.
2. Java9 Language Changes
2.1 Improvement of try-with-resources statements
try-with-resources는 java8부터 도입된 개념으로 사용 후 close 되어야 하는 resource에 대해 명시적인 close() 호출없이도 자동으로 resource가 반환될 수 있도록 해주는 안전장치라고 할 수 있다.
java8 이전
void printFile(String filePath) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(new File(filePath)));
String line;
try {
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} finally {
if (br != null) {
br.close();
}
}
java8void printFile(String filePath) throws IOException {
BufferedReader br1 = new BufferedReader(new FileReader(new File(filePath)));
String line;
try (BufferedReader br2 = br1) {
while ((line = br2.readLine()) != null) {
System.out.println(line);
}
}
}
java8에서는 위와 같이 명시적인 close() 메소드 호출 없이도 resource 반환이 이루어진다.(Exception 발생 여부와 관계없이) java9에서는 BufferedReader br2 = br1; 과 같이 resource를 재할당하는 구문조차 생략이 가능해졌다.
void printFile(String filePath) throws IOException {
BufferedReader br1 = new BufferedReader(new FileReader(new File(filePath)));
String line;
try (br1) {
while ((line = br1.readLine()) != null) {
System.out.println(line);
}
}
}
2.2 Deprecation
java9에서는 이전 버전보다 좀 더 향상된 deprecation 정의를 제공한다. java8이하에서는 특정 API가 deprecated 되더라도 취소선만 표시될 뿐 실제로 그 API가 삭제되는 경우는 없었는데 java9에서는 실제로 삭제될 것임을 명시하여 사용자에게 좀 더 신중한 migration 계획을 세우도록 예고할 수 있게 되었다. deprecation에 대한 표기는 이전 버전과 같이 @Deprecated 어노테이션을 사용한다.
Deprecation in java9
@Deprecated(since="[version]")
[version] 값은 해당 API가 deprecated된java version을 나타내며 default값은 empty string("") 이다.
@Deprecated(forRemoval=[boolean])
forRemoval flag는 향후 해당 API의 삭제 여부를 나타내며 default 값은 false 이다.
아래 코드는 java1.5 버전부터 deprecated된 Thread.destroy() 메소드가 향후 삭제될 것임을 알려주고 있다.
public class Thread implements Runnable {
...
@Deprecated(since="1.5", forRemoval=true)
public void destroy() {
...
}
...
}
2.3 Immutable Collections
2.3.1 Immutable Collections
java에서 immutable이란 말그대로 '변하지 않는' 것들을 의미한다. 대표적으로 String 클래스가 있다. 아래 코드를 실행해보면 결과가 바뀌지 않는 것을 알 수가 있는데, 그 이유는 String은 value자체를 변경하는 것이 아니라 새로운 String instance를 생성하여 value를 새롭게 할당하는 Immutable class이기 때문이다.public static void main(String[] args) {
String str = "Korea : Sweden = 3 : 2";
str.toUpperCase();
System.out.println(str);
}
=> 결과
Korea : Sweden = 3 : 2
Set, List, Map과 같은 Collection 객체들도 Immutable로 생성할 수 있는데, java8에서는 아래와 같이 생성하였다.
List strList = Arrays.asList("s", "t", "r");
strList = Collections.unmodifiableList(strList);
Set strSet = new HashSet<>(Arrays.asList("s", "t", "r"));
strSet = Collections.unmodifiableSet(strSet);
Map strMap = new HashMap<>();
strMap.put("s", "m");
strMap.put("t", "a");
strMap.put("r", "p");
strMap = Collections.unmodifiableMap(strMap);
이렇게 생성된 immutable list는 변경할 수 없으며, 변경을 시도하면 아래와 같이 Exception이 발생한다.
strList.add("a);
=> Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1056)
java9에서는 이보다 짧게, 아래와 같이 생성이 가능해졌다.
List strList = List.of("s", "t", "r");
Set strSet = Set.of("s", "t", "r");
Map strMap = Map.of("s", "m", "t", "a", "r", "p");
Immutable Map의 경우에는 아래와 같이 생성할 수도 있다.
Map strMap = Map.ofEntries(Map.entry("s", "m"), Map.entry("t", "a"), Map.entry("r", "p"));
2.3.2 Iteration
일반적으로 HashSet, HashMap으로 생성된 collection의 iteration은 순서가 변하지 않는다.
Set intSet = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
Map intMap = new HashMap<>();
intMap.put("a", 1);
intMap.put("b", 2);
intMap.put("c", 3);
System.out.println(intSet);
System.out.println(intMap);
=> 결과
[1, 2, 3, 4, 5]
{a=1, b=2, c=3}
하지만 아래와 같이 Set.of(), Map.of()로 생성된 collection은 iteration이 순차적으로 일어나지 않을 수 있음에 유의할 필요가 있다.
Set intSet = Set.of(1, 2, 3, 4, 5);
Map intMap = Map.ofEntries(Map.entry("a", 1), Map.entry("b", 2), Map.entry("c", 3));
System.out.println(intSet);
System.out.println(intMap);
=> 결과
[5, 4, 3, 2, 1]
{c=3, b=2, a=1}
2.3.3 Immutable vs Unmodifiable
Immutable Object는 기본적으로 '불변'을 원칙으로 하지만, immutable collection들은 반드시 그렇지만은 않다. immutable collection이 가지는 element들이 immutable하지 않은 경우가 많기 때문이다.
List mutableList = new ArrayList<>();
mutaleList.add("eye");
mutableList.add("nose");
List> immutableList = List.of(mutableList);
System.out.println(immutableList);
mutableList.add("mouth");
System.out.println(immutableList);
=> 결과
[[eye, nose]]
[[eye, nose, mouth]]
위와 같이 immutableList의 element는 얼마든지 바뀔 수 있다. 하지만 immutable collection을 직접 수정하려고 하는 시도는 Exception(UnsupportedOperationException)을 발생시키므로 'immutable'의 개념은 가지고 있다고 할 수 있다. 이렇게 mutable object를 가지는 immutable collection을 사용하는 것은 예기치 못한 상황을 발생시킬 수 있으므로 사용에 주의해야 한다. 가장 좋은 방법은 immutable object를 element로 가지는 immutable collection을 사용하는 것이다. Immutable collection은 기본적으로 thread-safe 하고, java9에서의 immutable collection은 메모리 절약에도 큰 일조를 하므로 제대로만 사용한다면 유용한 도구가 될 것이라 생각된다.
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class SizeCompare {
public static void main(String[] args) {
SizeCompare sc = new SizeCompare();
Set oldSet = new HashSet<>();
oldSet.add("korea");
oldSet.add("mexico");
oldSet = Collections.unmodifiableSet(oldSet);
Set newSet = Set.of("korea", "mexico");
System.out.println("size of oldSet(bytes) = " + sc.sizeOf(oldSet));
System.out.println("size of newSet(bytes) = " + sc.sizeOf(newSet));
}
private long sizeOf(Object object){
if (object == null) {
return 0;
}
SizeCounter counter = new SizeCounter();
try (counter) {
ObjectOutputStream oos = new ObjectOutputStream(counter);
oos.writeObject(object);
return counter.getSize();
} catch (IOException e){
return -1;
}
}
class SizeCounter extends OutputStream {
private long size = 0;
@Override
public void write(int b) {
size++;
}
@Override
public void write(byte[] b, int off, int len) {
size += len;
}
public long getSize() {
return size;
}
}
}
=> 결과
size of oldSet(bytes) = 212
size of newSet(bytes) = 72
위와 같이 기존의 new HashSet<>(), Collections.unmodifiableSet() 으로 생성하는데에 사용되는 heap 메모리의 1/3 정도의 space로 collection 생성이 가능하다. 이 경우 불필요한 Wrapper(unmodifiable set), HashSet등의 object 생성을 생략하기 때문이다.
2.4 그 밖의 변화들
2.4.1 @SafeVarargs
@SafeVarargs 어노테이션은 일반적으로 static method나 final instance method 등 override 할 수 없는 method에 적용된다. java9에서는 private instance method에도 적용하는 것이 가능해졌다.
public class MyStringBuilder {
private StringBuilder sb;
public MyStringBuilder() {
sb = new StringBuilder();
}
@SafeVarargs
private void appendStrings(String... strings) {
Arrays.asList(strings).forEach(e -> sb.append(e));
}
}
2.4.2 Underscore character in field
변수명에 under score('_')를 사용하는 것이 불가능해졌다.
2.4.3 private interface method
private interface method를 선언하는 것이 가능해졌다. 이는 interface 내의 method 들이 코드를 공유할 수 있게 하기 위함이다.
2.4.4 Diamond operator for anonymous class
public abstract class MyClass {
private T data;
MyClass(T data) {
this.data = data;
}
public T getData() {
return data;
}
abstract void doSomething();
}
위와 같은 abstract class가 있다고 가정하면 java8의 anonymous class에서는 아래와 같이 사용하였다.(diamond operator를 사용할 수 없었음)
public class DiamondOperatorTest {
public static void main(String[] args) {
MyClass<String> stringClass = new MyClass<String>("abc") {
@Override
void doSomething() {
System.out.println(getData());
}
};
stringClass.doSomething();
}
}
하지만 java9에서는 아래와 같이 사용가능해졌다.
MyClass<String> stringClass = new MyClass<>("abc");
MyClass<Integer> intClass = new MyClass<>(1);
3. JDK New Tools
3.1 jshell
java9에는 REPL(Read-Eval-Print Loop) tool에 해당하는 jshell 이 추가 되었다. REPL tool은 command line interface에서 code를 입력하면서 line-by-line 코딩을 하고 그 결과를 즉시 확인할 수 있는 tool 이다. 아래의 명령어로 실행할 수 있다.실행
D:\work\java\jdk-9.0.4\bin>jshell
| Welcome to JShell -- Version 9.0.4
| For an introduction type: /help intro
jshell>
jshell 실행시 자동으로 수행되는 코드가 있는데, 이것을 확인하려면 아래와 같이 입력한다.
jshell> /list -start
s1 : import java.io.*;
s2 : import java.math.*;
s3 : import java.net.*;
s4 : import java.nio.file.*;
s5 : import java.util.*;
s6 : import java.util.concurrent.*;
s7 : import java.util.function.*;
s8 : import java.util.prefs.*;
s9 : import java.util.regex.*;
s10 : import java.util.stream.*;
jshell>
위와 같이 import statement들이 기본적으로 수행됨으로써, 다양한 java coding을 통한 테스트를 수행할 수 있다. jshell을 통해서는 변수 선언, method 선언, class 선언 등의 다양한 coding을 수행할 수 있으며 아래와 같은 결과를 확인할 수 있다.
jshell> String str1 = "baseball"
str1 ==> "baseball"
jshell> String str2 = "basketball"
str2 ==> "basketball"
jshell> String toKoreanLeague(String name) {
...> if ("baseball".equals(name)) {
...> return "KBO";
...> } else if ("basketball".equals(name)) {
...> return "KBL";
...> }
...> return "hmm";
...> }
| created method toKoreanLeague(String)
jshell> toKoreanLeague(str1);
$4 ==> "KBO"
작성한 함수나 클래스를 수정하고 싶으면 아래와 같이 입력한다. 입력 후 JShell Edit Pad가 launch 되어 직접 수정이 가능하다.jshell> /edit toKoreanLeague
3.2 jlink
jlink tool은 java runtime image를 생성하는 도구로서, image를 최적화된 상태(dependencies optimized)로 조합(assemble)해준다. 최적화된 상태란 deploy 될 image 구조의 단순화, 파일 size 축소 등을 의미한다.
jlink [options] --module-path modulepath --add-modules module [,module...] --output name
modulepathlink할 module (source)이 존재하는 경로로서 jar, jmod 파일뿐 아니라 exploded type의 모듈도 가능하다.
module
runtime 이미지에 추가될 module, module 및 그들의 dependency 정보도 추가된다.
output
이미지가 생성될 경로
jlink options
- --add-modules mod [,mod...]
- 생성될 이미지에 추가될 모듈들을 정의한다.
- --bind-services
- service provider module들과 그 dependency를 link한다.
- -c or --compress
- size를 축소하기 위한 압축 방법을 선택
- 0 - 압축하지 않음
- 1 - String Constant들을 공유함
- 2 - zip
- -h or --help
- 도움말 출력
- --launcher command=module(module/main)
- 생성될 이미지의 실행 명령어 또는 main class를 지정한다.
- -p or --module-path modulepath
- jlink가 탐색할 모듈의 path를 지정한다.
- --output path
- 실행 결과로 생성될 이미지의 경로
- --suggest-providers [name, ...]
- 주어진 modulepath에서 특정 type에 대한 provider들을 보여준다.
Example
D:\work\java\jdk-9.0.4\bin>jlink --module-path D:\work\java\jdk-9.0.4\jmods
--add-modules java.base,java.sql --output result
위의 command는 jdk에서 제공하는 java.base와 java.sql 모듈을 link하여 그 결과를 result 디렉토리에 생성하는 명령어이다. result 디렉토리로 이동하여 다음 명령어를 입력하면 result 이미지가 어떤 module들을 포함하고 있는지 확인할 수 있다.D:\work\java\jdk-9.0.4\bin\result>bin\java --list-modules
java.base@9.0.4
java.logging@9.0.4
java.sql@9.0.4
java.xml@9.0.4
아래와 같이 compress 옵션을 사용하여 사용하지 않은 결과물과의 size 차이를 알수 있다.
D:\work\java\jdk-9.0.4\bin>jlink --module-path D:\work\java\jdk-9.0.4\jmods
--add-modules java.base,java.sql
--compress=2 --output compressed_result
compress 옵션을 사용하지 않은 경우 result 디렉토리의 경우 47.4MB 정도의 사이즈인데, compressed_result의 경우는 29.0MB로 size가 많이 줄어들었음을 알 수 있다.module path의 특정 Provider를 구현한 provider들을 확인하고 싶은 경우 아래와 같이 하면 된다.
D:\work\java\jdk-9.0.4\bin>jlink --module-path D:\work\java\jdk-9.0.4\jmods
--suggest-providers java.security.Provider
Suggested providers:
java.naming provides java.security.Provider used by java.base
java.security.jgss provides java.security.Provider used by java.base
java.security.sasl provides java.security.Provider used by java.base
java.smartcardio provides java.security.Provider used by java.base
java.xml.crypto provides java.security.Provider used by java.base
jdk.crypto.cryptoki provides java.security.Provider used by java.base
jdk.crypto.ec provides java.security.Provider used by java.base
jdk.crypto.mscapi provides java.security.Provider used by java.base
jdk.deploy provides java.security.Provider used by java.base
jdk.security.jgss provides java.security.Provider used by java.base
이전 예제들이 Service Provider를 제외한 모듈간의 단순한 조합이었다면 아래 예제는 Service Provider와 그 dependency를 포함하는 command이다.
D:\work\java\jdk-9.0.4\bin>jlink --module-path D:\work\java\jdk-9.0.4\jmods
--add-modules java.base,java.sql --compress=2 --output result_with_provider --bind-services
결과물에 포함된 module들을 확인해보면 이전과 달리 많은 module들이 추가되었음을 알 수 있다.
D:\work\java\jdk-9.0.4\bin\result_with_provider>bin\java --list-modules
java.base@9.0.4
java.compiler@9.0.4
java.datatransfer@9.0.4
java.desktop@9.0.4
java.logging@9.0.4
java.management@9.0.4
java.management.rmi@9.0.4
java.naming@9.0.4
java.prefs@9.0.4
java.rmi@9.0.4
java.scripting@9.0.4
java.security.jgss@9.0.4
java.security.sasl@9.0.4
java.smartcardio@9.0.4
java.sql@9.0.4
java.xml@9.0.4
java.xml.crypto@9.0.4
jdk.accessibility@9.0.4
jdk.charsets@9.0.4
jdk.compiler@9.0.4
jdk.crypto.cryptoki@9.0.4
jdk.crypto.ec@9.0.4
jdk.crypto.mscapi@9.0.4
jdk.deploy@9.0.4
jdk.dynalink@9.0.4
jdk.internal.opt@9.0.4
jdk.jartool@9.0.4
jdk.javadoc@9.0.4
jdk.jdeps@9.0.4
jdk.jfr@9.0.4
jdk.jlink@9.0.4
jdk.localedata@9.0.4
jdk.management@9.0.4
jdk.management.cmm@9.0.4
jdk.management.jfr@9.0.4
jdk.naming.dns@9.0.4
jdk.naming.rmi@9.0.4
jdk.scripting.nashorn@9.0.4
jdk.security.auth@9.0.4
jdk.security.jgss@9.0.4
jdk.unsupported@9.0.4
jdk.zipfs@9.0.4
댓글
댓글 쓰기