GraalVM

5 분 소요


RECOMMEND POSTS BEFORE THIS

0. 들어가면서

최근 참여한 SpringOne 조쉬 롱(Josh Long) 세션의 주요 아젠다는 스프링 부트(spring boot) 3.X 버전, JDK 21 그리고 GraalVM에 대한 이야기였습니다. “부팅 속도가 빠르다.”, “메모리가 적게 소요된다.” 같은 장점들을 이야기했지만, 그 좋은 기술을 아직 프로젝트 현장에서 접하지 못한 이유가 궁금했습니다. 어떤 기술인지, 어떤 한계점이 있는지, 지금 바로 사용할 수 있는지 관련된 내용을 찾아 정리해봤습니다.

1. GraalVM

GraalVM은 오라클 랩스(oracle labs)에서 진행 중인 프로젝트로 다양한 프로그래밍 언어를 통합하여 실행하고 성능을 최적화하는 목적으로 등장했습니다.

GraalVM compiles your Java applications ahead of time into standalone binaries. These binaries are smaller, start up to 100x faster, provide peak performance with no warmup, and use less memory and CPU than applications running on a Java Virtual Machine (JVM).

공식 홈페이지 GraalVM 소개를 보면 Java 어플리케이션을 AOT(Ahead of Time) 컴파일러를 통해 최적의 독립형 바이너리(binary) 파일로 만든다고 되어 있습니다. 독립형 바이너리 파일은 네이티브 이미지(native image)라고도 부르는데, GraalVM에서 강조하는 핵심 기술 중 하나입니다. 이 외에도 다양한 기능들을 제공합니다.

  • 네이티브 이미지
    • 메모리, 패키징 사이즈 최소화
    • 실행 시간 단축
  • Graal 컴파일러
    • C2 컴파일러를 대체하기 위한 GraalVM의 JIT(Just in Time) 컴파일러
  • 다중 언어 지원
    • C, C++, Scala, Kotlin, Groovy, Clojure, R, Python, JavaScript, Ruby 등 지원
    • Sulong, Truffle 등의 프레임워크를 사용해 GraalVM이 실행할 수 있는 바이트 코드(byte code) 생성

https://giljae.com/2021/04/13/GraalVM-%EC%86%8C%EA%B0%9C.html

2. Byte Code and Native Image

AOT 컴파일러를 통해 생성되는 결과물인 네이티브 이미지는 무엇인지 알아보겠습니다. 네이티브 이미지에 대한 이야기 이전에 바이트 코드를 먼저 살펴보겠습니다.

2.1. Byte Code

Java 어플리케이션은 다음과 같은 과정을 거쳐 실행됩니다.

  • 개발자가 소스 코드를 작성한다.
  • 컴파일러가 소스 코드를 바이트 코드로 컴파일한다.
  • JVM(Java Virtual Machine)의 인터프리터(interpreter)가 바이트 코드를 운영체제가 이해할 수 있는 기계어로 번역한다.
  • 운영체제는 기계어를 실행한다.

2.2. Native Image

GraalVM이 만든는 독립적인 바이너리 파일은 다음과 같은 구조를 가집니다.

  • 힙 메모리 이미지
  • 기계어
  • 어플리케이션 실행을 위 경량화 된 SubstrateVM

소스 코드는 AOT 컴파일러에 의해 기계어로 컴파일되지만, 이를 운영체제가 실행할 수 있도록 돕는 가벼운 가상 머신(virtual machine)이 필요합니다. Java 어플리케이션은 다음과 같은 과정을 거쳐 실행됩니다.

  • 개발자가 소스 코드를 작성한다.
  • 컴파일러가 소스 코드를 실행 파일(executable)로 빌드된다.
  • 운영체제가 실행 파일을 실행한다.

2.2.1. Native Image in Native Cloud

GraalVM을 공부하면서 느낀 것은 GraalVM이 추구하는 바가 Java가 처음 등장한 배경인 Write Once Run Anywhere과 정반대라는 점입니다. Java는 코드를 작성하고 한번 빌드하면 플랫폼에 종속되지 않고 어디서든 실행시킬 수 있습니다. GraalVM은 플랫폼에 최적화 된 컴파일을 수행하다보니 운영체제가 변경되면 어플리케이션을 다시 빌드해야합니다. 예전 C, C++ 처럼 플랫폼에 종속된 컴파일 기술이 다시 등장한 배경엔 클라우드(cloud) 기술 발전이 있습니다.

  • 컨테이너 베이스 이미지를 기준으로 빌드를 수행하므로 호스트 머신 플랫폼에 종속적이지 않다.
  • 클라우드 환경에선 컨테이너가 자주 생성되고 삭제되므로 가볍고 빠른 실행이 가능한 어플리케이션이 필요하다.
  • 작은 단위의 함수를 서비스처럼 사용하는 서버리스(serverless) 기술에 가볍고, 빠른 실행이 가능한 어플리케이션이 필요하다.

GraalVM의 네이티브 이미지는 이런 문제점을 해결합니다.

  • JVM이 필요한 리소스의 일부만 사용하기 때문에 실행 속도가 빠르다.
    • 수 밀리초(milliseconds)만에 실행된다.
  • 네이티브 바이너리로 작성되었기 때문에 웜업(warmup) 없이 최고의 성능을 즉시 제공한다.
  • 빠르고 효율적인 배포를 위해 경량 컨테이너 이미지로 패키징이 가능하다.
  • 불필요하거나 사용하지 않는 코드들은 제거하기 때문에 공격에 대한 노출을 최소화한다.

3. Compilers

GraalVM은 어플리케이션을 실행하는 두 가지 방법을 제공합니다. 사용하는 컴파일러에 따라 실행 방법이 바뀝니다. GraalVM이 사용하는 두 컴파일러에 대해 살펴보겠습니다.

  • Graal Compiler
    • C2 컴파일러를 대체하기 위해 Java로 개발된 JIT 컴파일러
  • AOT(Ahead Of Time) Compiler
    • 네이티브 이미지를 생성할 때 사용하는 컴파일러

3.1. Graal Compiler

3.1.1. Just In Time Compiler

JIT 컴파일러의 개념을 먼저 살펴보겠습니다. Java 어플리케이션이 실행되면 바이트 코드는 인터프리터를 통해 매번 기계어로 번역된 후 실행됩니다. Java가 C, C++ 같은 컴파일 언어보다 느린 이유는 바이트 코드를 기계어로 번역하는 과정 때문입니다. 실행 속도의 격차를 줄이기 위해 JIT 컴파일러가 등장합니다.

In computing, just-in-time (JIT) compilation (also dynamic translation or run-time compilations) is a way of executing computer code that involves compilation during execution of a program (at run time) rather than before execution.

JIT 컴파일러는 어플리케이션 런타임 중 자주 실행되는 바이트 코드 영역을 기계어로 컴파일합니다. 디바이스 정보나 CPU 클럭 같은 프로파일링 정보를 활용해 컴파일 최적화를 수행합니다. JIT 컴파일러 덕분에 최고 성능(throughput) 측면에서 Java가 C, C++ 같은 컴파일 언어를 능가할 때도 있다고 합니다. Java 어플리케이션이 실행 초반에 속도가 느리고 웜업이 된 후 빨라지는 이유이기도 합니다.

3.1.2. Substitute C2 Compiler

GraalVM 프로젝트는 HotSpot 가상머신의 몇 가지 문제를 해결하기 위해 Java로 GraalVM 전용 컴파일러를 직접 만들었습니다. HotSpot JVM에서 사용하는 C1, C2 두 개의 JIT 컴파일러 중 C2를 대체할 Graal 컴파일러를 개발합니다.

  • Java 기반으로 개발
    • C++ 언어를 사용하는 개발자가 줄어들어 개발, 유지보수가 어렵다.
    • 현재는 Java도 충분히 빠르다.
  • 더 이상 어려운 C2 컴파일러 최적화
    • 오랜 기간 유지 보수되어 컴파일러의 복잡도가 너무 높다.
    • 거의 모든 최적화 작업을 수행하여 더이상 최적화할 수 있는 여지가 없다.

https://dzone.com/articles/episode-2-quotthe-holy-grailquot-graalvm-building

3.2. Ahead Of Time Compiler

네이티브 이미지는 네이티브 이미지 생성기(native image generator)에 의해 생성됩니다. native-image라는 도구를 설치하면 이미지 생성기를 사용할 수 있습니다. 이미지 생성기는 내부에서 AOT 컴파일러를 사용합니다. 네이티브 이미지를 생성하는 과정은 두 단계에 거쳐 컴파일이 수행됩니다.

  • javac 컴파일러를 통해 바이트 코드 혹은 jar 파일 생성
  • 네이티브 이미지 생성기를 통해 네이티브 이미지 생성
    • 컴파일 된 파일을 클래스 로더를 통해 로딩합니다.
    • 정적 코드 분석을 수행합니다.
    • 분석 결과를 힙 이미지라는 형태의 파일로 만들고 실행할 때 그대로 사용합니다.

이미지 생성기에 의해 만들어진 네이티브 이미지는 다음과 같은 구조를 가집니다.

  • 힙 메모리 이미지
  • 기계어
  • 어플리케이션 실행을 위 경량화 된 SubstrateVM

http://taewan.kim/graalvm/not_found_native-image_graalvm/

3.2.1. Performance Limitation of Native Image

AOT 컴파일러는 정정 코드 분석을 통해 실행 가능한 파일을 미리 만들기 때문에 빠른 실행 속도, 적은 메모리 사용, 작은 패키징 사이즈 등의 장점이 있지만, 런타임 중 최적화를 수행하는 JIT 컴파일러에 비해 최대 처리율은 떨어집니다.

AOT Compiler VS JIT Compiler

https://mangkyu.tistory.com/302

3.2.2. Dynamic Features Limitation of Native Image

정적 코드 분석을 통해 미리 기계어로 컴파일을 진행하기 때문에 Java의 강력한 동적 기능들을 사용하는데 제약이 있는 것 같습니다.

  • Accessing Resources
  • Certificate Management
  • Dynamic Proxy
  • Java Native Interface (JNI)
  • JCA Security Services
  • Reflection
  • URL Protocols

Java 동적 기능들을 지원하지만, 힌트(hint)를 별도로 작성해야하 하는 불편함이 있다고 합니다. 스프링 프레임워크는 내부적으로 리플렉션(reflection)이나 동적 프록시(dynamic proxy) 기능을 많이 사용할 텐데 이를 어떻게 지원하는지에 대해선 다음 포스트로 정리할 예정입니다.

3.4. Compilers in OpenJDK

OpenJDK는 실험적으로 Graal JIT 컴파일러와 AOT 컴파일러를 추가헀다가 현재는 제외하였습니다.

  • JEP 243 - Java-Level JVM Compiler Interface
  • JEP 295 - Ahead-of-Time Compilation
  • JEP 317 - Experimental Java-Based JIT Compiler
  • JEP 410 - Remove the Experimental AOT and JIT Compiler

4. GraalVM as a Language Platform

GraalVM은 다양한 프로그래밍 언어를 실행할 수 있는 플랫폼 기능을 제공합니다. 다음과 같은 구조를 가집니다.

  • Java HotSpot VM
    • JVM 환경 제공
    • JVMCI(JVM Compiler Interface)를 통해 필요한 컴파일러 모듈을 선택적으로 사용
  • Graal Compiler
    • Java 기반으로 만들어진 JIT 컴파일러
  • Truffle Framework
    • JVM 기반이 아니었던 언어들을 번역하기 위한 프레임워크
  • Sulong
    • C, C++, Rust 같은 저수준 언어를 GraalVM에서 실행시키기 위한 LLVM 기반 컴파일러

https://www.graalvm.org/community/assets/

RECOMMEND NEXT POSTS

REFERENCE

댓글남기기