JLink으로 런타임·애플리케이션 이미지 만들기

JLink으로 런타임·애플리케이션 이미지 만들기

jlink 명령줄 도구를 쓰면 플랫폼 모듈과 애플리케이션을 이루는 모듈 중 원하는 것들을 골라 **런타임 이미지(runtime image)**로 묶을 수 있어요. 이 이미지는 우리가 다운로드해 쓰는 JDK와 비슷하게 동작하지만, 직접 고른 모듈과 그 동작에 필요한 의존성만 담겨요.

출처: dev.java

본문

jlink 명령줄 도구로 모듈을 선택해 런타임 이미지로 링크할 수 있어요. 플랫폼 모듈뿐 아니라 애플리케이션을 구성하는 모듈도 포함할 수 있죠. 이런 런타임 이미지는 다운로드해서 쓰는 JDK처럼 동작하지만, 고른 모듈과 그것이 동작하는 데 필요한 의존성만 담고 있어요. 거기에 우리 프로젝트까지 포함하면, 결과는 애플리케이션의 자체 포함(self-contained) 산출물이 돼요. 즉 대상 시스템에 JDK가 설치되어 있지 않아도 동작해요.

링크 단계에서 jlink는 이미지 크기를 더 최적화하고, 특히 시작 시간을 비롯한 VM 성능을 개선할 수 있어요.

jlink 입장에서는 큰 차이가 없지만, 두 가지를 구분해 두면 이해하기 편해요. **런타임 이미지(runtime image)**는 JDK의 부분집합이고, **애플리케이션 이미지(application image)**는 프로젝트 특정 모듈도 함께 담아요. 그래서 그 순서대로 진행할게요.

참고: jlink는 바이트코드를 "그냥" 링크할 뿐, 기계 코드로 컴파일하지 않아요. 따라서 이건 AOT 컴파일(ahead-of-time compilation)이 아니에요.

런타임 이미지 만들기

이미지를 만들려면 jlink에 두 가지 정보가 필요한데, 각각 명령줄 옵션으로 지정해요.

  • 시작할 모듈 — --add-modules
  • 이미지를 만들 폴더 — --output

이 명령줄 옵션이 주어지면 jlink--add-modules에 나열된 모듈에서 시작해 모듈을 해석(resolve)해요. 다만 몇 가지 peculiarity(특이점)가 있어요.

  • 서비스(service)는 기본적으로 바인딩되지 않아요 — 아래에서 어떻게 처리하는지 볼게요.
  • 선택적 의존성(optional dependency)은 해석되지 않아요 — 수동으로 추가해야 해요.
  • 자동 모듈(automatic module)은 허용되지 않아요 — 애플리케이션 이미지에서 다룰게요.

누락되거나 중복된 모듈 같은 문제가 없다면, 해석된 모듈(루트 모듈 + 전이 의존성)이 새 런타임 이미지에 담겨요.

가장 작은 런타임

한번 볼게요. 가장 단순한 런타임 이미지는 기본 모듈만 담아요.


# create the image
$ jlink
    --add-modules java.base
    --output jdk-base
# use the image's java launcher to list all contained modules
$ jdk-base/bin/java --list-modules
> java.base

애플리케이션 이미지 만들기

앞서 말했듯이 jlink는 JDK의 모듈과 그 밖의 모듈을 구분하지 않아요. 그래서 비슷한 방식으로 애플리케이션 전체를 담은 이미지를 만들 수 있어요. 즉 애플리케이션 모듈(앱 자체와 그 의존성)과 그것을 지원하는 데 필요한 플랫폼 모듈을 담는 거죠. 그런 이미지를 만들려면 다음이 필요해요.

  • --module-path로 jlink가 앱 모듈을 어디서 찾을지 알려 주기
  • --add-modules에 애플리케이션의 메인 모듈과 필요 시 다른 모듈(예: 서비스, 아래 참고, 또는 선택적 의존성)을 나열하기

이미지에 담긴 플랫폼 모듈과 애플리케이션 모듈을 통틀어 **시스템 모듈(system modules)**이라고 불러요. jlink는 **명시적 모듈(explicit module)**에만 동작한다는 점에 주의하세요. 자동 모듈에 의존하는 애플리케이션은 이미지로 링크할 수 없어요.

선택적 모듈 경로

예를 들어 애플리케이션의 모듈들이 mods 폴더에 있고, 메인 모듈이 com.example.app이라고 가정할게요. 그러면 다음 명령이 app-image 폴더에 이미지를 만들어요.


# create the image
$ jlink
    --module-path mods
    --add-modules com.example.main
    --output app-image

# list contained modules
$ app-image/bin/java --list-modules
> com.example.app
# other app modules
> java.base
# other java/jdk modules

이미지가 애플리케이션 전체를 담고 있으므로, 실행할 때 모듈 경로를 사용할 필요가 없어요.


$ app-image/bin/java --module com.example.app/com.example.app.Main

모듈 경로를 꼭 안 써도 되지만, 쓸 수도 있어요. 그 경우 시스템 모듈은 항상 모듈 경로에서 같은 이름의 모듈을 가려요(shadow). 즉 모듈 경로에 있는 그것들은 존재하지 않는 것처럼 취급돼요. 그래서 모듈 경로로 시스템 모듈을 교체할 수는 없지만, 애플리케이션에 추가 모듈을 더할 수는 있어요. 그것들은 아마 서비스 제공자(service provider)일 텐데, 이를 통해 이미지를 애플리케이션과 함께 배포하면서도 사용자가 로컬에서 쉽게 확장할 수 있게 해 주는 거예요.

네이티브 런처 생성

애플리케이션 모듈은 **커스텀 런처(custom launcher)**를 포함할 수 있어요. 그것은 이미지의 bin 폴더에 있는 실행 스크립트(Unix 계열 OS에서는 shell, Windows에서는 batch)인데, 특정 모듈과 메인 클래스로 JVM을 시작하도록 미리 설정되어 있어요.

런처를 만들려면 --launcher $NAME=$MODULE/$MAIN-CLASS 옵션을 써요.

  • $NAME — 실행 파일로 고를 파일 이름
  • $MODULE — 함께 실행할 모듈 이름
  • $MAIN-CLASS — 모듈의 메인 클래스 이름

뒤의 두 개는 평소 java --module 뒤에 두는 것들이에요. 그리고 거기처럼 모듈이 메인 클래스를 정의하면 /$MAIN-CLASS를 생략할 수 있어요.

위 예시를 확장해서, app이라는 이름의 런처를 만드는 방법은 다음과 같아요.


# create the image
$ jlink
    --module-path mods
    --add-modules com.example.main
    --launcher app=com.example.app/com.example.app.Main
    --output app-image

# launch
$ app-image/bin/app

런처를 쓰는 데는 단점이 있어요. 실행 중인 JVM에 적용하려는 모든 옵션은 --module 옵션 뒤에 둔 것처럼 해석되어, 프로그램 인자(program argument)가 돼요. 즉 런처를 쓸 때는 java 명령을 임시로 설정할 수 없어요. 예를 들어 앞서 논의한 것처럼 서비스를 추가하는 일이 불가능해져요. 해결 방법 중 하나는 스크립트를 편집해서 그런 옵션을 JLINK_VM_OPTIONS 환경 변수에 넣는 거예요. 또 하나는 이미지에 여전히 존재하는 java 명령 자체로 돌아가는 거예요.

서비스 포함하기

작고 신중하게 조립된 런타임 이미지를 만들 수 있도록 jlink는 기본적으로 이미지를 만들 때 서비스 바인딩을 수행하지 않아요. 대신 서비스 제공자 모듈은 --add-modules에 나열해서 수동으로 포함해야 해요.

특정 서비스를 제공하는 모듈이 무엇인지 알아보려면 --suggest-providers $SERVICE 옵션을 쓰면 돼요. 이 옵션은 런타임 또는 모듈 경로에서 $SERVICE의 구현을 제공하는 모든 모듈을 나열해요. 개별 서비스를 추가하는 대안으로, --bind-services 옵션을 쓰면 다른 해석된 모듈이 사용하는 서비스를 제공하는 모든 모듈을 포함할 수 있어요.

예시로 ISO-8859-1, UTF-8, UTF-16 같은 charset을 골라 볼게요. 기본 모듈은 매일 필요한 것들을 알고 있지만, 몇 가지 추가 문자 집합을 담은 특정 플랫폼 모듈이 있어요. 바로 jdk.charsets예요. 기본 모듈과 jdk.charsets는 **서비스를 통해 결합(decouple)**돼요. 이게 관련 모듈 선언의 일부예요.


module java.base {
    uses java.nio.charset.spi.CharsetProvider;
}

module jdk.charsets {
    provides java.nio.charset.spi.CharsetProvider
        with sun.nio.cs.ext.ExtendedCharsets
}

일반 실행 중에 모듈 시스템이 모듈을 해석할 때는 서비스 바인딩이 jdk.charsets를 끌어들이므로, 표준 JDK에서 실행할 때는 charsets가 항상 사용 가능해요. 하지만 jlink로 런타임 이미지를 만들 때는 기본적으로 그렇게 되지 않아서, 그런 이미지는 charsets 모듈을 담지 않아요. 필요하다고 판단되면 --add-modules로 모듈을 이미지에 간단히 포함할 수 있어요.


$ jlink
    --add-modules java.base,jdk.charsets
    --output jdk-charsets
$ jdk-charsets/bin/java --list-modules
> java.base
> jdk.charsets

여러 운영체제용 이미지 생성

애플리케이션과 라이브러리 JAR이 담은 바이트코드가 운영체제(OS)와 독립적이기는 하지만, 그것을 실행하려면 OS에 특화된 JVM이 필요해요. 그래서 Linux, macOS, Windows용 JDK를 따로 다운로드하는 거죠. jlink가 플랫폼 모듈을 가져오는 곳이 바로 거기이고, 그 덕분에 jlink가 만드는 런타임·애플리케이션 이미지는 항상 특정 OS에 묶여 있어요.

다행히 그 OS가 반드시 우리가 jlink를 실행하는 OS일 필요는 없어요.

다른 운영체제용 JDK를 다운로드해 압축을 풀면, 우리 시스템의 JDK에서 jlink 버전을 실행할 때 그 jmods 폴더를 모듈 경로에 놓을 수 있어요. 그러면 링커는 그 다른 OS용 이미지를 만들어야 한다고 판단하고, 그 OS에서 동작하는(물론 다른 OS에서는 안 되는) 이미지를 만들어요. 따라서 애플리케이션이 지원하는 모든 OS용 JDK가 있다면, 같은 머신에서 각각에 대한 런타임·애플리케이션 이미지를 생성할 수 있어요.

문제없이 동작하게 하려면 jlink 바이너리와 정확히 같은 JDK 버전의 모듈만 참조하는 것이 좋아요. 예를 들어 jlink 버전이 16.0.2라면, JDK 16.0.2의 플랫폼 모듈을 로드하도록 확인하세요.

앞서 만든 애플리케이션 이미지로 돌아가서, Linux 빌드 서버에서 만들었다고 가정해 볼게요. 그러면 Windows용 애플리케이션 이미지를 만드는 방법은 이래요.


# download JDK for Windows and unpack into `jdk-win`

# create the image with the jlink binary from the system's JDK
# (in this example, Linux)
$ jlink
    --module-path jdk-win/jmods:mods
    --add-modules com.example.main
    --output app-image

이 이미지가 Windows 전용인지 확인하려면 app-image/bin을 보면 되는데, 거기에 java.exe가 들어 있어요.

이미지 최적화

애플리케이션용 이미지를 생성하는 방법을 배웠으니, 이제 그 이미지를 최적화할 수 있어요. 대부분의 최적화는 이미지 크기를 줄이고, 일부는 시작 시간을 조금 개선해요. jlink 레퍼런스에서 활용할 수 있는 옵션 전체 목록을 확인해 보세요. 어떤 옵션을 적용하든, 결과로 나온 이미지를 철저히 테스트하고 실제 개선을 측정하는 것을 잊지 마세요.

더 알아보기 (Learn more)

jlink로 만든 이미지의 크기를 줄이고 시작 시간을 개선하려면 공식 jlink 레퍼런스 문서에서 사용 가능한 최적화 옵션 전체를 확인해 보세요. 다중 OS 배포가 필요하다면 각 OS용 JDK의 jmods를 모듈 경로에 두고 같은 머신에서 이미지를 생성하는 흐름을 활용할 수 있어요.