GraalVM 네이티브 이미지: Spring AOT 컴파일 완전 가이드
GraalVM native-image와 Spring AOT의 동작 원리, JVM 대비 기동 시간·메모리 비교, 리플렉션·리소스·프록시 힌트 등록 방법, 그리고 서버리스·컨테이너 환경에서의 실전 적용법을 코드 예제와 함께 설명합니다.
지난 글에서 Spring Boot 3 마이그레이션을 완료했다면, 이제 Boot 3의 핵심 기능 중 하나인 GraalVM 네이티브 이미지를 활용할 차례입니다.
GraalVM 네이티브 이미지란
GraalVM의 native-image 도구는 Java 바이트코드를 **미리 컴파일(AOT)**해 JVM 없이 실행 가능한 네이티브 바이너리로 만듭니다. 결과물은 OS에 직접 실행되는 단일 실행 파일입니다.
기존 JVM 방식은 JIT(Just-In-Time) 컴파일로 실행 중 최적화하기 때문에 최고 성능까지 워밍업이 필요합니다. 반면 네이티브 이미지는 빌드 타임에 모든 분석을 마치므로 즉시 최고 성능에 가까운 상태로 시작합니다.
Spring AOT의 역할
네이티브 이미지 빌드에서 가장 큰 도전은 동적 기능의 처리입니다. GraalVM은 빌드 타임에 닫힌 세계(Closed World) 가정을 적용하기 때문에, 런타임에 리플렉션이나 동적 프록시로 처음 접근하는 클래스를 미리 알 수 없습니다.
Spring AOT(Ahead-Of-Time) 엔진은 빌드 타임에 애플리케이션 컨텍스트를 분석해 세 가지 작업을 수행합니다.
- 코드 생성:
@Configuration클래스의 빈 등록 로직을 리플렉션 없는 Java 코드로 변환 - 리플렉션 힌트 생성:
@Entity, Jackson 직렬화 대상 등 리플렉션이 필요한 클래스를 힌트로 자동 등록 - 프록시 힌트 생성: AOP,
@Transactional등에서 사용하는 동적 프록시를 컴파일 타임 프록시로 교체
빌드 설정
<!-- Maven: native profile -->
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>build-native</id>
<goals><goal>compile-no-fork</goal></goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
// Gradle build.gradle
plugins {
id 'org.springframework.boot' version '3.2.5'
id 'org.graalvm.buildtools.native' version '0.10.2'
}
graalvmNative {
binaries {
main {
buildArgs.add('--no-fallback')
// 메모리 제한 설정 (CI 환경)
buildArgs.add('-J-Xmx6g')
}
}
}
# Maven 네이티브 빌드
./mvnw -Pnative native:compile
# Gradle 네이티브 빌드
./gradlew nativeCompile
# Docker 기반 빌드 (GraalVM 미설치 환경)
./gradlew bootBuildImage --imageName=my-app:native
리플렉션 힌트 등록
Spring이 자동으로 처리하지 못하는 동적 클래스 접근은 힌트를 직접 등록해야 합니다.
// @RegisterReflectionForBinding: DTO 직렬화/역직렬화
@SpringBootApplication
@RegisterReflectionForBinding(OrderDto.class)
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// RuntimeHintsRegistrar: 세밀한 제어가 필요할 때
@Component
@ImportRuntimeHints(MyRuntimeHints.class)
public class MyService { }
class MyRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader cl) {
// 리플렉션 힌트
hints.reflection().registerType(
OrderDto.class,
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
MemberCategory.DECLARED_FIELDS
);
// 리소스 힌트 (classpath 파일)
hints.resources().registerPattern("templates/*.html");
// 직렬화 힌트
hints.serialization().registerType(OrderDto.class);
}
}
reflect-config.json (수동 방식)
자동화 도구가 커버하지 못하는 경우 JSON 파일로 직접 지정할 수 있습니다.
// src/main/resources/META-INF/native-image/reflect-config.json
[
{
"name": "com.example.OrderDto",
"allDeclaredConstructors": true,
"allPublicFields": true,
"allDeclaredMethods": true
}
]
네이티브 이미지 테스트
AOT 모드에서 동작을 미리 검증할 수 있습니다.
// 네이티브 테스트: 실제 native-image 빌드 없이 AOT 처리만 검증
@SpringBootTest
@TestPropertySource(properties = "spring.aot.enabled=true")
class MyServiceNativeTest {
@Autowired
MyService myService;
@Test
void contextLoads() {
// AOT 모드에서 빈이 정상 생성되는지 확인
assertThat(myService).isNotNull();
}
}
# 네이티브 테스트 실행 (실제 native-image 컴파일 포함 — 시간 소요)
./gradlew nativeTest
실행 파일 크기와 실행
# 빌드 결과: build/native/nativeCompile/my-app
ls -lh build/native/nativeCompile/my-app
# -rwxr-xr-x 1 user user 68M my-app
# 실행 — JVM 불필요
./build/native/nativeCompile/my-app
# Started MyApplication in 0.057 seconds (process running for 0.08)
언제 Native Image를 쓸까
네이티브 이미지가 적합한 경우와 그렇지 않은 경우를 명확히 구분해야 합니다.
| 적합 | 부적합 |
|---|---|
| AWS Lambda, Cloud Run 등 서버리스 | 장기 실행 + 높은 처리량이 필요한 서비스 |
| 컨테이너 스케일아웃이 잦은 MSA | 동적 클래스 로딩 많이 사용하는 레거시 |
| 메모리 비용 절감이 중요한 환경 | 빌드 파이프라인 시간이 제약인 프로젝트 |
| CLI 도구 배포 | JVM 최신 기능(ZGC, Virtual Threads) 의존 |
자주 만나는 빌드 오류
ReflectiveOperationException at runtime
→ 리플렉션 힌트 누락입니다. --trace-class-initialization 옵션으로 원인 클래스를 추적하세요.
ClassNotFoundException for dynamic class
→ Class.forName() 호출 대상 클래스에 reflect 힌트가 없습니다.
빌드 중 OutOfMemoryError
→ native-image는 6GB 이상 메모리를 사용합니다. CI 빌드 머신 사양을 확인하고 -J-Xmx6g를 명시하세요.
지난 글: Java EE에서 Jakarta EE로: Spring Boot 3 마이그레이션 완전 가이드
다음 글: Virtual Threads로 Spring MVC 성능 극대화하기
읽어주셔서 감사합니다. 😊