블로그 Help

자바 모듈

시작

모듈은 자바 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 파일을 확인 할 수 있다.

module-info.png

모듈식 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

    결과는 다음과 같다.

    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

마무리

이로 인해 자바 모듈에 대한 이해를 높일 수 있었다. 모듈은 자바의 새로운 기능이며 애플리케이션 아키텍처에 깊은 영향을 미친다. 그래서 모듈을 이해하는 것은 중요하다. 이 글에서 간단하게 모듈을 생성하고 사용하는 방법을 정리했다.

참조

Last modified: 08 December 2024