본문 바로가기
Java

자바 모듈

by AlbertIm 2024. 12. 4.

시작

모듈은 자바 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

댓글