원문 - GraalVM Native Image Support


    1. GraalVM 네이티브 이미지 지원(GraalVM Native Image Support)
      • 15.1. GraalVM 네이티브 이미지 소개(Introducing GraalVM Native Images)
        • 15.1.1. JVM 배포의 주요 차이점(Key Differences with JVM Deployments)
        • 15.1.2. 스프링 Ahead-of-Time 이해(Understanding Spring Ahead-of-Time Processing)
      • 소스 코드 생성(Source Code Generation)Source Code Generation
      • 힌트 파일 생성(Hint File Generation)
      • 프록시 클래스 생성(Proxy Class Generation) - 15.2. 첫 번째 GraalVM 네이티브 애플리케이션 개발(Developing Your First GraalVM Native Application) - 15.2.1. 샘플 애플리케이션(Sample Application) - 15.2.2. 빌드팩을 사용하여 네이티브 이미지 빌드(Building a Native Image Using Buildpacks)
      • 시스템 요구 사항(System Requirements)
      • 메이븐 사용(Using Maven)
      • 그레이들 사용(Using Gradle)
      • 예제 실행(Running the example) - 15.2.3. 네이티브 빌드 도구를 사용하여 네이티브 이미지 빌드(Building a Native Image using Native Build Tools)
      • 전제조건(Prerequisites)
      • 메이븐 사용(Using Maven)
      • 그레이들 사용(Using Gradle)
      • 예제 실행(Running the example) - 15.3. GraalVM 네이티브 이미지 테스트(Testing GraalVM Native Images)
      • 15.3.1. JVM을 사용한 AOT 처리 테스트(Testing Ahead-of-time Processing With the JVM)
      • 15.3.2. 네이티브 빌드 툴 테스트(Testing With Native Build Tools)
        • 메이븐 사용(Using Maven)
        • 그레이들 사용(Using Gradle) - 15.4. 고급 네이티브 이미지 토픽(Advanced Native Images Topics)
      • 15.4.1. 중첩된 구성 프로퍼티스(Nested Configuration Properties)
      • 15.4.2. 스프링 부트 실행가능한 Jar 변환(Converting a Spring Boot Executable Jar)
        • 빌드팩 사용(Using Buildpacks)
        • GraalVM 네이티브 이미지 사용(Using GraalVM native-image)
      • 15.4.3. 트레이싱 에이전트 사용(Using the Tracing Agent)
        • 애플리케이션 직접 실행(Launch the Application Directly)
      • 15.4.4. 커스텀 힌트(Custom Hints)
        • 커스텀 힌트 테스트(Testing custom hints)
      • 15.4.5. 알려진 제한사항(Known Limitations) - 15.5. 다음에 읽을 내용(What to Read Next)

15. GraalVM 네이티브 이미지 지원(GraalVM Native Image Support)

GraalVM 네이티브 이미지는 컴파일된 자바 애플리케이션을 미리 처리하여 생성할 수 있는 독립형 실행 파일이다. 네이티브 이미지는 일반적으로 JVM 이미지보다 메모리 공간이 더 작고 더 빠르게 시작된다.

15.1. GraalVM 네이티브 이미지 소개(Introducing GraalVM Native Images)

GraalVM 네이티브 이미지는 자바 애플리케이션을 배포하고 실행하는 새로운 방법을 제공한다. 자바 버추얼 머신에 비해 네이티브 이미지는 더 작은 메모리 공간과 훨씬 더 빠른 시작 시간으로 실행될 수 있다.

이는 컨테이너 이미지를 사용하여 배포되는 애플리케이션에 매우 적합하며 FaaS(Function as a Service) 플랫폼과 결합할 때 특히 그렇다.

JVM용으로 작성된 기존 애플리케이션과 달리 GraalVM 네이티브 이미지 애플리케이션은 실행 파일을 생성하기 위해 사전 처리가 필요하다. 이러한 사전 처리에는 기본 진입점에서 애플리케이션 코드를 정적으로 분석하는 작업이 포함된다.

GraalVM 네이티브 이미지는 플랫폼별 실행 파일이다. 네이티브 이미지를 실행하기 위해 자바 버추얼 머신을 제공할 필요는 없다.

GraalVM을 시작하고 실험하고 싶다면 “첫 번째 GraalVM 네이티브 애플리케이션 개발” 장으로 건너뛰고 나중에 이 장으로 돌아올 수 있다.

15.1.1. JVM 배포의 주요 차이점(Key Differences with JVM Deployments)

GraalVM 네이티브 이미지가 미리 생성된다는 사실은 기본 애플리케이션과 JVM 기반 애플리케이션 간에 몇 가지 차이점이 있다. 차이점은 다음과 같습니다.

  • 애플리케이션의 정적 분석은 빌드 시 기본 진입점에서 수행된다.
  • 네이티브 이미지가 생성될 때 접근할 수 없는 코드는 제거되고 실행 파일의 일부가 되지 않는다.
  • GraalVM은 코드의 동적 엘리먼트를 직접 인식하지 못하며 리플렉션, 리소스, 직렬화 및 동적 프록시에 대해 알려주어야 한다.
  • 애플리케이션 클래스패스는 빌드 시 고정되며 변경할 수 없다.
  • 레이지(lazy) 클래스 로딩이 없으며, 실행 파일에 포함된 모든 것이 시작 시 메모리에 로드된다.
  • 완전히 지원되지 않는 자바 애플리케이션의 일부 기능은 몇 가지 제한사항이 있다.

GraalVM 레퍼런스 문서의 네이티브 이미지 호환성 가이드에서는 GraalVM 제한 사항에 대한 자세한 내용을 제공한다.

15.1.2. 스프링 Ahead-of-Time 이해(Understanding Spring Ahead-of-Time Processing)

일반적인 스프링 부트 애플리케이션은 매우 동적이며 구성은 런타임에 수행된다. 실제로 스프링 부트 자동 구성은 올바른 구성을 위해 런타임 상태 반응에 크게 의존한다.

애플리케이션의 이러한 동적 측면을 GraalVM에 알리는 것이 가능하더라도 그렇게 하면 정적 분석의 이점이 대부분 없어진다. 따라서 대신 스프링 부트를 사용하여 네이티브 이미지를 생성할 때는 닫힌 세계로 가정하고 애플리케이션의 동적 측면은 제한된다.

닫힌 세계 가정은 다음과 같은 제한 사항을 의미한다.

  • 클래스패스는 고정되어 있으며 빌드 시 완전히 정의된다.
  • 애플리케이션에 정의된 빈은 런타임 시 변경될 수 없다. 즉, 다음과 같다.
    • 스프링 @Profile 어노테이션과 프로필별 구성에는 제한이 있다.
    • 빈이 생성되면 변경되는 프로퍼티(예: @ConditionalOnProperty.enable 프로퍼티스)은 지원되지 않다.

이러한 제한 사항이 적용되면 스프링이 빌드 시간 동안 전처리를 수행하고 GraalVM이 사용할 수 있는 추가 자산(asset)을 생성하는 것이 가능해진다. 스프링 AOT 처리 애플리케이션은 일반적으로 다음을 생성한다.

  • 자바 소스 코드
  • 바이드코드(다이나믹 프록시 등)
  • GraalVM JSON 힌트 파일:
    • 리소스 힌트(resource-config.json)
    • 리플렉션 힌트(reflect-config.json)
    • 직렬화(Serialization) 힌트(serialization-config.json)
    • 자바 프록시 힌트(proxy-config.json)
    • JNI 힌트(jni-config.json)

소스 코드 생성(Source Code Generation)

스프링 애플리케이션은 스프링 빈으로 구성된다. 내부적으로 스프링 프레임워크는 두 가지 별개의 개념을 사용하여 빈을 관리한다. 실제로 생성되어 다른 빈에 주입될 수 있는 빈 인스턴스가 있다. 빈의 애트리뷰트와 인스턴스 생성 방법을 정의하는 데 사용되는 빈 정의도 있다.

일반적인 @Configuration 클래스 사용 예제다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MyConfiguration {
  @Bean
  public MyBean myBean() {
    return new MyBean();
  }
}

빈 정의는 @Configuration 클래스를 파싱하여 @Bean 메소드를 찾아 생성한다. 위의 예제에서는 myBean이라는 싱글톤 빈에 대한 BeanDefinition을 정의한다. 또한 MyConfiguration 클래스 자체에 대한 BeanDefinition을 생성하고 있다.

myBean 인스턴스가 필요할 때 스프링은 myBean() 메서드를 호출하고 그 결과를 사용해야 한다는 것을 알고 있다. JVM에서 실행할 때 @Configuration 클래스 파싱은 애플리케이션이 시작되고 @Bean 메소드가 리플렉션을 사용하여 호출할 때 발생한다.

네이티브 이미지를 생성할 때 스프링은 다른 방식으로 작동한다. @Configuration 클래스를 파싱하고 런타임에 비 정의를 생성하는 대신 빌드 타임에 수행한다. 빈 정의가 검색되면 처리되어 GraalVM 컴파일러에서 분석할 수 있는 소스 코드로 변환된다.

스프링 AOT 프로세스는 위의 구성 클래스를 다음과 같은 코드로 변환한다.

import org.springframework.beans.factory.aot.BeanInstanceSupplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;

/**
 * {@link MyConfiguration}에 대한 빈 정의다.
 */
public class MyConfiguration__BeanDefinitions {
  /**
    * 'myConfiguration'에 대한 빈 정의를 가져온다.
    */
  public static BeanDefinition getMyConfigurationBeanDefinition() {
      Class<?> beanType = MyConfiguration.class;
      RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
      beanDefinition.setInstanceSupplier(MyConfiguration::new);
      return beanDefinition;
  }

  /**
    * 'myBean'에 대한 빈 인스턴스 공급자(supplier)를 가져온다.
    */
  private static BeanInstanceSupplier<MyBean> getMyBeanInstanceSupplier() {
      return BeanInstanceSupplier.<MyBean>forFactoryMethod(MyConfiguration.class, "myBean")
          .withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(MyConfiguration.class).myBean());
  }

  /**
   * 'myBean'에 대한 빈 정의를 가져온다.
    */
  public static BeanDefinition getMyBeanBeanDefinition() {
    Class<?> beanType = MyBean.class;
    RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
    beanDefinition.setInstanceSupplier(getMyBeanInstanceSupplier());
    return beanDefinition;
  } 
}

생성된 정확한 코드는 빈 정의 특성에 따라 다를 수 있다.

위에서 생성된 코드는 @Configuration 클래스와 동등한 빈 정의를 생성하지만 GraalVM이 이해할 수 있는 직접적인 방식임을 볼 수 있다.

myConfiguration 빈에 대한 빈 정의와 myBean에 대한 정의가 있다. myBean 인스턴스가 필요할 때 BeanInstanceSupplier가 호출됩니다. 이 공급자(supplier)는 myConfiguration 빈에서 myBean() 메소드를 호출한다.

스프링 AOT 처리 중에 애플리케이션은 빈 정의가 사용 가능한 지점까지 시작된다. AOT 처리 단계에서는 빈 인스턴스가 생성되지 않는다.

스프링 AOT는 모든 빈 정의에 대해 이와 같은 코드를 생성한다. 또한 빈 사후 처리가 필요할 때(예: @Autowired 메소드 호출) 코드를 생성한다. AOT 처리된 애플리케이션이 실제로 실행될 때 스프링 부투에서 애플리케이션컨텍스트(ApplicationContext)를 초기화하는 데 사용되는 애플리케이션컨텍스트이니셜라이저(ApplicationContextInitializer)도 생성된다.

AOT에서 생성된 소스 코드는 장황할 수 있지만 읽기 쉽고 애플리케이션을 디버깅할 때 도움이 될 수 있다. 생성된 소스 파일은 메이븐을 사용하고 그레이들에서 build/generated/aotSources를 사용할 때 target/spring-aot/main/sources에서 찾을 수 있다.

힌트 파일 생성(Hint File Generation)

소스 파일을 생성하는 것 외에도 스프링 AOT 엔진은 GraalVM에서 사용되는 힌트 파일도 생성한다. 힌트 파일에는 GraalVM이 코드를 직접 검사하여 이해할 수 없는 항목을 처리하는 방법을 설명하는 JSON 데이터가 포함되어 있다.

예를 들어, 프라이빗 메소드에 스프링 어노테이션을 사용할 수 있다. 스프링은 GraalVM에서도 프라이빗 메소드를 호출하기 위해 리플렉션을 사용해야 한다. 이러한 상황이 발생하면 스프링은 프라이빗 메서드가 직접 호출되지 않더라도 여전히 네이티브 이미지에서 사용할 수 있어야 함을 GraalVM이 알 수 있도록 리플렉션 힌트를 작성할 수 있다.

힌트 파일은 META-INF/native-image 아래에 생성되며 GraalVM에서 자동으로 선택된다.

생성된 힌트 파일은 메이븐을 사용하고 그레이들에서 build/generated/aotResources를 사용할 때 target/spring-aot/main/resources에서 찾을 수 있다.

프록시 클래스 생성(Proxy Class Generation)

스프링은 때때로 추가 기능으로 작성한 코드를 향상시키기 위해 프록시 클래스를 생성해야 한다. 이를 위해 바이트코드를 직접 생성하는 cglib 라이브러리를 사용한다.

애플리케이션이 JVM에서 실행 중인 경우 애플리케이션이 실행될 때 프록시 클래스가 동적으로 생성된다. 네이티브 이미지를 생성할 때 이러한 프록시는 GraalVM에 포함될 수 있도록 빌드 시 생성되어야 한다.

소스 코드 생성과 달리 생성된 바이트코드는 애플리케이션을 디버깅할 때 특별히 도움이 되지 않는다. 그러나 javap와 같은 도구를 사용하여 .class 파일의 내용을 검사해야 하는 경우 메이븐은 target/spring-aot/main/classes에서, 그레이들은 build/generated/aotClasses에서 찾을 수 있다.

15.2. 첫 번째 GraalVM 네이티브 애플리케이션 개발(Developing Your First GraalVM Native Application)

이제 GraalVM 네이티브 이미지에 대한 개요와 스프링 엔진 작동 방식을 살펴보았으므로 애플리케이션을 생성하는 방법을 살펴보자.

스프링 부트 네이티브 이미지 애플리케이션을 빌드하는 두 가지 주요 방법이 있다.

  • 클라우드 네이티브 빌드팩에 대한 스프링 부트 지원을 사용하여 네이티브 실행 파일이 포함된 경량 컨테이너를 생성한다.\
  • GraalVM 네이티브 빌드 도구를 사용하여 네이티브 실행 파일을 생성한다.

새로운 네이티브 스프링 부트 프로젝트를 시작하는 가장 쉬운 방법은 start.spring.io로 이동하여 “GraalVM 네이티브 지원” 의존성을 추가하고 프로젝트를 생성하는 것이다. 포함된 HELP.md 파일은 시작 힌트를 제공한다.

15.2.1. 샘플 애플리케이션(Sample Application)

네이티브 이미지를 생성하는 데 사용할 수 있는 예제 애플리케이션이 필요하다. 우리의 목적을 위해 간단한 “Hello World!” “첫 번째 스프링 부트 애플리케이션 개발” 장에서 다룬 웹 애플리케이션이면 충분하다.

요약하면 기본 애플리케이션 코드는 다음과 같다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class MyApplication {
    @RequestMapping("/")
    String home() {
      return "Hello World!";
    }

  public static void main(String[] args) {
      SpringApplication.run(MyApplication.class, args);
  } 
}

이 애플리케이션은 GraalVM 네이티브 이미지와 작동하도록 테스트 및 검증된 스프링 MVC와 임베디드 톰캣을 사용한다.

15.2.2. 빌드팩을 사용하여 네이티브 이미지 빌드(Building a Native Image Using Buildpacks)

스프링 부트에는 메이븐과 그레이들 모두에 대한 네이티브 이미지에 대한 빌드팩 지원이 직접 포함되어 있다. 즉, 명령 하나만 입력하면 로컬로 실행되는 도커 데몬에 적절한 이미지를 빠르게 가져올 수 있다. 결과 이미지에는 JVM이 포함되지 않고 대신 기본 이미지가 정적으로 컴파일된다. 이로 인해 이미지가 더 작아진다.

이미지에 사용되는 빌더는 paketobuildpacks/builder:tiny이다. 설치 공간이 작지만 필요한 경우 paketobuildpacks/builder:base 또는 paketobuildpacks/builder:full을 사용하여 이미지에서 다양한 도구를 사용할 수도 있다.

시스템 요구 사항(System Requirements)

도커를 설치해야 한다. 자세한 내용은 도커 가져오기를 참고하자. 리눅스를 사용하는 경우 루트가 아닌 사용자를 허용하도록 구성해야한다.

docker run hello-world(sudo 없이)를 실행하여 도커 데몬이 예상대로 연결 가능한지 확인할 수 있다. 자세한 내용은 메이븐 또는 그레이들 스프링 부트 플러그인 문서를 확인하자.

맥OS에서는 도커에 할당된 메모리를 최소 8GB로 늘리고 잠재적으로 더 많은 CPU를 추가하는 것이 좋다. 자세한 내용은 이 스택 오버플로 답변을 참고하자. 마이크로소프트 윈도우에서는 더 나은 성능을 위해 도커 WSL 2 백엔드를 활성화해야 한다.

메이븐 사용(Using Maven)

메이븐을 사용하여 네이티브 이미지 컨테이너를 빌드하려면 pom.xml 파일이 spring-boot-starter-parentorg.graalvm.buildtools:native-maven-plugin을 사용하는지 확인해야 한다. 다음과 같은 구역이 있어야 한다.

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.1.1</version>
</parent>

추가로 <build> <plugins> 구역에 다음 내용이 있어야 한다.

<plugin>
  <groupId>org.graalvm.buildtools</groupId>
  <artifactId>native-maven-plugin</artifactId>
</plugin>

spring-boot-starter-parent는 네이티브 이미지를 생성하기 위해 실행해야 하는 실행을 구성하는 네이티브 프로필을 선언한다. 커맨드라인에서 -P 플래그를 사용하여 프로필을 활성화할 수 있다.

spring-boot-starter-parent를 사용하지 않으려면 스프링 부트 플러그인과 추가 기능에서 process-aot 목표에 대한 실행을 구성해야 한다. 네이티브 빌드 도구 플러그인의 도달 접근성(reachability) 메타데이터 목표.

spring-boot-starter-parent를 사용하지 않으려면 Spring Boot 플러그인의 process-aot 골(goal)과 Native Build Tools 플러그인의 add-reachability-metadata 골(goal)에 대한 실행을 구성해야 한다.

이미지를 빌드하려면, 네이티브 프로필이 활성화된 상태에서 spring-boot:build-image 골을 실행할 수 있다.

$ mvn -Pnative spring-boot:build-image 

그레이들 사용(Using Gradle)

스프링 부트 그레이들 플러그인은 GraalVM 네이티브 이미지 플러그인이 적용될 때 AOT 작업을 자동으로 구성한다. 그레이들 빌드에 org.graalvm.buildtools.native를 포함하는 플러그인 블록이 포함되어 있는지 확인해야 한다.

org.graalvm.buildtools.native 플러그인이 적용되는 한 bootBuildImage 태스크는 JVM 이미지가 아닌 네이티브 이미지를 생성한다. 다음을 사용하여 태스크를 실행할 수 있다.

$ gradle bootBuildImage

예제 실행(Running the example)

적절한 빌드 명령을 실행하면 도커 이미지를 사용할 수 있다. docker run을 사용하여 애플리케이션을 시작할 수 있다.

$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT

다음과 같은 출력이 표시된다.

.   ____          _            __ _ _
  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
  =========|_|==============|___/=/_/_/_/
  :: Spring Boot ::  (v3.1.1)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 0.08 seconds (process running for 0.095)

시작 시간은 시스템마다 다르지만 JVM에서 실행되는 스프링 부트 애플리케이션보다 훨씬 빠르다.

localhost:8080에 대한 웹 브라우저를 열면 다음 출력이 표시된다.

Hello World!

애플리케이션을 정상적으로 종료하려면 ctrl-c를 누르자.

15.2.3. 네이티브 빌드 도구를 사용하여 네이티브 이미지 빌드(Building a Native Image using Native Build Tools)

도커를 사용하지 않고 직접 기본 실행 파일을 생성하려면 GraalVM 기본 빌드 도구를 사용할 수 있다. 기본 빌드 도구는 메이븐 및 그레이들용으로 GraalVM에서 제공하는 플러그인이다. 이를 사용하여 기본 이미지 생성을 포함하여 다양한 GraalVM 작업을 수행할 수 있다.

전제조건(Prerequisites)

네이티브 빌드 도구를 사용하여 네이티브 이미지를 빌드하려면 컴퓨터에 GraalVM 배포판이 필요하다. Liberica Native Image Kit 페이지에서 수동으로 다운로드하거나 SDKMAN!과 같은 다운로드 관리자를 사용할 수 있다.

리눅스와 맥OS

macOS 또는 리눅스에 네이티브 이미지 컴파일러를 설치하려면 SDKMAN!을 사용하는 것이 좋다. SDKMAN을 다운받자. sdkman.io에서 다음 명령을 사용하여 Liberica GraalVM 배포판을 설치한다.

$ sdk install java 22.3.r17-nik
$ sdk use java 22.3.r17-nik

java -version의 출력을 확인하여 올바른 버전이 구성되었는지 확인하자.

$ java -version
openjdk version "17.0.5" 2022-10-18 LTS
OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode)
윈도우

윈도우에서는 다음 지침에 따라 GraalVM 또는 Liberica 네이티브 이미지 키트(버전 22.3), 비주얼 스튜디오(Visual Studio) 빌드 도구 및 윈도우 SDK를 설치한다. 윈도우 관련 커멘드라인 최대 길이로 인해 메이븐 또는 그레이들 플러그인을 실행하려면 일반 윈도우 명령줄 대신 x64 기본 도구 명령 프롬프트를 사용해야 한다.

메이븐 사용(Using Maven)

빌드팩 지원과 마찬가지로, 네이티브 프로필을 상속하기 위해 spring-boot-starter-parent를 사용하고 있고 org.graalvm.buildtools:native-maven-plugin 플러그인이 사용되는지 확인해야 한다.

네이티브 프로필이 활성화되어 있으면 native:compile 골(goal)을 호출하여 네이티브 이미지 컴파일을 트리거할 수 있다.

$ mvn -Pnative native:compile

네이티브 이미지 실행 파일은 대상 디렉토리에서 찾을 수 있다.

그레이들 사용(Using Gradle)

네이티브 빌드 도구 그레이들 플러그인이 프로젝트에 적용되면 스프링 부트 그레이들 플러그인이 자동으로 스프링 AOT 엔진을 트리거한다. 작업 의존성은 자동으로 구성되므로 표준 NativeCompile 태스크를 실행하여 네이티브 이미지를 생성할 수 있다.

$ gradle nativeCompile

네이티브 이미지 실행 파일은 build/native/nativeCompile 디렉토리에서 찾을 수 있다.

예제 실행(Running the Example)

이 시점에서 애플리케이션이 작동해야 한다. 이제 직접 실행하여 애플리케이션을 시작할 수 있다.

Maven

$ target/myproject

Gradle

$ build/native/nativeCompile/myproject

다음과 같이 출력된다.

.   ____          _            __ _ _
  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
  =========|_|==============|___/=/_/_/_/
  :: Spring Boot ::  (v3.1.1)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 0.08 seconds (process running for 0.095)

시작 시간은 시스템마다 다르지만 JVM에서 실행되는 스프링 부트 애플리케이션보다 훨씬 빠르다.

localhost:8080에 대한 웹 브라우저를 열면 다음 내용이 출력된다.

Hello World!

애플리케이션을 정상 종료하려면 ctrl-c를 누르자.

15.3. GraalVM 네이티브 이미지 테스트(Testing GraalVM Native Images)

네이티브 이미지 애플리케이션을 작성할 때 가능하면 JVM을 계속 사용하여 대부분의 단위 및 통합 테스트를 개발하는 것이 좋다. 이렇게 하면 개발자 빌드 시간을 단축하고 기존 IDE 통합을 사용할 수 있다. JVM에 대한 광범위한 테스트를 통해 다를 가능성이 있는 영역에 네이티브 이미지 테스트를 집중할 수 있다.

네이티브 이미지 테스트의 경우 일반적으로 다음과 같은 측면이 잘 작동하는지 확인하려고 한다.

  • 스프링 AOT 엔진은 애플리케이션을 처리할 수 있으며 AOT 처리(AOT-processed) 모드에서 실행된다.
  • GraalVM에 유효한 네이티브 이미지가 생성될 수 있는 충분한 힌트가 있다.

15.3.1. JVM을 사용한 AOT 처리 테스트(Testing Ahead-of-time Processing With the JVM)

스프링 부트 애플리케이션이 실행되면 네이티브 이미지로 실행되고 있는지 감지를 시도한다. 네이티브 이미지로 실행 중인 경우 스프링 AOT 엔진에 의해 빌드 시 생성된 코드를 사용하여 애플리케이션을 초기화한다.

애플리케이션이 일반 JVM에서 실행 중인 경우 AOT 생성 코드는 무시한다.

네이티브 이미지 컴파일 단계를 완료하는 데 시간이 걸릴 수 있으므로 JVM에서 애플리케이션을 실행하는 것이 유용할 때도 있지만 AOT 생성 초기화 코드를 사용하도록 하는 것이 좋다. 이렇게 하면 AOT 생성 코드에 오류가 없고 애플리케이션이 최종적으로 네이티브 이미지로 변환될 때 누락된 것이 없는지 신속하게 검증하는 데 도움이 된다.

JVM에서 스프링 부트 애플리케이션을 실행하고 AOT 생성 코드를 사용하려면 spring.aot.enabled 시스템 프로퍼티를 true로 설정할 수 있다.

다음은 예제이다.

$ java -Dspring.aot.enabled=true -jar myapplication.jar

테스트 중인 jar에 AOT 생성 코드가 포함되어 있는지 확인해야 한다. 메이븐의 경우 네이티브(native) 프로필을 활성화하려면 -Pnative를 사용하여 빌드해야 한다. 그레이들의 경우 빌드에 org.graalvm.buildtools.native 플러그인이 포함되어 있는지 확인해야 한다.

애플리케이션의 spring.aot.enabled 프로퍼티를 true로 설정하고 시작하면 네이티브 이미지로 변환 후 작동하는 것이 명확해진다.

실행 중인 애플리케이션에 대해 통합 테스트 실행을 고려할 수도 있다. 예를 들어 스프링 웹클라이언트(WebClient)를 사용하여 애플리케이션 REST 엔드포인트를 호출할 수 있다. 또는 Selenium과 같은 프로젝트를 사용하여 애플리케이션의 HTML 응답을 확인하는 것을 고려할 수도 있다.

15.3.2. 네이티브 빌드 툴 테스트(Testing With Native Build Tools)

GraalVM 네이티브 빌드 도구에는 네이티브 이미지 내에서 테스트를 실행하는 기능이 포함되어 있다. 이는 애플리케이션 내부가 GraalVM 네이티브 이미지에서 작동하는지 심층적으로 테스트하려는 경우에 유용할 수 있다.

실행할 테스트가 포함된 네이티브 이미지를 생성하는 작업은 시간이 많이 걸릴 수 있으므로 대부분의 개발자는 JVM을 로컬로 사용하는 것을 선호할 것이다. 그러나 CI 파이프라인의 일부로는 매우 유용할 수 있다. 예를 들어 하루에 한 번 기본 테스트를 실행하도록 선택할 수 있다.

스프링 프레임워크에는 테스트 실행을 위한 AOT 지원이 포함되어 있다. 모든 일반적인 스프링 테스트 기능은 네이티브 이미지 테스트와 함께 작동한다. 예를 들어 @SpringBootTest 어노테이션을 계속 사용할 수 있다. 스프링 부트 테스트 슬라이스를 사용하여 애플리케이션의 특정 부분만 테스트할 수도 있다.

스프링 프레임워크의 네이티브 테스트 지원은 다음과 같은 방식으로 작동한다.

  • 필요한 ApplicationContext 인스턴스를 검색하기 위해 테스트가 분석된다.
  • 이러한 각 애플리케이션 컨텍스트에 AOT 처리가 적용되고 애셋(assets)이 생성된다.
  • 생성된 애셋이 GraalVM에서 처리되는 네이티브 이미지가 생성된다.
  • 네이티브 이미지에는 검색된 테스트 목록으로 구성된 JUnit TestEngine도 포함되어 있다.
  • 네이티브 이미지가 시작되어 각 테스트를 실행하고 결과를 보고하는 엔진이 트리거된다.

메이븐 사용(Using Maven)

메이븐을 사용하여 기본 테스트를 실행하려면 pom.xml 파일이 spring-boot-starter-parent를 사용하는지 확인하자. 다음과 같은 <parent>이 있어야 한다.

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.1.1</version>
</parent>

spring-boot-starter-parent는 네이티브 테스트를 실행하는 데 필요한 실행을 구성하는 nativeTest 프로필을 선언한다. 커멘드라인에서 -P 플래그를 사용하여 프로필을 활성화할 수 있다.

spring-boot-starter-parent를 사용하지 않으려면 스프링 부트 플러그인의 process-test-aot 골(goal)과 네이티브 빌드 툴(Native Build Tools) 플러그인의 테스트 골(goal)에 대한 실행을 구성해야 한다.

이미지를 빌드하고 테스트를 실행하려면 nativeTest 프로필이 활성화된 테스트 골(goal)을 사용하자.

$ mvn -PnativeTest test

그레이들 사용(Using Gradle)

스프링 부트 그레이들 플러그인은 GraalVM 네이티브 이미지 플러그인이 적용될 때 AOT 테스트 태스크을 자동으로 구성한다. 그레이들 빌드에 org.graalvm.buildtools.native를 포함하는 플러그인 블록이 포함되어 있는지 확인해야 한다.

그레이들을 사용하여 네이티브 테스트를 실행하려면 nativeTest 태스크를 사용할 수 있다.

$ gradle nativeTest

15.4. 고급 네이티브 이미지 토픽(Advanced Native Images Topics)

15.4.1. 중첩된 구성 프로퍼티스(Nested Configuration Properties)

스프링 AOT 엔진에 의해 구성 프로퍼티에 대한 리플렉션 힌트가 자동으로 생성된다. 그러나 내부 클래스가 아닌 중첩 구성 프로퍼티는 @NestedConfigurationProperty로 어노테이션을 달아야 한다. 그렇지 않으면 감지되지 않으며 바인딩할 수 없다.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties(prefix = "my.properties")
public class MyProperties {
  private String name;

  @NestedConfigurationProperty
  private final Nested nested = new Nested();

  public String getName() {
    return this.name;
  }
  
  public void setName(String name) {
    this.name = name;
  }
  
  public Nested getNested() {
    return this.nested;
  } 
}

중첩된 위치는 다음과 같다.

public class Nested {
  private int number;

  public int getNumber() {
    return this.number;
  }

  public void setNumber(int number) {
    this.number = number;
  } 
}

위의 예제에서는 my.properties.namemy.properties.nested.number에 대한 프로퍼티스를 생성한다. 중첩 필드에 @NestedConfigurationProperty 어노테이션이 없으면 my.properties.nested.number 프로퍼티를 네이티브 이미지에 바인딩할 수 없다.

생성자 바인딩을 사용할 때 필드에 @NestedConfigurationProperty 어노테이션을 달아야 한다.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties(prefix = "my.properties")
public class MyPropertiesCtor {
  private final String name;

  @NestedConfigurationProperty
  private final Nested nested;

  public MyPropertiesCtor(String name, Nested nested) {
      this.name = name;
      this.nested = nested;
  }
  public String getName() {
    return this.name;
  }
  
  public Nested getNested() {
    return this.nested;
  } 
}

레코드를 사용할 때 @NestedConfigurationProperty로 파라미터에 어노테이션을 달아야 한다.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;

@ConfigurationProperties(prefix = "my.properties")
public record MyPropertiesRecord(String name, @NestedConfigurationProperty Nestednested) {
}

코틀린을 사용하는 경우, @NestedConfigurationProperty를 사용하여 데이터 클래스의 파라미터에 어노테이션을 달아야 한다.

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.NestedConfigurationProperty

@ConfigurationProperties(prefix = "my.properties")
data class MyPropertiesKotlin(
  val name: String,
  @NestedConfigurationProperty val nested: Nested
)

모든 상황에 퍼블릭 getter 및 setter를 사용하자. 그렇지 않으면 프로퍼티스를 바인딩할 수 없다.

15.4.2. 스프링 부트 실행가능한 Jar 변환(Converting a Spring Boot Executable Jar)

jar에 AOT 생성 애셋(asset)이 포함되어 있는 한 스프링 부트의 실행 가능 jar을 네이티브 이미지로 변환할 수 있다. 이는 다음과 같은 여러 가지 이유로 유용할 수 있다.

  • 일반 JVM 파이프라인을 유지하고 JVM 애플리케이션을 CI/CD 플랫폼의 네이티브 이미지로 전환할 수 있다.
  • 네이티브 이미지크로스 컴파일을 지원하지 않으므로 나중에 다른 OS 아키텍처로 변환하는 OS 중립 배포 아티팩트를 유지할 수 있다.

클라우드 네이티브 빌드팩(Cloud Native Buildpacks)을 사용하거나 GraalVM과 함께 제공되는 네이티브 이미지 도구을 사용하여 스프링 부트 실행 가능 jar을 네이티브 이미지로 변환할 수 있다.

실행 가능한 jar에는 생성된 클래스 및 JSON 힌트 파일과 같은 AOT 생성 애셋이 포함되어야 한다.

빌드팩 사용(Using Buildpacks)

스프링 부트 애플리케이션은 일반적으로 메이븐(mvn spring-boot:build-image) 또는 그레이들(gradle bootBuildImage) 통합을 통해 클라우드 네이티브 빌드팩을 사용한다. 그러나 pack을 사용하여 AOT 처리된 스프링 부트 실행 가능 jar을 기본 컨테이너 이미지로 전환할 수도 있다.

먼저, 도커 데몬을 사용할 수 있는지 확인하자(자세한 내용은 도커 가져오기 참고). 리눅스를 사용하는 경우 루트가 아닌 사용자를 허용하도록 구성하자.

또한 buildpacks.io의 설치 가이드에 따라 팩을 설치해야 한다.

myproject-0.0.1-SNAPSHOT.jar로 빌드된 AOT 처리 스프링 부트 실행 가능 jar이 대상 디렉토리에 있다고 가정하고 다음을 실행한다.

$ pack build --builder paketobuildpacks/builder:tiny \
    --path target/myproject-0.0.1-SNAPSHOT.jar \
    --env 'BP_NATIVE_IMAGE=true' \
    my-application:0.0.1-SNAPSHOT

이런 방식으로 이미지를 생성하기 위해 로컬 GraalVM을 설치할 필요는 없다.

압축이 완료되면, docker run을 사용하여 애플리케이션을 시작할 수 있다.

$ docker run --rm -p 8080:8080 docker.io/library/myproject:0.0.1-SNAPSHOT

GraalVM 네이티브 이미지 사용(Using GraalVM native-image)

AOT 처리된 스프링 부트 실행 가능 jar를 네이티브 실행 파일로 전환하는 또 다른 옵션은 GraalVM 네이티브 이미지 도구를 사용하는 것이다. 이것이 작동하려면 컴퓨터에 GraalVM 배포판이 필요하다. Liberica Native Image Kit 페이지에서 수동으로 다운로드하거나 SDKMAN!과 같은 다운로드 관리자를 사용할 수 있다.

myproject-0.0.1-SNAPSHOT.jar로 빌드된 AOT 처리 스프링 부트 실행 가능 jar이 대상 디렉토리에 있다고 가정하고 다음 내용을 실행한다.

$ rm -rf target/native
$ mkdir -p target/native
$ cd target/native
$ jar -xvf ../myproject-0.0.1-SNAPSHOT.jar
$ native-image -H:Name=myproject @META-INF/native-image/argfile -cp .:BOOT-
INF/classes:`find BOOT-INF/lib | tr '\n' ':'`
$ mv myproject ../

이러한 명령은 리눅스 또는 맥OS 컴퓨터에서 작동하지만 윈도우에 맞게 조정해야 한다.

@META-INF/native-image/argfile이 jar에 패키지화되어 있을 수 있다. 접근성(reachability) 메타데이터에 오버라이드가 필요한 경우에만 포함한다.

네이티브 이미지 -cp 플래그는 와일드카드를 허용하지 않는다. 모든 jar가 나열되어 있는지 확인해야 한다(위 명령은 이를 수행하기 위해 findtr을 사용한다).

15.4.3. 트레이싱 에이전트 사용(Using the Tracing Agent)

GraalVM 네이티브 이미지 트레이싱 에이전트를 사용하면 관련 힌트를 생성하기 위해 JVM에서 리플렉션, 리소스 또는 프록시 사용을 가로챌 수 있다. 스프링은 대부분의 힌트를 자동으로 생성해야 하지만 트레이싱 에이전트를 사용하면 누락된 항목을 빠르게 식별할 수 있다.

에이전트를 사용하여 네이티브 이미지에 대한 힌트를 생성하는 경우 몇 가지 접근 방식이 있다.

  • 애플리케이션을 직접 실행하고 실습해 보자.
  • 애플리케이션 테스트를 실행하여 애플리케이션을 시험해 보자.

첫 번째 옵션은 스프링이 라이브러리나 패턴을 인식하지 못할 때 누락된 힌트를 식별하는 데 유용하다.

두 번째 옵션은 반복 가능한 설정에 더 매력적으로 들리지만 기본적으로 생성된 힌트에는 테스트 인프라에 필요한 모든 것이 포함된다. 이들 중 일부는 애플리케이션이 실제로 실행될 때 불필요하다. 이 문제를 해결하기 위해 에이전트는 생성된 출력에서 ​​특정 데이터를 제외시키는 접근 필터 파일을 지원한다.

애플리케이션 직접 실행(Launch the Application Directly)

다음 명령을 사용하여 네이티브 이미지 트레이싱 에이전트가 연결된 애플리케이션을 시작한다.

$ java -Dspring.aot.enabled=true \
    -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ \
    -jar target/myproject-0.0.1-SNAPSHOT.jar

이제 힌트를 얻으려는 코드를 연습한 다음 ctrl-c를 사용하여 애플리케이션을 중지할 수 있다.

애플리케이션 종료 시 네이티브 이미지 트레이싱 에이전트는 지정된 구성 출력 디렉토리에 힌트 파일을 작성한다. 이러한 파일을 수동으로 검사하거나 네이티브 이미지 빌드 프로세스에 대한 입력으로 사용할 수 있다. 이를 입력으로 사용하려면 src/main/resources/META-INF/native-image/ 디렉토리에 복사하자. 다음에 네이티브 이미지를 빌드할 때 GraalVM은 이러한 파일을 고려한다.

네이티브 이미지 트레이싱 에이전트에 설정할 수 있는 고급 옵션이 있다(예: 호출자 클래스별로 기록된 힌트 필터링 등). 자세한 내용은 공식 문서를 참고하자.

15.4.4. 커스텀 힌트(Custom Hints)

리플렉션, 리소스, 직렬화, 프록시 사용 등에 대한 고유한 힌트를 제공해야 하는 경우 RuntimeHintsRegistrar API를 사용할 수 있다. RuntimeHintsRegistrar 인터페이스를 구현하는 클래스를 만든 다음 제공된 RuntimeHints 인스턴스를 적절하게 호출한다.

import java.lang.reflect.Method;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.util.ReflectionUtils;
public class MyRuntimeHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        // 리플렉션을 위한 메소드 등록
        Method method = ReflectionUtils.findMethod(MyClass.class, "sayHello", String.class);
        hints.reflection().registerMethod(method, ExecutableMode.INVOKE);
        // 리소스 등록
        hints.resources().registerPattern("my-resource.txt");
        // 직렬화 등록
        hints.serialization().registerType(MySerializableClass.class);
        // 프록시 등록
        hints.proxies().registerJdkProxy(MyInterface.class);
    }
}

그런 다음 @Configuration 클래스(예: @SpringBootApplication 어노테이션이 달린 애플리케이션 클래스)에서 @ImportRuntimeHints를 사용하여 해당 힌트를 활성화할 수 있다.

바인딩이 필요한 클래스가 있는 경우(JSON을 직렬화하거나 역직렬화할 때 주로 필요) 모든 빈에서 @RegisterReflectionForBinding을 사용할 수 있다. 예를 들어 @RestController 메서드에서 데이터를 수락하거나 반환할 때 대부분의 힌트는 자동으로 추론된다. 그러나 웹클라이언트(WebClient) 또는 레스트템플릿(RestTemplate)을 직접 사용하여 작업하는 경우 @RegisterReflectionForBinding을 사용해야 할 수도 있다.

커스텀 힌트 테스트(Testing custom hints)

RuntimeHintsPredicates API를 사용하여 힌트를 테스트할 수 있다. API는 RuntimeHints 인스턴스를 테스트하는 데 사용할 수 있는 조건자(Predicate)를 구축하는 메서드를 제공한다.

AssertJ를 사용하는 경우 테스트는 다음과 같다.

import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.docs.nativeimage.advanced.customhints.MyRuntimeHints;
import static org.assertj.core.api.Assertions.assertThat;

class MyRuntimeHintsTests {
  @Test
  void shouldRegisterHints() {
    RuntimeHints hints = new RuntimeHints();
    new MyRuntimeHints().registerHints(hints, getClass().getClassLoader());
    assertThat(RuntimeHintsPredicates.resource().forResource("my-resource.txt")).accepts(hints);
  }
}

15.4.5. 알려진 제한사항(Known Limitations)

GraalVM 네이티브 이미지는 진화하는 기술이며 모든 라이브러리가 지원을 제공하는 것은 아니다. GraalVM 커뮤니티는 아직 자체적으로 출시되지 않은 프로젝트에 대한 접근성(reachability) 메타데이터를 제공하여 도움을 주고 있다. 스프링 자체에는 서드파티 라이브러리에 대한 힌트가 포함되어 있지 않으며 대신 접근성 메타데이터 프로젝트에 의존한다.

스프링 부트 애플리케이션용 네이티브 이미지를 생성할 때 문제가 발생하는 경우 스프링 부트 위키의 GraalVM을 사용한 스프링 부트 페이지를 확인하자. 또한 일반적인 애플리케이션 타입이 예상대로 작동하는지 확인하는 데 사용되는 깃헙의 spring-aot-smoke-tests 프로젝트에 문제를 기여할 수도 있다.

GraalVM과 작동하지 않는 라이브러리를 찾으면 접근성 메타데이터 프로젝트에 문제를 제기해보자.

빌드 플러그인에서 제공하는 AOT 처리에 대해 자세히 알아보려면 메이븐그레이들 플러그인 문서를 참고하자. 처리를 수행하는 데 사용되는 API에 대해 자세히 알아보려면 스프링 프레임워크 소스의 org.springframework.aot.generateorg.springframework.beans.factory.aot 패키지를 찾아보자.

스프링 및 GraalVM의 알려진 제한 사항은 스프링 부트 위키를 참고하자.

다음 장에서는 스프링 부트 CLI를 다룬다.