Spring GraalVM Native Image

GraalVM Native Image란?

GraalVM Native Image는 Java 애플리케이션을 AOT(Ahead-of-Time) 컴파일하여 독립 실행 가능한 네이티브 바이너리로 변환하는 기술입니다. 기존 JVM 기반 실행 대비 시작 시간 50ms 이하, 메모리 사용량 1/5 수준을 달성할 수 있습니다. Spring Boot 3.x에서 공식 지원하며, 서버리스, CLI 도구, 사이드카 컨테이너 등에 최적입니다.

이 글에서는 Spring Boot + GraalVM Native Image 빌드 방법, 리플렉션/프록시 제약 해결, 빌드 최적화, 그리고 운영 환경 적용 전략까지 실전 중심으로 다루겠습니다.

JVM vs Native Image 비교

항목 JVM (JIT) Native Image (AOT)
시작 시간 2~10초 20~80ms
메모리 (RSS) 200~500MB 40~100MB
피크 처리량 높음 (JIT 최적화) 약간 낮음 (AOT 한계)
빌드 시간 ~30초 3~10분
이미지 크기 ~300MB (JRE 포함) ~80MB
리플렉션 완전 지원 사전 선언 필요

프로젝트 설정

// build.gradle.kts — Spring Boot 3.2+ / GraalVM
plugins {
    id("org.springframework.boot") version "3.2.3"
    id("io.spring.dependency-management") version "1.1.4"
    id("org.graalvm.buildtools.native") version "0.10.1"  // 핵심!
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"
}

graalvmNative {
    binaries {
        named("main") {
            buildArgs.add("--enable-preview")
            buildArgs.add("-H:+ReportExceptionStackTraces")
            
            // 메모리 제한 (CI 환경용)
            buildArgs.add("-J-Xmx6g")
            
            // 빌드 시간 최적화
            buildArgs.add("-march=native")
        }
    }
    
    // AOT 테스트도 Native로 실행
    binaries.all {
        buildArgs.add("--verbose")
    }
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    runtimeOnly("org.postgresql:postgresql")
}

빌드 방법

# 방법 1: 로컬 GraalVM으로 직접 빌드
# GraalVM JDK 21 설치 필요
sdk install java 21.0.2-graalce
sdk use java 21.0.2-graalce

# Native Image 빌드 (3~10분 소요)
./gradlew nativeCompile

# 실행
./build/native/nativeCompile/myapp
# Started MyApplication in 0.045 seconds! 🚀

# 방법 2: Buildpacks (GraalVM 설치 불필요)
./gradlew bootBuildImage 
  --imageName=myapp:native

# 방법 3: Docker Multi-Stage Build
# Dockerfile에서 빌드 (CI/CD용)

Dockerfile: Native Image 빌드

# syntax=docker/dockerfile:1

# 1단계: GraalVM으로 네이티브 바이너리 빌드
FROM ghcr.io/graalvm/native-image-community:21 AS build
WORKDIR /app

COPY build.gradle.kts settings.gradle.kts gradle.properties ./
COPY gradle ./gradle
COPY gradlew ./

RUN --mount=type=cache,target=/root/.gradle/caches 
    ./gradlew dependencies --no-daemon

COPY src ./src

RUN --mount=type=cache,target=/root/.gradle/caches 
    ./gradlew nativeCompile --no-daemon -x test

# 2단계: 최소 런타임 이미지
FROM debian:bookworm-slim
WORKDIR /app

# 네이티브 바이너리만 복사 (JRE 불필요!)
COPY --from=build /app/build/native/nativeCompile/myapp ./myapp

# 필수 라이브러리만
RUN apt-get update && apt-get install -y --no-install-recommends 
    libz-dev && rm -rf /var/lib/apt/lists/*

EXPOSE 8080
ENTRYPOINT ["./myapp"]

# 결과 이미지 크기: ~80MB (JVM 기반 ~300MB 대비 73% 절감)

리플렉션 제약과 해결

Native Image의 가장 큰 도전은 리플렉션 제약입니다. AOT 컴파일 시점에 모든 클래스 참조를 파악해야 합니다.

// Spring Boot 3.x는 대부분의 리플렉션을 자동 처리합니다
// 하지만 수동 등록이 필요한 경우도 있습니다

// 1. @RegisterReflectionForBinding — DTO 클래스 등록
@RestController
@RegisterReflectionForBinding({
    OrderResponse.class,
    OrderItemDto.class,
    PageResponse.class,
})
public class OrderController {
    // ...
}

// 2. RuntimeHints — 프로그래밍 방식 등록
@Configuration
@ImportRuntimeHints(MyRuntimeHints.class)
public class NativeConfig {
}

public class MyRuntimeHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // 리플렉션 등록
        hints.reflection()
            .registerType(ExternalApiResponse.class, 
                MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
                MemberCategory.INVOKE_PUBLIC_METHODS,
                MemberCategory.DECLARED_FIELDS);

        // 리소스 파일 등록
        hints.resources()
            .registerPattern("templates/*.html")
            .registerPattern("static/**");

        // JNI 등록
        hints.jni()
            .registerType(SomeNativeClass.class);

        // 프록시 등록
        hints.proxies()
            .registerJdkProxy(MyService.class);
    }
}

// 3. reflect-config.json (수동)
// src/main/resources/META-INF/native-image/reflect-config.json
[
  {
    "name": "com.app.dto.LegacyResponse",
    "allDeclaredConstructors": true,
    "allPublicMethods": true,
    "allDeclaredFields": true
  }
]

호환성 체크리스트

라이브러리 Native 지원 비고
Spring MVC ✅ 완전 자동 힌트 생성
Spring Data JPA ✅ 완전 Hibernate 6.x 필수
Spring Security ✅ 완전
Spring Cloud ⚠️ 부분 모듈별 확인 필요
Lombok ✅ 완전 컴파일 타임 처리
MapStruct ✅ 완전 컴파일 타임 코드 생성
Flyway ✅ 완전 10.x+
Logback ⚠️ 주의 XML 설정 시 힌트 필요
CGLIB 프록시 ❌ 미지원 인터페이스 기반 프록시 사용

AOT 테스트

# AOT 처리 검증 (네이티브 빌드 전 빠른 확인)
./gradlew processAot

# 테스트도 AOT 모드로 실행
./gradlew processTestAot
./gradlew nativeTest

// 테스트 코드에서 RuntimeHints 검증
@SpringBootTest
class NativeHintsTest {

    @Autowired
    private RuntimeHints runtimeHints;

    @Test
    void shouldRegisterReflectionHints() {
        assertThat(RuntimeHintsPredicates.reflection()
            .onType(OrderResponse.class)
            .withMemberCategory(MemberCategory.DECLARED_FIELDS)
            .test(runtimeHints)).isTrue();
    }
}

적용 시나리오 가이드

// ✅ Native Image에 적합한 경우:
// - AWS Lambda / Cloud Functions (서버리스, 콜드 스타트 중요)
// - CLI 도구 (Spring Shell)
// - 사이드카 컨테이너 (메모리 절약)
// - 마이크로서비스 (빠른 스케일 아웃)
// - Kubernetes에서 빠른 롤링 업데이트

// ❌ JVM이 더 나은 경우:
// - 장시간 실행 배치 서버 (JIT 최적화 효과 큼)
// - 동적 클래스 로딩이 많은 앱 (플러그인 시스템)
// - 피크 처리량이 최우선인 고트래픽 서버
// - 빌드 시간이 중요한 빠른 이터레이션 환경

CI/CD 최적화

# GitHub Actions — Native Image 빌드
name: Native Build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: graalvm/setup-graalvm@v1
        with:
          java-version: '21'
          distribution: 'graalvm-community'
      
      - uses: gradle/actions/setup-gradle@v3
      
      # 캐시로 빌드 시간 단축
      - name: Cache Native Image
        uses: actions/cache@v4
        with:
          path: build/native
          key: native-${{ hashFiles('src/**', 'build.gradle.kts') }}
      
      - name: Build Native Image
        run: ./gradlew nativeCompile -x test
        env:
          JAVA_OPTS: "-Xmx6g"
      
      # Docker 이미지 생성 + 푸시
      - name: Build Container
        run: |
          docker build -f Dockerfile.native -t myapp:native .
          docker push myapp:native

마무리

Spring Boot + GraalVM Native Image는 서버리스와 K8s 오토스케일링 환경에서 혁신적인 시작 성능을 제공합니다. Spring Boot 3.x의 AOT 엔진이 대부분의 리플렉션 힌트를 자동 생성하므로, 순수 Spring 생태계 라이브러리를 사용한다면 전환 비용이 크지 않습니다. 다만 빌드 시간이 길고, AOP 프록시 등 일부 동적 기능에 제약이 있으므로, 워크로드 특성에 맞게 JVM과 Native 중 적합한 방식을 선택하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux