Native Image
JVM을 공부하다가 JVM 없이도 Java 어플리케이션을 실행할 수 있다는 재밌는 사실을 발견했다. C언어를 컴파일하면 실행가능한 파일이 생성되는 것처럼, Java를 바이트코드가 아닌 실행가능한 파일로 컴파일할 수 있다는 뜻이다. 이러한 파일을 Native Image라고 한다. 이렇게 Native Image를 실행하면 JVM에서 실행하는 것이 아니기 때문에 시작 시간이 빠르며, 메모리를 아낄 수 있다고 한다.
GraalVM
Native Image를 만들기 위해서는 GraalVM이 필요하다. GraalVM은 기존에 C++로 작성된 JIT 컴파일러를 Java로 대체해 Java로 Java를 실행하는(meta-circular Java) 목표를 가지고 만든 JVM이다. 뿐만 아니라, 컴파일시 Native Image를 만드는 AOT(Ahead Of Time) 컴파일과 고성능, 다양한 언어를 지원하는 polyglot 프로그래밍 등이 특징이다. 그러나 아직은 Native Image를 지원하는 라이브러리가 많지는 않은 것 같다.
[참고] https://www.graalvm.org/native-image/libraries-and-frameworks/
GraalVM 설치
일단 Native Image를 만들기 위해 GraalVM을 설치해보자. 커뮤니티 22.3 버전을 설치했다.
[다운로드 링크] https://www.graalvm.org/downloads/
bash <(curl -sL https://get.graalvm.org/jdk)
Mac OS 카탈리나 이후 버전은 quarantine attribute를 삭제해야한다고 나온다. 찾아보니 quarantine은 파일에 표시하는 플래그의 일종으로, 인터넷에서 다운로드한 파일들을 함부로 실행하지 못하게 막는 것인 듯하다.
[참고] https://eclecticlight.co/2020/10/29/quarantine-and-the-quarantine-flag/
어쨌든, 현재 Mac OS Ventura를 사용중이기 때문에 아래 명령어를 실행했다.
sudo xattr -r -d com.apple.quarantine "/Users/jisang/graalvm-ce-java17-22.3.1/Contents/Home"
또, xcode가 없다면 xcode를 설치해줘야 한다.
xcode-select --install
그리고, GraalVM을 설치했으니 PATH와 JAVA_HOME 환경변수를 설정해준다.
export PATH="/Users/jisang/graalvm-ce-java17-22.3.1/Contents/Home/bin:$PATH"
export JAVA_HOME="/Users/jisang/graalvm-ce-java17-22.3.1/Contents/Home"
Native Image 만들기
[참고] https://www.graalvm.org/22.0/reference-manual/native-image/
본격적으로 이미지를 만들기 전에, 이미지를 만들 수 있는 도구가 필요하다. 직관적인 이름을 가진 Native Image가 그것이다. GraalVM을 설치했으면 GraalVM Updater를 사용할 수 있고, 이것을 이용해서 Native Image를 설치해준다.
gu install native-image
참고로 Native Image를 설치하기 위해서는 환경에 따라 미리 설치해야하는 것들이 있다. Mac OS는 xcode가 필요하기 때문에 위에서 미리 설치해둔 것이다.
[참고] https://www.graalvm.org/22.0/reference-manual/native-image/#prerequisites
이제, Native Image로 만들 Java 코드를 작성해준다. 간단하게 main 메소드만 만들도록 하겠다.
public class Test {
public static void main(String[] args) {
long result = 0;
for (int i = 1; i <= 1000000; i++) {
result += i;
}
System.out.println(result);
}
}
이제 javac로 컴파일한 다음 이미지를 만들면 된다. classpath를 잡아주려면 아래와 같은 명령어를 사용하면 된다.
javac -d ./out Test.java
native-image -classpath ./out Test
간단한 어플리케이션인데도 33.8초나 걸렸다. 확인해보면 test라는 실행파일과 생겼다.
실행파일이 잘 되는지 확인해보자.
참고로, Native Image 생성 과정을 살펴보면 가비지 컬렉터가 Serial GC로 되어있는 것을 확인할 수 있다. 실제로 현재 사용가능한 GC 알고리즘은 Serial GC와 Epsilon GC인데, Epsilon GC는 GC를 하지 않고 힙이 가득차면 종료하는 알고리즘이다.
Spring Native Image
Native Image는 Spring에서도 사용가능하다. Spring Initializer의 dependency 중에 'GraalVM Native Support'를 추가해주면 된다. 간단한 API를 만들어서 테스트해보자.
먼저, Spring Initializer에서 GraalVM Native Support와 Spring Web을 추가하고, 프로젝트를 생성했다. 생성된 프로젝트에 TestController를 아래와 같이 추가해주었다.
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
이제 Native Image를 만들면 된다.
./gradlew nativeCompile
컴파일은 3분 43초가 걸렸고, 컴파일된 이미지는 build/native/nativeCompile 디렉토리에 demo라는 이름으로 만들어졌다.
실제로 이미지를 실행해보면 놀라운 결과를 볼 수 있다. 어플리케이션 실행에 0.055초가 걸린 것을 확인할 수 있다. 정확이 동일한 코드를 Native Image가 아닌 JVM으로 실행시키면 1.1~1.3초 정도가 걸리는 것과 비교해보면 시작 시간이 매우 빠르다는 것을 확인할 수 있다. 아래와 같이 API가 잘 동작하는 것도 확인할 수 있다.
그러나 시작 시간이 빠르다는 것이지, 응답 시간은 큰 차이가 없는 것 같다. 어떤 테스트에서는 JVM이 더 빠르고, 어떤 테스트에서는 Native Image가 더 빠르다. 그러나 시작 시간이 빠르고, 메모리 사용량이 줄어들었다는 것은 확실해 보인다.
[JVM이 더 빠른 테스트] https://medium.com/@alexeynovikov_89393/ultimate-2023-web-server-benchmark-nodejs-vs-java-vs-rust-vs-go-e367d932f699
[Native Image가 더 빠른 테스트] https://betterprogramming.pub/how-to-integrate-spring-native-into-spring-boot-microservices-add2ece541b8
Spring에서 Native Image 도입을 고려하려면 직접 성능테스트를 해 보는 것이 좋을 것 같다. 싱글 코어에서 동작하는 Serial GC를 사용하고 있고, 또 아직은 지원되는 라이브러리가 많지 않기 때문이다.
3줄 요약
- JVM 설치 없이도 Java 어플리케이션 실행이 가능하다
- GraalVM을 이용하면 Native Image를 만들 수 있다
- 실제로 활용하려면 꼭 테스트를 해보길 바란다
'Java > 기타' 카테고리의 다른 글
[Java] GC가 일어나면 hashCode의 값이 바뀔까? (0) | 2023.03.29 |
---|---|
[Java] JNI(Java Native Interface) 사용해보기 (0) | 2023.03.20 |