Oracle JDK와 다른 OpenJDK 배포판들은 뭐가 다른지 찾아보다가 GraalVM을 알게 됐다. 처음엔 단순히 JDK 종류를 정리하려고 했는데, Native Image가 지금 학습 중인 MSA 환경과도 잘 맞아 보여서 조금 더 찾아보게 됐다.
Java를 처음 배울 때는 그냥 Oracle JDK 받으면 된다고 했다. 그러다 어느 순간 OpenJDK가 나오고, Temurin이 나오고, GraalVM이 나오고, Corretto가 나오고…
Temurin, Corretto, Microsoft OpenJDK 같은 배포판은 기반이 OpenJDK로 거의 같다. 소스는 같은데 누가 빌드하고 패키징하느냐의 차이다.
다만 GraalVM은 거기서 한 발 더 나간다. 대부분의 JDK는 HotSpot JVM을 그대로 쓰는데, GraalVM은 런타임과 컴파일 전략 자체가 다르다. 자체 JIT 컴파일러에다가 Native Image라는 기능까지 얹혀 있다.
GraalVM은 Oracle이 개발하는 OpenJDK 기반 배포판이다. 거기서 특이한 게 두 가지 있다.
하나는 Graal JIT 컴파일러다. HotSpot 기본 컴파일러 대신 Java로 작성된 Graal 컴파일러를 JIT로 쓴다. 장시간 실행 서비스에서 더 공격적인 최적화를 한다는 게 공식 입장인데, 실제 차이는 워크로드마다 다를 거다. 아직 직접 비교해본 건 아니다.
다른 하나는 Native Image다. 이게 GraalVM을 더 찾아보게 된 이유였다.
Native Image는 Java 애플리케이션을 AOT(Ahead-Of-Time) 컴파일로 네이티브 바이너리로 만든다. JVM 없이 실행 파일 하나로 돌아간다.
JVM 기반 Spring Boot가 뜨는 과정을 보면 이렇다.
JVM 기동
→ 클래스 로딩
→ Spring 컨텍스트 초기화
→ JIT 컴파일 워밍업
→ 서비스 ReadyJVM 기동
→ 클래스 로딩
→ Spring 컨텍스트 초기화
→ JIT 컴파일 워밍업
→ 서비스 Ready이게 보통 수 초에서 수십 초씩 걸린다.
Native Image는 이걸 빌드 타임에 다 처리해놓는다. 실행 파일이 뜨는 순간 이미 준비된 상태다. 시작 시간이 초 단위에서 밀리초 단위로 줄어든다.
메모리도 달라진다. JVM 자체 오버헤드가 사라지니까 서비스당 사용량이 눈에 띄게 줄어든다.
모놀리식이면 JVM이 하나다. MSA는 서비스마다 JVM이 따로 뜬다.
Gateway JVM
Auth JVM
User JVM
Order JVM
Payment JVMGateway JVM
Auth JVM
User JVM
Order JVM
Payment JVM서비스가 10개면 JVM 오버헤드도 10배다. 서비스당 200~500MB만 잡아도 10개면 2~5GB가 기본으로 나간다.
Native Image로 서비스당 100~150MB 수준으로 낮출 수 있다면, 같은 EC2에서 훨씬 많은 서비스를 올릴 수 있다. 단순한 성능 얘기가 아니라 인프라 비용 얘기다.
오토스케일링에서도 차이가 난다. 트래픽이 갑자기 몰려서 새 인스턴스를 띄울 때, JVM 기반이면 Ready 상태까지 몇 초가 걸린다. Native는 그게 훨씬 짧다. Cold Start가 빠르면 오토스케일링 반응 속도 자체가 달라진다.
Kubernetes 환경에서는 이게 더 극대화된다. Pod 하나가 뜨는 시간이 짧을수록 HPA 반응이 빨라지고, 컨테이너 밀도가 높아지면 같은 노드에서 더 많은 서비스를 굴릴 수 있다. 노드 수를 줄일 수 있다는 건 곧 비용이다.
다만 오해하면 안 되는 게 있다. 병목이 DB나 Redis, 서비스 간 네트워크 I/O에서 온다면 Native Image가 그 병목을 없애주진 않는다. 의미 있는 구간은 인프라 효율, 그러니까 메모리 사용량과 시작 시간이다.
AOT 컴파일이라는 특성 때문에 제약이 하나 있다.
빌드 타임에 코드를 분석해서 미리 컴파일해두는 방식이다 보니, 컴파일러가 미리 알 수 없는 코드에서 문제가 생긴다. 대표적인 게 리플렉션이다. 런타임에 클래스 이름을 문자열로 받아서 동적으로 호출하는 코드는, 빌드 타임에 어떤 클래스가 쓰일지 알 수 없다. 그래서 따로 명시해줘야 한다.
Spring이 내부적으로 리플렉션이나 프록시를 많이 써서 예전엔 Native 빌드가 꽤 까다로웠다고 한다. Spring Boot 3.x부터 AOT 처리가 내장돼서 많이 나아졌다는데, 직접 해보기 전까진 모르는 거다.
당장 Native Image를 쓸 거라서가 아니다.
GraalVM CE JDK는 일반 JVM 실행도 된다. 개발하는 동안은 기존 JAR 방식 그대로 쓰면 된다. 나중에 Native 빌드를 시도할 때 JDK를 다시 바꾸지 않아도 된다는 게 이유였다.
지금은 HotSpot처럼 쓰다가, 나중에 필요하면 Native Image로 넘어가면 되는 구조다.
참고로 JDK는 배포판마다 라이선스가 다르다. Oracle JDK와 GraalVM Oracle Edition은 상업적 사용 시 라이선스 조건을 반드시 확인해야 한다. 필자는 GraalVM Community Edition을 선택했다.
대부분의 JDK는 OpenJDK 기반이지만, GraalVM처럼 런타임과 컴파일 전략 자체가 다른 배포판도 있다.
Native Image의 핵심 가치는 API 응답 속도보다 시작 시간과 메모리 사용량에 있다. MSA처럼 서비스가 많고 오토스케일링이 잦은 구조, 특히 Kubernetes 환경에서 인프라 비용과 직결된다.
아직 실제로 Native 빌드를 해본 건 아니다. 써보면 따로 기록할 예정이다.
참고 자료