Spring Boot 3부터, GraalVM Native Image를 공식 지원하여 애플리케이션의 시작 속도와 메모리 사용량을 크게 줄일 수 있다.
Native Image란 기존의 JVM 기반 위에서 돌아가는 Java 애플리케이션과는 달리 JVM 없이 즉시 실행할 수 있는 바이너리 파일이다. (Native Java) Kotlin 언어로 SpringBoot 애플리케이션을 만들고, 이를 Native Image로 컴파일하여 실행하는 예제를 구현해본다.
1. 프로젝트 준비
먼저 간단하게 Kotlin으로 SpringBoot 3.x 버전으로 프로젝트를 만든다.
예제코드에는 h2, MySQL DB와의 접속 설정과 간단하게 JPA로 구현한 Create, Read 기능을 수행하는 API Endpoint 2개를 넣었다.
2. GraalVM JDK 설치
- 설치 URL: https://www.graalvm.org/downloads/
- 가이드 (macOS): https://www.graalvm.org/latest/getting-started/macos/
설치 가이드에서는 sdkman 이라는 유틸리티 사용을 권장하는데, 꼭 쓸 필요는 없다. JVM 버전 스위칭하기에는 편리한 프로그램이다.
GraalVM에는 Community Edition (CE) 버전과 Enterprise Edition (EE) 버전 2개가 있는데, 대부분의 경우는 꼭! CE 버전을 설치하여야 한다. EE 버전을 쓰다가 걸리면 오라클에서 돈 내놓으라고 찾아올 수도 있다.
macOS에서는 native-image 도구를 사용하기 위해 Xcode를 사용한다.
xcode-select --install
3. build.gradle.kts 설정
build.gradle.kts의 plugins 블록에 GraalVM의 native 관련 플러그인을 추가한다.

4. application.yaml 설정
application.yaml에서 Spring AOT (Ahead-of-Time)를 활성화한다.

5. 환경변수 GRAALVM_HOME 설정
환경변수 GRAALVM_HOME에 GraalVM의 설치 경로를 설정한다.
IntelliJ IDEA에서 실행할 경우, Task별로 Run Configuration에서 환경변수를 별도로 지정할 수 있다.
GRAALVM_HOME 환경변수가 설정되어 있지 않으면, Native Compile 시도 시 아래와 같은 에러가 발생하고 진행이 중단된다.

6. Native Image 컴파일
./gradlew nativeCompile 명령어로 Native Image 컴파일을 실행한다.
./gradlew nativeCompile
컴파일 로그 (요약):
========================================================================================================================
GraalVM Native Image: Generating 'native-image' (executable)...
========================================================================================================================
[1/8] Initializing... (24.2s @ 0.35GB)
Java version: 21.0.5+9-LTS, vendor version: Oracle GraalVM 21.0.5+9.1
Graal compiler: optimization level: 2, target machine: x86-64-v3, PGO: ML-inferred
C compiler: cc (apple, x86_64, 14.0.3)
Garbage collector: Serial GC (max heap size: 80% of RAM)
[2/8] Performing analysis... [******] (210.7s @ 3.72GB)
39,037 reachable types (91.9% of 42,491 total)
60,717 reachable fields (59.7% of 101,763 total)
213,249 reachable methods (64.1% of 332,639 total)
[3/8] Building universe... (67.9s @ 4.38GB)
[4/8] Parsing methods... [********] (67.4s @ 2.82GB)
[5/8] Inlining methods... [****] (7.3s @ 4.42GB)
[6/8] Compiling methods... [*******************] (374.9s @ 4.45GB)
[7/8] Laying out methods... [******] (40.6s @ 6.98GB)
[8/8] Creating image... [*****] (27.9s @ 5.67GB)
104.75MB (58.24%) for code area: 125,301 compilation units
74.16MB (41.23%) for image heap: 789,483 objects and 337 resources
965.87kB ( 0.52%) for other data
179.86MB in total
------------------------------------------------------------------------------------------------------------------------
Finished generating 'native-image' in 13m 47s.
BUILD SUCCESSFUL in 13m 53s
컴파일 시간은 일반 jar 빌드에 비해 대단히 오래 걸린다.
이 정도 볼륨의 프로젝트를 컴파일하는데 걸리는 시간은 약 12~13분.

컴파일이 완료되면 build/native/nativeCompile 경로에 바이너리 파일이 생성된 것을 확인할 수 있고, 해당 파일을 실행하면 애플리케이션이 구동된다.
7. 실행 속도 비교
JVM (jar) 실행 시

- 컨텍스트 초기화: 약 1.3초
- 애플리케이션 서비스 시작: 약 4.7초
Native Image 실행 시

- 컨텍스트 초기화: 약 0.048초
- 애플리케이션 서비스 시작: 약 0.302초
실제 운영 프로그램에 적용하면 물론 달라지겠지만, 이 예제의 경우 대략 93.5%의 속도 개선이 확인된다. (흠... 광팔기 정말 좋은 현상이다)
8. 운영 적용 시 고려사항
물론 실제 운영에 적용하기에는 몇 가지 취약점이 있어, 좀 더 검토가 필요할 것 같다.
- 컴파일 시간이 너무 오래 걸린다.
- 컴파일 환경과 실제 운영 환경이 완전히 동일해야 안심할 수 있을 것 같다.
- Native Image를 지원하지 않는 라이브러리가 있다고 한다.
- 운영 레퍼런스가 아직 절대 부족.
9. JVM 옵션 대체 방법
JVM 옵션이 Native Image에서는 먹혀들지 않으므로, 아래와 같이 대체하여야 한다.
- profile 지정:
SPRING_PROFILES_ACTIVE환경변수로 설정 - property 지정: 바이너리 실행 시 옵션으로 추가 (JVM 방식 → Native 방식)
# JVM 방식
-Dserver.port=8080
# Native Image 방식
--server.port=8080