GraalVM 으로 네이티브 이미지 만들기

GraalVM은 GraalVM 네이티브 이미지라는 AOT (ahead-of-time) 컴파일 기술을 제공합니다. 이는 자바 코드를 독립적으로 실행 가능한 네이티브 이미지로 컴파일해주는 기술입니다. GraalVM으로 컴파일된 실행 파일은 응용프로그램과 응용프로그램이 사용하는 라이브러리의 모든 클래스와 Substrate VM이라는 런타임을 내장합니다.

즉, 생성된 실행 파일은 JVM으로 구동하는게 아니라 자체 VM을 내장하고 있어서, 메모리 관리나 스레드 스케줄링 등의 모든 기능을 수행합니다.

이전 글

이 글에서는 공개 배포된 로그프레소 미니 실행 파일을 GraalVM으로 어떻게 빌드했는지 소개합니다.

윈도우 빌드 환경 세팅

  1. 윈도우용 GraalVM 설치 파일 다운로드
  2. 시스템 환경변수 설정
    • JAVA_HOME: graalvm 배포 파일에 포함된 JDK로 경로를 지정합니다.
    • PATH: graalvm 경로의 bin 디렉터리가 포함되도록 변경합니다.
  3. native-image 설치 압축을 해제하면 bin 디렉터리에 gu.cmd 파일이 있습니다. 이를 아래와 같이 실행하여 native-image 패키지를 설치합니다.

     cmd> gu install native-image
     Downloading: Component catalog from www.graalvm.org
     Processing Component: Native Image
     Downloading: Component native-image: Native Image  from github.com
     Installing new component: Native Image (org.graalvm.native-image, version 21.0.0.2)
    
    
  4. Microsoft Visual C++ 준비
    이 문서는 Visual Studio 2019 환경에서 작성되었지만, GraalVM은 아래의 윈도우 빌드 환경을 명시하고 있습니다.
    • JDK 8: MSVC 2010 SP1 버전 필요
    • JDK 11: MSVC 2017 15.5.5 이상의 버전 필요

리플렉션 설정

GraalVM 네이티브 이미지가 자바 바이트코드를 분석해서 의존성을 추출하긴 하지만, 응용 프로그램이 런타임에 동적으로 리플렉션을 호출하는 모든 경우의 수를 추적할 수 없기 때문에 리플렉션으로 호출되는 대상을 모두 설정으로 지정해야 합니다.

예를 들면 아래와 같이 reflect-config.json 파일을 구성합니다:

[
 { "name": "org.araqne.logdb.query.engine.QueryServiceImpl", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredFields": true, "allDeclaredMethods": true, "allPublicMethods": true},
 { "name": "org.araqne.logdb.query.engine.QueryParserServiceImpl", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredFields": true, "allDeclaredMethods": true, "allPublicMethods": true},
 ...
]

이 리플렉션 설정 추출 작업이 너무 힘들기 때문에 Tracing Agent를 사용하는 경우도 있으나, 런타임에 발견하지 못한 클래스가 누락되기 때문에 이 글에서는 설명하지 않습니다.

이미지 빌드

x64 Native Tools Command Prompt를 실행하고, 해당 명령 프롬프트에서 아래의 명령을 실행합니다. 어차피 같은 명령줄 아닌가 해서 Developer Command Prompt에서 빌드하면 컴파일 중 guessArchitecture에서 NullPointerException을 맞게 됩니다.

cmd> native-image -jar logpresso-mini-1.0.0-package.jar logpresso -H:-CheckToolchain -H:ReflectionConfigurationFiles=reflect-config.json -H:IncludeResources=(.*/.*EvtxMessages.json$"|".*/.*properties$) --no-fallback --enable-http --enable-https

[logpresso:38392]    classlist:   2,356.89 ms,  0.96 GB
[logpresso:38392]        (cap):   2,857.85 ms,  0.96 GB
[logpresso:38392]        setup:   6,108.98 ms,  0.96 GB
[logpresso:38392]     (clinit):     723.32 ms,  3.69 GB
[logpresso:38392]   (typeflow):  15,988.98 ms,  3.69 GB
[logpresso:38392]    (objects):  14,367.36 ms,  3.69 GB
[logpresso:38392]   (features):   1,497.97 ms,  3.69 GB
[logpresso:38392]     analysis:  33,515.24 ms,  3.69 GB
[logpresso:38392]     universe:   1,438.35 ms,  3.69 GB
[logpresso:38392]      (parse):   6,594.25 ms,  4.34 GB
[logpresso:38392]     (inline):   4,539.15 ms,  6.09 GB
[logpresso:38392]    (compile):  22,801.97 ms,  6.40 GB
[logpresso:38392]      compile:  36,019.61 ms,  6.40 GB
[logpresso:38392]        image:   3,698.44 ms,  6.40 GB
[logpresso:38392]        write:   1,230.76 ms,  6.40 GB
[logpresso:38392]      [total]:  84,567.52 ms,  6.40 GB

  • jar 옵션: AOT 컴파일 대상 JAR 파일을 지정합니다.
  • H:-CheckToolchain: 툴 체인 검사를 생략합니다. 이 옵션을 넣지 않으면 아래와 같이 IA64 미지원 오류 메시지를 보게 됩니다.

      Error: Native-image building on Windows currently only supports target architecture: AMD64 ((x64) unsupported)
      Error: To prevent native-toolchain checking provide command-line option -H:-CheckToolchain
      Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
      Error: Image build request failed with exit status 1
    
  • H:ReflectionConfigurationFiles: 리플렉션 클래스 설정 파일을 지정합니다.
  • H:IncludeResources: getResourceAsStream() 등으로 리소스를 읽어오는 경우, 생성되는 실행 파일에 리소스를 내장해야 합니다. 이를 위해 내장할 리소스 파일의 목록을 정규표현식으로 지정합니다.
  • -enable-http, --enable-https: 기본 빌드에서는 HTTP 통신 기능이 포함되지 않으므로, 별도로 옵션을 추가해야 합니다. (실행 파일 크기 약 13MB 증가)

정리

GraalVM 네이티브 이미지 기술이 아직까지는 일부 불안정하게 보이는 부분도 있고, 빌드 오류 메시지가 친절하지는 않아서 JAR 버전 의존성이 불일치한다거나 하는 경우에 원인을 모르고 헤매게 되는 경우들이 있습니다.

그렇지만 로그프레소 미니처럼 자바 기반의 기존 프로젝트를 별도의 JVM 없이 단 하나의 작은 실행 파일로 단순화하여 배포할 수 있다는 점에서는 매력적인 기술이라 생각합니다.

리눅스의 경우에는 상대적으로 손쉽게 빌드 환경을 구성할 수 있으니 자바 개발하신다면 한 번 네이티브 빌드를 시도해보세요. 데비안 계열을 쓰시는 경우에는 미리 빌드된 deb 패키지를 활용해보시면 좋습니다.

레퍼런스