GraalVM Native Image에 대하여
SpringBoot 3는 GraalVM을 왜 지원하는가
- 결론부터 말하면 성능 때문이다. 기존 JVM보다 프로그램의 실행 시간을 줄이고 메모리를 더 효율적으로 사용한다.
- 이는 클라우드 환경에서, 컨테이너가 생성된 후 서버가 구동되기까지의 시간을 단축한다는 뜻이다.
- 특히 빠르게 동작하는 Scale-Out이 필요한 상황에서 유용하다.
Native Image
- Java 코드를 독립적으로 실행 가능한(standalone executable) 이미지로 만들어주는 기술을 말한다.
- 기존의 Java는 실행하는 OS에 JRE를 설치하여 실행하지만, Native Image는 JVM 등을 자체적으로 포함하여 다른 프로그램을 필요로 하지 않는다.
- 다만, 실행 환경에 맞는 Image를 만들어야 한다.
AOT 컴파일 vs JIT 컴파일
- GraalVM을 찾아보면 AOT 컴파일을 핵심 특징으로 내세우고 있다. 컴파일의 방식인데, 기존 Java 방식인 JIT 컴파일과 간단히 비교하면 다음과 같다.
- JIT (Just-In-Time)
- 저수준(low level) 기계어 코드 컴파일을 런타임 때 한다.
- 그때 그때 컴파일을 진행하기 때문에, 상황에 최적화된 코드를 컴파일 한다. 다만 AOT보다 느리다.
- AOT (Ahead-Of-Time)
- 런타임 전에 저수준(low level) 기계어 코드 컴파일을 미리 진행한다.
- 미리 컴파일 해놓기 때문에 실행 속도가 빠르다. 다만 JIT보다 상황에 최적화된 컴파일을 할 수 없다.
- AOT의 경우 JIT와 다르게 런타임에서 동적 요소들(ex Reflection, Dynamic Proxy 등)을 처리할 수 없다.
- 따라서 전체 코드를 컴파일하기 전에, 이에 대한 정보들을 설정 파일(configuration file)로 전달하여 보완한다.
- 즉, 컴파일 시에 byte code와 동적 요소들을 나타내는 meta data들을 함께 사용해 native image를 생성한다.
Spring Boot 3.0이 GraalVM Native Image를 지원한다는 의미
- AOT complile 단계에서 Spring Boot와 관련된 meta data(configuration file)를 GraalVM에 제공하는 것이다.
- AOT 컴파일의 특징이 Spring Boot 환경에서 제약으로 작용하기 때문이다.
- 이는 "정의된 Bean들은 런타임에서 변경될 수 없다."을 의미한다.
- 예를 들어, Spring Boot가 meta data를 생성해주지 않으면 다음과 같은 문제가 발생한다.
@Profile
같은 어노테이션은 사용할 수 없다. 반드시 컴파일 당시에 관련 변수를 함께 넘겨주어야만 한다.
@ConditionalOnProperty
와 같은 동적으로 빈을 생성하는 어노테이션도 마찬가지이다.
주의점
- 아직 완전히 Java application을 지원하는 것이 아니다.
- 특히, 동적 요소들에 대한 meta data를 완전히 전달하지 못할 경우, 런타임 실패가 발생할 수 있다.
- Mockito 등의 라이브러리는 사용할 수 없다.
적용하기
환경
- Kotlin 1.7.22 (Java 17), Spring Boot 3.0.2 (적용을 위해 3.0.6으로 올림), Gradle (Kotlin DSL)
Gradle
- 플러그인 추가
- Spring Boot가 지원하는 것 외의 meta data 생성을 위한 플러그인이다.
plugins {
// ...
id("org.graalvm.buildtools.native") version "0.9.21"
}
- Docker 이미지 생성
- 위에 명시한 플러그인을 통해 gradle에서 바로 네이티브 이미지를 만들 수 있다.
$ gradle bootBuildImage
- 혹은, Docker가 필요없는 실행 파일을 만들 수도 있다. 다만, GraalVM이 설치되어 있어야 한다.
$ gradle nativeCompile
적용에 실패한 이유
- 아직 SpringBoot가 관리하는 Dependency(라이브러리) 이외에 대한 Hint들은 직접 찾아서 configuration file에 명시해야 한다.
- 예를 들어, mybatis를 사용하면서 SQL을 로그에 남기려면 log4jdbc를 사용해야 한다. AOT 컴파일 실행 후 서버를 띄워보면 이를 위해 log4j에 대한 참조를 필요로 한다. (왜 그런지는 모르겠다.)
- 관련 힌트를 주면, 또 다른 힌트를 요구하고, 또 다른 힌트를 주면, 또 다른 힌트를 요구하는 것의 무한 반복이었다.
그렇다면 GraalVM만이라도 적용한다면?
- 결론만 말하면 기존보다 서버 구동 속도가 조금 느려졌었다.
- GraalVM을 적용하는 것만으로도 속도가 향상된다고 공식 페이지에 명시되어 있지만, 서버 구성에 따라 결과가 다른 듯하다.
결론 - SpringBoot 3.0.6 기준
- Spring의 동적 요소를 활용하는, SpringBoot가 관리하는 것 외의 Dependency가 추가된 경우 AOT 컴파일을 위한 Hint를 모두 명시해야 한다.
- 즉, Gradle에 명시된 Dependency에 대한 모든 힌트를 추가하는 자동화가 아직은 완성도가 높지 않다.
- GraalVM을 적용하여도, 서버 구동 시간이 반드시 빨라지지 않으므로, 적용 후 측정이 필요하다.
참고자료
graalvm 을 적용했는데 서버구동이 느려진거면 이미지가 잘못빌드된거같은데요. 그런경우는 첨봤네요. 저는 잘 작동되었어서요