시작
모듈은 자바 9부터 추가된 기능이다. 모듈의 도입은 애플리케이션 아키텍처에 깊은 영향을 미친다. 그래서 모듈을 이해하고 사용하는 것은 중요하다고 생각하여 이 글을 작성하게 되었다.
본문
배경
모듈은 런타임에 의미를 가지는 응용 프로그램 배포 및 의존성의 단위다.
이는 다음과 같은 자바 개념과 다르다.
- JAR 파일은 런타임에는 보이지 않으며 단순히 클래스 파일들을 포함하고 있는 압축된 디렉터리다.
- 패키지는 실제로 접근 제어를 위해 클래스를 그룹화하기 위한 네임스페이스다.
- 의존성은 클래스 레벨에서만 정의한다
- 접근 제어와 리플렉션이 결합돼 명확한 배포 단위 경계 없이 최소한의 시행으로 개방적인 시스템을 생성한다.
반면에 모듈은 다음과 같은 특징을 가진다.
- 모듈은 모듈 간의 의존성 정보를 정의하므로 컴파일 또는 애플리케이션 시작 시점에서 모든 종류의 해결(resolution)과 연결(linkage) 문제를 감지할 수 있다.
- 적절한 캡슐화를 제공해서 내부 패키지와 클래스를 조작하려는 사용자로부터 안전하게 보호할 수 있다.
- 최신 자바 런타임에서 이해하고 사용할 수 있는 메타데이터가 포함된 적절한 배포 단위이며 자바 타입 시스템에서 표현한다.
프로젝트 직소(Project Jigsaw)
이 프로젝트는 다음과 같은 목표를 가지고 있다.
- JDK 플랫폼 소스 모듈화하기
- 프로세스 풋프린트 줄이기
- 애플리케이션 시작 시간 개선하기
- JDK와 애플리케이션 코드에서 모듈 사용할 수 있게 하기
- 자바에서 처음으로 진정한 의미의 엄격한 캡슐화 허용
- 이전에는 불가능했던 새로운 접근 제어 모드를 자바 언어에 추가하기
이러한 목표는 다시 JDK와 자바 런타임에 더욱 밀접하게 초점을 맞춘 후 다은과 같은 2차 목표로 추진됐다.
- 단일 모놀리식 런타임 JAR(rt.jar) 끝내기
- JDK 내부를 적절히 캡슐화해서 보호하기
- 외부에 영향 없이 주요 내부의 변경 가능하게 하기(승인되지 않은 비 JDK 사용을 막는 변경 포함)
- 모듈을 슈퍼 패키지로 도입하기
아래 명령어로 모듈을 확인할 수 있다.
$ java --list-modules
java.base@11.0.18
java.compiler@11.0.18
java.datatransfer@11.0.18
java.desktop@11.0.18
java.instrument@11.0.18
java.logging@11.0.18
java.management@11.0.18
java.management.rmi@11.0.18
java.naming@11.0.18
java.net.http@11.0.18
...
모놀리식은 아닌 모듈식 자바 런타임
모듈은 프로그램의 생명 주기에서 서로 다른 시점(각각 컴파일/링크 타임과 런타임)에 사용되는 두 가지 새로운 형식(JMOD 및 JIMAGE)을 제공한다
- JMOD 형식은 기존 JAR 파일과 유사하지만 자바 8에서처럼 별도의 공유 객체 파일을 제공하지 않고 네이티브 코드를 단일 파일의 일부로 포함한다.
- JIMAGE 형식은 자바 런타임 이미지의 내부 구조를 설명하는 새로운 형식이다. 이 형식은 모듈화 된 JMOD 파일을 사용하여 자바 런타임 이미지를 생성한다.
jimage
도구를 사용하면 자바 런타임 이미지의 내부 구조를 살펴볼 수 있다.
$ jimage info $JAVA_HOME/lib/modules
Major Version: 1
Minor Version: 0
Flags: 0
Resource Count: 33969
Table Length: 33969
Offsets Size: 135876
Redirects Size: 135876
Locations Size: 706305
Strings Size: 775253
Index Size: 1753338
또는
$ jimage list $JAVA_HOME/lib/modules
...
Module: jdk.internal.vm.compiler.management
META-INF/providers/org.graalvm.compiler.hotspot.management.HotSpotGraalManagement
META-INF/providers/org.graalvm.compiler.hotspot.management.JMXServiceProvider
module-info.class
org/graalvm/compiler/hotspot/management/HotSpotGraalManagement$RegistrationThread.class
org/graalvm/compiler/hotspot/management/HotSpotGraalManagement.class
org/graalvm/compiler/hotspot/management/HotSpotGraalRuntimeMBean$1.class
org/graalvm/compiler/hotspot/management/HotSpotGraalRuntimeMBean.class
org/graalvm/compiler/hotspot/management/JMXServiceProvider.class
Module: jdk.internal.vm.compiler
META-INF/providers/org.graalvm.compiler.code.HexCodeFileDisassemblerProvider
META-INF/providers/org.graalvm.compiler.core.amd64.AMD64NodeMatchRules_MatchStatementSet
META-INF/providers/org.graalvm.compiler.core.sparc.SPARCNodeMatchRules_MatchStatementSet
...
rt.jar
에서 벗어남으로써 자바 런타임 이미지는 더 작아지고 더 빨라지며 더 캡슐화된다.
내부의 캡슐화
자바는 공개(public), 비공개(private), 보호된(protected) 및 패키지(package) 접근 제어를 제공한다. 이러한 접근 제어는 클래스와 인터페이스의 멤버에 적용된다.
하지만 리플렉션이나 다른 수단을 사용하면 이러한 접근 제어를 우회할 수 있다. 이는 자바의 캡슐화를 약화시키고 런타임에 더 많은 문제를 발생시킨다.
모듈은 이러한 문제를 해결하기 위해 새로운 접근 제어 모드를 도입한다. 이러한 모드는 다음과 같다.
open
모듈은 모든 패키지를 공개한다.opens
패키지는 모든 클래스를 공개한다.exports
패키지는 모든 클래스를 공개한다.exports ... to
패키지는 특정 패키지에만 클래스를 공개한다.uses
서비스를 사용한다.provides ... with
서비스를 제공한다.
module com.mycompany.myapp {
// openServices 패키지는 모든 클래스를 공개한다.
exports com.mycompany.myapp.api.openServices;
// util 패키지를 impl 패키지에만 공개한다.
exports com.mycompany.myapp.util to com.mycompany.myapp.impl;
// internal 패키지를 모든 클래스를 공개한다.
opens com.mycompany.myapp.internal;
// MyService 서비스를 사용한다.
uses com.mycompany.myapp.api.MyService;
// MyServiceImpl 서비스를 제공한다.
provides com.mycompany.myapp.api.MyService with com.mycompany.myapp.impl.MyServiceImpl;
}
각 모듈의 module-info.class
파일에 이러한 접근 제어 모드를 정의할 수 있다. 모듈에서 module-info.class
을 확인할 수 있다.
예시로 java.base
모듈의 module-info.class
파일을 확인할 수 있다.
모듈식 JVM
HelloWorld.java를 생성하고 다음과 같이 작성한다.
public class Main {
public static void main(String[] args) {
int result = Integer.parseInt("Fail");
}
}
그리고 다음과 같이 실행한다.
$ java HelloWorld.java
결과는 다음과 같다.
Exception in thread "main" java.lang.NumberFormatException: For input string: "Fail"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.main(Main.java:4)
스택 프레임은 이제 패키지 이름, 클래스 이름, 라인 번호뿐만 아니라 모듈 이름(java.base)도 포함한다.
Java 8의 결과는 다음과 같다.
Exception in thread "main" java.lang.NumberFormatException: For input string: "Fail"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at Main.main(Main.java:4)
모듈 생성 및 사용
1. hello
모듈을 생성한다.
2. hello
모듈에 module-info.java 파일을 생성한다.
module hello {
// util 모듈을 사용한다.
requires util;
}
3. hello
모듈에 HelloWorld.java 파일을 생성한다.
package me.service.hello;
import me.service.util.Utils;
public class HelloWorld {
public static void main(String[] args) {
System.out.println(Utils.getHelloWorld());
}
}
4. util
모듈을 생성한다.
5. util
모듈에 module-info.java 파일을 생성한다.
module util {
exports me.service.util;
}
6. util
모듈에 Utils.java 파일을 생성한다.
package me.service.util;
public class Utils {
public static String getHelloWorld() {
return "Hello world!";
}
}
7. 디렉터리 구조 확인
project/
├── hello/
│ └── src/
│ ├── module-info.java
│ └── me/service/hello/HelloWorld.java
├── util/
│ └── src/
│ ├── module-info.java
│ └── me/service/util/Utils.java
└── out/
8. util
모듈을 컴파일한다.
$ javac -d out/util util/src/module-info.java util/src/me/service/util/Utils.java
9. hello
모듈을 컴파일한다.
$ javac --module-path out/util -d out/hello hello/src/module-info.java hello/src/me/service/hello/HelloWorld.java
10. hello
모듈을 실행한다.
$ java --module-path out/util:out/hello --module hello/me.service.hello.HelloWorld
11. 결과는 다음과 같다.
Hello world!
jdeps
도구
jdeps
도구는 모듈 간의 의존성을 분석하는 데 사용된다.
$ jdeps out/hello/me/service/hello/HelloWorld.class
HelloWorld.class -> java.base
HelloWorld.class -> not found
me.service.hello -> java.io java.base
me.service.hello -> java.lang java.base
me.service.hello -> me.service.util not found
마무리
이로 인해 자바 모듈에 대한 이해를 높일 수 있었다. 모듈은 자바의 새로운 기능이며 애플리케이션 아키텍처에 깊은 영향을 미친다. 그래서 모듈을 이해하는 것은 중요하다. 이 글에서 간단하게 모듈을 생성하고 사용하는 방법을 정리했다.
참조
'Java' 카테고리의 다른 글
성능 튜닝의 중요성 (2) | 2024.12.15 |
---|---|
자바 동시성 기초 (1) | 2024.12.14 |
클래스 로딩 (1) | 2024.12.05 |
자바 17 (0) | 2024.12.04 |
자바 11에서의 작은 변경 사항 (0) | 2024.12.02 |
댓글