Google Cloud 기반 Spring Native

1. 개요

이 Codelab에서는 Spring Native 프로젝트에 관해 알아보고 이를 사용하는 앱을 빌드하여 Google Cloud에 배포합니다.

구성 요소, 프로젝트의 최근 기록, 몇 가지 사용 사례, 프로젝트에서 해당 요소를 사용하는 데 필요한 단계를 살펴봅니다.

Spring Native 프로젝트는 현재 실험 단계에 있으므로 시작하려면 몇 가지 특정 구성이 필요합니다. 하지만 SpringOne 2021에서 발표된 바와 같이 Spring Native는 일급 지원을 통해 Spring Framework 6.0 및 Spring Boot 3.0에 통합될 예정이므로 출시 몇 달 전에 프로젝트를 자세히 살펴보기에 지금이 절호의 기회입니다.

Just-in-Time 컴파일은 장기 실행 프로세스와 같은 작업에 매우 잘 최적화되었지만, AOT 컴파일한 애플리케이션의 성능이 훨씬 더 뛰어난 특정 사용 사례도 있습니다. 이에 관해서는 Codelab에서 살펴보겠습니다.

다음 실습에서는

  • Cloud Shell 사용하기
  • Cloud Run API 사용 설정
  • Spring Native 앱 만들기 및 배포
  • 이러한 앱을 Cloud Run에 배포

필요한 항목

설문조사

이 튜토리얼을 어떻게 사용하실 계획인가요?

읽기만 할 계획입니다 읽은 다음 연습 활동을 완료할 계획입니다

귀하의 Java 사용 경험을 평가해 주세요.

초급 중급 고급

귀하의 Google Cloud 서비스 사용 경험을 평가해 주세요.

<ph type="x-smartling-placeholder"></ph> 초보자 중급 숙련도

2. 배경

Spring Native 프로젝트는 개발자에게 네이티브 애플리케이션 성능을 제공하기 위해 여러 기술을 사용합니다.

Spring Native를 완전히 이해하려면 몇 가지 구성요소 기술, 이러한 구성요소 기술의 이점, 이러한 기술이 함께 작동하는 방식을 이해하면 도움이 됩니다.

AOT 컴파일

개발자가 컴파일 시간에 javac를 정상적으로 실행하면 .java 소스 코드가 바이트 코드로 작성된 .class 파일로 컴파일됩니다. 이 바이트코드는 Java 가상 머신에서만 이해하는 것이기 때문에 JVM이 코드를 실행하기 위해 다른 머신에서 이 코드를 해석해야 합니다.

이 프로세스 덕분에 Java의 특징 이식성을 확보할 수 있기 때문에 '한 번만 작성하면 어디서든 실행'할 수 있지만 네이티브 코드를 실행하는 것과 비교하면 비용이 많이 듭니다.

다행히도 대부분의 JVM 구현은 적시 컴파일을 사용하여 이러한 해석 비용을 완화합니다. 이는 함수의 호출을 세는 방식으로 이루어지며, 기준점 ( 기본적으로 10,000개)을 초과할 정도로 자주 호출되는 경우 더 많은 비용이 드는 해석을 방지하기 위해 런타임 시 네이티브 코드로 컴파일됩니다.

Ahead-of-time 컴파일은 반대 접근 방식을 취합니다. 즉, 도달 가능한 모든 코드를 컴파일 시간에 네이티브 실행 파일에 컴파일합니다. 이렇게 하면 이식성 대신 메모리 효율성과 기타 런타임 성능 향상을 기대할 수 있습니다.

5042e8e62a05a27.png

물론 이는 타협해야 하는 조치이며 받을 가치가 항상 있는 것은 아닙니다. 그러나 AOT 컴파일은 다음과 같은 특정 사용 사례에서 유용할 수 있습니다.

  • 시작 시간이 중요한 단기 애플리케이션
  • JIT의 비용이 많이 들 수 있는 메모리 제약이 높은 환경

재미있는 사실은 AOT 컴파일이 JDK 9에서 실험용 기능으로 도입되었다는 것입니다. 하지만 이 구현은 유지 비용이 많이 들고 제대로 포착되지 않았기 때문에 Java 17에서 조용하게 삭제되어 GraalVM만 사용하는 개발자를 위해 만들어졌습니다.

GraalVM

GraalVM은 고도로 최적화된 오픈소스 JDK 배포로, 매우 빠른 시작 시간, AOT 네이티브 이미지 컴파일, 개발자가 여러 언어를 단일 애플리케이션에 혼합할 수 있는 다국어 기능을 자랑합니다.

GraalVM은 항상 새로운 기능을 얻고 기존 기능을 개선하는 등 활발히 개발 중이므로 개발자들은 계속해서 지켜봐 주시기 바랍니다.

최근 주요 시점은 다음과 같습니다.

  • 새로운 사용자 친화적인 네이티브 이미지 빌드 출력 ( 2021년 1월 18일)
  • 자바 17 지원 ( 2022-01-18)
  • 다국어 컴파일 시간을 개선하기 위해 다중 계층 컴파일을 기본적으로 사용 설정 ( 2021년 4월 20일)

스프링 네이티브

간단히 말해 Spring Native는 GraalVM의 네이티브 이미지 컴파일러를 사용하여 Spring 애플리케이션을 네이티브 실행 파일로 변환할 수 있도록 지원합니다.

이 프로세스에는 컴파일 시점에 애플리케이션의 정적 분석을 수행하여 진입점에서 도달할 수 있는 애플리케이션의 모든 메서드를 찾는 작업이 포함됩니다.

이는 기본적으로 '폐쇄된 세계'를 애플리케이션 콘셉트에 적용됩니다. 즉, 컴파일 시점에 모든 코드가 알려져 있다고 가정하고 런타임에 새로운 코드를 로드할 수 없습니다.

네이티브 이미지 생성은 일반 애플리케이션을 컴파일하는 것보다 오래 걸리는 메모리 집약적인 프로세스이며 Java의 특정 측면에 제한을 적용한다는 점에 유의해야 합니다.

경우에 따라 애플리케이션을 Spring Native에서 작동하기 위해 코드를 변경하지 않아도 됩니다. 하지만 제대로 작동하려면 특정 네이티브 구성이 필요한 경우도 있습니다. 이러한 상황에서 Spring Native는 이 프로세스를 간소화하기 위해 네이티브 힌트를 제공하는 경우가 많습니다.

3. 설정/사전 작업

Spring Native를 구현하기 전에 나중에 네이티브 버전과 비교할 수 있는 성능 기준을 설정하기 위해 앱을 만들고 배포해야 합니다.

1. 프로젝트 만들기

먼저 start.spring.io에서 앱을 가져옵니다.

curl https://start.spring.io/starter.zip -d dependencies=web \
           -d javaVersion=11 \
           -d bootVersion=2.6.4 -o io-native-starter.zip

이 시작 앱은 작성 당시 스프링 네이티브 프로젝트에서 지원하는 최신 버전인 Spring Boot 2.6.4를 사용합니다.

GraalVM 21.0.3 출시 이후 이 샘플에도 자바 17을 사용할 수 있습니다. 이 튜토리얼에서는 관련 구성을 최소화하기 위해 Java 11을 계속 사용합니다.

명령줄에서 zip 파일을 받으면 프로젝트의 하위 디렉터리를 생성하고 여기에 폴더의 압축을 풀 수 있습니다.

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. 코드 변경사항

프로젝트가 열리면 우리는 생명의 징후를 빠르게 추가하고 봄철 네이티브 공연을 보여드리겠습니다.

다음과 일치하도록 DemoApplication.java를 수정합니다.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.Instant;

@RestController
@SpringBootApplication
public class DemoApplication {
    private static Instant startTime;
    private static Instant readyTime;

    public static void main(String[] args) {
        startTime = Instant.now();
                SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/")
    public String index() {
        return "Time between start and ApplicationReadyEvent: "
                + Duration.between(startTime, readyTime).toMillis()
                + "ms";
    }

    @EventListener(ApplicationReadyEvent.class)
    public void ready() {
                readyTime = Instant.now();
    }
}

이제 기준 앱을 사용할 준비가 되었습니다. 기본 애플리케이션으로 변환하기 전에, 이미지를 빌드하고 로컬에서 실행하여 시작 시간을 알 수 있습니다.

이미지를 빌드하는 방법은 다음과 같습니다.

mvn spring-boot:build-image

docker images demo를 사용하여 기준 이미지 크기를 파악할 수도 있습니다(6ecb403e9af1475e.png).

앱을 실행하려면 다음 단계를 따르세요.

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3. 기준 앱 배포

이제 앱을 만들었으므로 앱을 배포하고 시간을 기록해 둡니다. 이 시간은 나중에 네이티브 앱 시작 시간과 비교하겠습니다.

빌드 중인 애플리케이션의 유형에 따라 내 항목을 여러 다른 방식으로 호스팅할 수 있습니다.

하지만 이 예는 매우 간단하고 간단한 웹 애플리케이션이므로 작업을 단순하게 유지하고 Cloud Run을 사용할 수 있습니다.

자체 머신에서 이 작업을 진행하는 경우 gcloud CLI 도구를 설치 및 업데이트해야 합니다.

Cloud Shell에서 이 모든 작업이 처리되고 소스 디렉터리에서 다음을 실행하기만 하면 됩니다.

gcloud run deploy

4. 애플리케이션 구성

1. Maven 저장소 구성

이 프로젝트는 아직 실험 단계이므로 Maven의 중앙 저장소에서 사용할 수 없는 실험용 아티팩트를 찾을 수 있도록 앱을 구성해야 합니다.

이 작업을 수행하려면 pom.xml에 다음 요소를 추가해야 합니다. 이 작업은 원하는 편집기에서 수행할 수 있습니다.

pom에 다음 저장소 및 pluginRepositories 섹션을 추가합니다.

<repositories>
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

2. 종속 항목 추가

다음으로 Spring 애플리케이션을 네이티브 이미지로 실행하는 데 필요한 Spring 네이티브 종속 항목을 추가합니다. 참고: Gradle을 사용하는 경우에는 이 단계가 필요하지 않습니다.

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.11.2</version>
    </dependency>
</dependencies>

3. 플러그인 추가/사용 설정

이제 AOT 플러그인을 추가하여 네이티브 이미지 호환성과 공간을 개선합니다 ( 자세히 알아보기).

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-aot-maven-plugin</artifactId>
        <version>0.11.2</version>
        <executions>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

이제 sample-boot-maven-plugin을 업데이트하여 네이티브 이미지를 지원하고 paketo 빌더를 사용하여 네이티브 이미지를 빌드합니다.

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                    <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
            </image>
        </configuration>
    </plugin>    
</plugins>

작은 빌더 이미지는 여러 옵션 중 하나일 뿐입니다. 추가 라이브러리와 유틸리티가 거의 없어 공격 표면을 최소화하는 데 도움이 되므로 사용 사례에 적합합니다.

예를 들어 몇 가지 일반적인 C 라이브러리에 액세스해야 하는 앱을 빌드하고 있거나 아직 앱의 요구사항을 확신할 수 없는 경우에는 전체 빌더가 더 적합할 수 있습니다.

5. 네이티브 앱 빌드 및 실행

모두 준비되면 이미지를 빌드하고 컴파일된 네이티브 앱을 실행할 수 있습니다.

빌드를 실행하기 전에 유의해야 할 사항은 다음과 같습니다.

  • 일반 빌드보다 시간이 더 걸립니다 (몇 분). d420322893640701.png
  • 이 빌드 프로세스는 많은 메모리 (몇 GB)를 사용할 수 있습니다. cda24e1eb11fdbea.png
  • 이 빌드 프로세스를 사용하려면 Docker 데몬에 연결할 수 있어야 합니다.
  • 이 예에서는 프로세스를 수동으로 진행하지만 네이티브 빌드 프로필을 자동으로 트리거하도록 빌드 단계를 구성할 수도 있습니다.

이미지를 빌드하는 방법은 다음과 같습니다.

mvn spring-boot:build-image

빌드가 완료되면 네이티브 앱이 실제로 작동하는 모습을 볼 수 있습니다.

앱을 실행하려면 다음 단계를 따르세요.

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

이 시점에서는 기본 적용 방정식의 양측을 모두 확인할 수 있는 유리한 위치에 서게 되었습니다.

컴파일 시 약간의 시간과 추가 메모리 사용량을 포기했지만 그 대가로 워크로드에 따라 훨씬 더 빠르게 시작할 수 있고 훨씬 적은 메모리를 소비할 수 있는 애플리케이션을 얻었습니다.

docker images demo를 실행하여 네이티브 이미지의 크기를 원본과 비교하면 극적으로 감소하는 것을 확인할 수 있습니다.

e667f65a011c1328.png

또한 더 복잡한 사용 사례에서는 앱의 런타임 시 실행할 작업을 AOT 컴파일러에 알리기 위해 추가 수정이 필요합니다. 따라서 일괄 작업과 같은 예측 가능한 특정 워크로드가 여기에 적합할 수 있지만 광고효과가 더 큰 워크로드도 있습니다.

6. 네이티브 앱 배포

앱을 Cloud Run에 배포하려면 네이티브 이미지를 Artifact Registry와 같은 패키지 관리자로 가져와야 합니다.

1. Docker 저장소 준비

저장소를 만들어 이 프로세스를 시작할 수 있습니다.

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

다음으로 새 레지스트리로 푸시하도록 인증되었는지 확인합니다.

gcloud CLI를 사용하면 이 프로세스를 상당히 간소화할 수 있습니다.

gcloud auth configure-docker us-central1-docker.pkg.dev

2. Artifact Registry로 이미지 푸시

다음으로 이미지에 태그를 지정합니다.

export PROJECT_ID=$(gcloud config list --format 'value(core.project)')


docker tag  demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

그런 다음 docker push를 사용하여 Artifact Registry로 전송할 수 있습니다.

docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

3. Cloud Run에 배포

이제 Artifact Registry에 저장한 이미지를 Cloud Run에 배포할 준비가 되었습니다.

gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

앱을 네이티브 이미지로 빌드하고 배포했기 때문에 애플리케이션이 실행될 때 인프라 비용을 잘 활용하고 있습니다.

기준 앱의 시작 시간과 이 새로운 기본 앱의 시작 시간을 직접 비교해 보세요.

6dde63d35959b1bb.png

7. 요약/삭제

축하합니다. Google Cloud에서 Spring Native 애플리케이션을 빌드하고 배포하셨습니다.

이 튜토리얼이 Spring Native 프로젝트에 더 익숙해지고 향후 니즈를 충족할 경우 이를 염두에 두는 데 도움이 되기를 바랍니다.

선택사항: 서비스 정리 및/또는 사용 중지

이 Codelab용으로 Google Cloud 프로젝트를 만들었든 기존 프로젝트를 재사용하든 상관없이 Google에서 사용한 리소스에서 불필요한 비용이 청구되지 않도록 주의하세요.

생성된 Cloud Run 서비스를 삭제 또는 사용 중지하거나, 호스팅한 이미지를 삭제하거나, 전체 프로젝트를 종료할 수 있습니다.

8. 추가 리소스

Spring Native 프로젝트는 현재 새롭고 실험적인 프로젝트이지만, 얼리 어답터가 문제를 해결하고 참여하는 데 도움이 되는 훌륭한 리소스가 이미 풍부합니다.

추가 리소스

다음은 이 튜토리얼과 관련이 있을 수 있는 온라인 리소스입니다.

라이선스

이 작업물은 Creative Commons Attribution 2.0 일반 라이선스에 따라 사용이 허가되었습니다.