Spring GraalVM Native Image 띄어보기

엄태권·2023년 3월 1일
5

Spring

목록 보기
1/1

서론

시작은 Docker에 대한 학습이었다. Docker를 다루어 보고 싶었고, Docker 이미지를 활용하여 SpringBoot Application을 띄어보고 싶었다. 의식의 흐름처럼 생각하다보니, SpringBoot는 Version 3를 사용해보자, 그런데 SrpingBoot 3에서 GraalVM 기반의 Spring Native 공식 지원 한다는 내용을 보았고, 이 Native를 활용하면, 이미지를 통한 빠른 start time을 가져갈 수 있다는 대략적인 장점만 보고 Spring Native를 활용해서 Docker 이미지를 빌드하고, 띄어보자로 이번 학습을 결정지었다.

GraalVm은?

공식문서 의 내용을 보자

GraalVm은 Java 및 기타 JVM 언어로 작성된 애플리케이션의 실행을 가속화하는 동시에,
JavaScript, Python 및 기타 여러 인기 언어에 대한 런타임을 제공하도록 설계된 고성능 JDK 입니다.

GraalVM은 Java 애플리케이션을 실행하는 두 가지 방법을 제공합니다. Graal JIT(Just-In-Time) 컴파일러가 있는 HotSpot JVM에서 또는 AOT(Ahead-of-Time) 컴파일된 네이티브 실행 파일로 실행됩니다

중요한 점은
HotSpot의 C2컴파일러의 대안으로 만들어진 것으로 기존의 OpenJDK 컴파일러는 C++과 Java로 만들어져있는데, 이를 Java로 만들어낸 컴파일러 이다.

이중 우리는 Graal의 특징인 Native Image를 사용하는 것이다.

AOT? JIT?

Graal의 공식 문서를 보면 JIT, AOT를 컴파일러 이야기가 나오는데 JIT 와 AOT는 뭘까?

JIT(Just-In-Time)
기본적으로 Java가 C보다 느리다 라고 생각하는 이유중 하나는 컴파일을 런타임시에 해야하기 떄문이다. 라는 부분이있다. 여기서 중요한 점은 컴파일을 런타임시에 한다. 이부분이다. 즉 런타임시에 기계어로 컴파일을 실행하는 점이 특징이며, 장점과 단점은 아래와 같다.

[장점]
JIT의 장점은 상황에 대한 최적화된 Code를 컴파일할 수 있다는 장점이 있다.(예를 들면, @Profile 처럼 특정 Profile에서 SpringBean등록 수행하는 상황)

[단점]
런타임 시에 OverHead가 발생할 수 있다.

AOT(Ahead-Of-Time)
실행하기 전에, 코드에 대한 정적코드 테스트를 만들고, 정적코드 테스트에 대한 결과로 기계어로 컴파일한다. 즉 런타임 시가 아닌 런타임 전에 컴파일을 진행한다

[장점]
런타임에 실행하는 속도가 JIT에 비해 빠르다.

[단점]
실행 환경 정보 수집이 어려움.
코드 최적화가 부족하다.

두 컴파일러의 궁극적 목표는 같다. 기계어로 컴파일하는 것이 목적이나, 그 시기에 따라 다르다는 점이 특징이며, 각자만의 장단점이 존재한다.(서로의 장단점이 서로 뒤바뀐 모습이기도 하다)

In Spring

우선 Spring에선 두컴파일러 방식을 모두 지원하며, 그중 이번 Boot ver 3에선 Naitve Image를 활용한 AOT 컴파일러 방식에 대한 공식적인 지원을 한다는게 특징이다.

그럼 Spring의 GraalVm Native Image 공식문서를 보자.

GraalVm Native Image 소개

  • Native 이미지란 Java 코드를 미리 바이너리(네이티브 실행 파일)로 컴파일 하는 기술을 말한다.
  • JVM의 필요한 리소스의 일부만 사용하기 떄문에 실행 비용이 낮다.
  • 애플리케이션 배포 및 실행에 기존보다 더 작은 메모리 공간과 훨씬 빠르게 실행이 가능하다.
  • 컨테이너 이미지를 사용하여 배포되는 애플리케이션에 매우 적합하다.

우선 AOT 의 특징과 크게 다를건 없으며, 컨테이너 이미지를 사용하여 배포되는 환경에 최적화란 점이 눈에 띈다.

Spring의 AOT
일반적인 SPring Boot의 경우 매우 동적이며, 런타임시 구성이 변경된다. 이러한 동적인 측면에 대해 GraalVm에 알릴 순 있지만, 네이티브 이미지를 만든다면, 닫힌세계를 가정하고 동적인 측면이 제한된다.

[닫힌세계란 ?]

  • 클래스 경로는 빌드 시 고정되고 완전히 정의된다.
  • @Profile 주석 및 profile별 구성은 지원되지 않는다.(???)
  • ConditionalOnProperty 및 .enable 속성은 지원되지 않는다.

뭔가 우리가 기존의 Spring application을 구동할때와는 좀 다른 부분이 눈에 띈다. 우선 간단하게 말하자면, 위에 말한 내용과 동일하게, JIT의 장점중 하나인 특정 환경에 특화된 Code 제공이 어렵다는 것이다.

SpringAOT의 생성 결과물

  • JAVA 소스코드
  • 바이트 코드
  • GrralVm JSON 힌트 파일
    resource-config.json
    reflect-config.json
    serialization-config.json
    proxy-config.json
    jni-config.json

우선 Json 파일들이 눈에 띄는데 뭔가 힌트를 사용해서 GraalVM이 코드를 직접 검사하여 이해할 수 없는 항목을 처리하는 방법을 설명하는 JSON 데이터가 포함할 수 있다고 한다.

그냥 띄워 보자

우선은 간단하게 Native Image 방식에 대해 알아봤고, 그럼 실제 간단한 프로젝트를 실행시켜 보도록 하자.

Boot Project의 시작은 잘 지원되어 있는 https://start.spring.io 를 사용했으며 아래와 같이 생성했다.

우선 나는 Gradle을 사용할 것이기 때문에 Gradle로 빌드하는 방식에 대해 설명할 예정이며 해당 과정을 진행하기 위해 Docker 설치가 필수이다.

Docker 설치시 오류 삽질(Window 환경)
Docker 설치시 Docker DeskTop이 띄어지기도 전에 오류가 발생해 종료가 되었는데 아래의 방법으로 해결이 가능했다. Window 환경이라면 참고해도 좋을거 같다.

1. Hyper-V가 필수적으로 켜져있어야 한다.(해당 부분은 Home 등의 윈도우 버전에선 지원불가)
2. CPU의 가상화가 켜있어야 한다.(해당부분은 검색하면 나오니 찾아보길)
3. 가상화가 꺼져있다면 각자의 CPU에 따라 BIOS Setting 에서 가상화 Enable이 필요하다.

사실 오류 발생 문구를 구글에 검색하면 더 빠르게 해결할 수 있으니, 오류는 각자 찾아보자..

그럼 이제 본격적으로 Gradle Native Image Build를 위한 순서이다.

org.graalvm.buildtols.native가 build.gradle plugin 블럭에 있어야 한다.
우리는 Dependency를 추가했기 때문에 있겠지만, 혹시나 없다면 아래와 같이 추가가 필요하다.

여기서 나는 우선 테스트를 위해 아래와 같이 간단한 서비스 등록을 진행해봤다.

Controller 등록Service 등록Dto 등록

Gradle로 Build를 해보자.

gradle bootBuildImage(윈도우는 .\gradlew.bat bootBuildImage)
해당 명령어로 build 시 Cloud Native BuildPack을 사용하여 알아서 이미지를 생성해준다.
SpringBoot 2.3부터 사용 가능하며, bootBuildImage 명령어는 애플리케이션에 맞는 설정을 바탕으로 레이어 별로 나누어 이미지를 생성하게 되고, 수정된 부분만 다시 빌드하게 된다.

위의 과정중 오류를 만나 기록해둔다.

gradle bootBuildImage 오류 삽질
No matching variant of org.springframework.boot:spring-boot-gradle-plugin:3.0.3 was found. The consumer was configured to find a runtime of a library compatible with Java 8

[해결방안]
우선 해당 명령어를 실행한 terminal에서 java 버전이 맞지 않는 버전을 사용하고 있는 부분이며,
java -version을 해본다면, 프로젝트와는 다른 버전의 Java임을 알 수 있다. Java version을 맞춰주자. 나의 경우 19이기 때문에 해당 터미널에서 19를 썼어야 했고, terminal에서 java를 유동적으로 바꾸기위해, mac에서 쓰던 sdkMan을 윈도우로 설치하였다(방법은 검색..)

[또 다른 방안]
사실 위의 부분은 명령어를 통한 해결을 위한 방법이고, 제일 간단한 방법은 Gradle에 있는 Task를 활용하면 간단하게 끝나난다.

이렇게 진행하면 아래와 같이 GraalVm을 활용하여 우리의 application을 정적 컴파일을 진행한다.

해당 부분을 확인하다보면 미리 스프링을 띄우고 이미지를 빌드하는 것을 볼 수 있으며, 어떠한 Profile을 사용하는지, 또한, 어떠한 Embeded tomcat을 사용하는지에 대한 정보들이 나오고 있다.
해당 작업은 초기작업으로 이미지를 만드는데 시간은 조금 오래걸리지만, 해당 이미지가 빌드되고 나면 application을 기동시에는 확연하게 줄어든 시간을 확인할 수 있을 것이다.

빌드가 완료되었다면, 위에서 말한 AOT의 출력 파일들을 확인해 보자!

AOT의 출력 파일
기본적으로 아래와 같은 경로에 빌드 파일들이 생성되며, 해당 부분에서 AOT가 어떻게 bean을 등록하는 지 확이이 가능하다.
target/spring-aot/main/resources(maven) , build/generated/aotResources(gradle)

나는 Gradle을 활용했기 때문에 해당 경로에 다음과 같은 파일들이 생성되었다.

우선 Spring은 Runtime시에 동적으로 bean을 등록한다.(JIT) 그렇다면, AOT에선 어떻게 Bean을 등록하여 컴파일 할까?

우선 위에서 등록한 Controller를 예로 보자.

사진을 보면, TestController가 있는 package경로에 TestController_BeanDefinitions.Class 파일이 생성되어있다. 대충보면 TestController에 대한 Bean정의 Calss 파일이다.

이어서 아래쪽의 DemoApplication__BeanFactoryRegistrations.Class를 확인해보자

빨간 네모 박스를 보면, 위에서 등록했던 TestController와, Service가 BeanFactory에 등록되는 것을 볼 수 있다. 신기한 것은 해당 부분에 Spring이 기동시 필요한 기본 Bean 등록들이 나열되어 있다는 것이다. (dispatcherServlet, restTemplate 등) 해당 부분을 보면 Spring 기동시 어떠한 bean들이 기본적으로 등록되는지 확인해 볼 수도 있을거 같다.

그럼 이제 해당 부분을 Docker로 띄어보자.

Intellij의 terminal에서 작업을 진행했다.
설치된 Docker를 활용하여 Docker 이미지를 run 시킬 예정이며, 명령어는 아래와 같다.
Docker run --rm -p 8081:8080 [이미지명] (명령어는 검색해보자)

참고로 Docker의 이미지 명을 알아볼 수 있는 명령어는 Docker images 이다.
해당 명령어 입력시 우리의 application 이름으로 이미지파일이 생성되어 있는 것을 볼 수 있다.
최종적으로 사진과 같이 Spring이 기동되는 것을 볼 수있다. 그렇다면 진짜 빠를까?

속도 비교
[일반 프로젝트 구동]

보이는 지모르겠지만 1.857 seconds 가 소비되었다. 그렇다면 Native는 ..?

[Native Image 구동]

0.05 seconds. 테스트해보면 알겠지만 build 과정이 생략되어 엄청 빠르게 구동되는것을 볼 수 있다.

이렇게 GraalVm을 활용한 Native 의 최대 장점인 실행시 빠른 구동까지 확인해 봤다. 그렇다면, 궁금한점이 하나 더 있다. 어떻게 우리는 Profile별로 bean 등록을 해야할까? 정말 @Profile은 못쓰는 것인가?

Profile 별 Bean 등록하기
우선 위에도 계속 말해왔던 내용이지만 동적인 Profile별 bean등록은 AOT에서 어렵다
그럼 우리는 어떻게 해결해볼 수 있을까?

위에서 말한 Native의 json hint 파일을 이용해도 좋겠지만, 우선은 내가 해볼 수 있는 방법을 해봤다.

@Profile("dev")를 주고, application.yml(properties)에 default profile을 줘보자!
우선 그냥 @Profile("dev")를 주고 빌드를 할경우 해당 Class 파일은 AOT 컴파일시 Spring에 Bean등록이 되지 않는다. 대신 우리는 기본적으로 profile 지정이 없을시 dev 활성화 시키도록 설정할 것이다.

그전에 다음과 같이 ProfileTest.class를 작성했다.

application.yml을 작성해보자(다음의 내용은 기본 default로 dev를 profile로 지정하겠다는 뜻이다.)

[application.yml 파일]

spring:
  profiles:
    active: dev

그리고 build 를 해보자

보이는 지모르겠지만, 우리가 등록했던 ProfileTest.class 가 정상적으로 bean 등록이 되는것을 확인할 수 있다.

이렇게 우선은 기본적인 application을 native image를 통해 띄어보는 과정을 진행해 봤다. 물론 내가 놓친것들도 많겠지만, 기본적으로 natvie가 어떻게 동작하는지를 설명해 봤다. 참고로 lombok의 경우 native에서 왠만한 것들은 원할하게 지원하나 특정 특이 케이스에 대해서는 위에서 언급한 json hint를 활용해야 한다고 한다!

정리

Docker를 공부하려다 이상하게 여기까지 흘러와 버렸지만, 의미 있는 시간이었다. 사실 AOT며 JIT며 한번도 들어본 적없는 용어고, Native는 난 쓸일 없을 테니까~~ 로 뒷전으로 생각하고 있던 내용이었다. 그치만 얻어가는게 참 많았던 부분이다. 사실 Spring 기동시 자동으로 주입되는 bean들에 대한 정보를 확인할 수 있다는 것 자체만으로도 큰 소득이 아닌가 싶었다.

참조

https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
https://github.com/spring-attic/spring-native/issues/381
https://www.youtube.com/watch?v=C7toO3WV1NQ

profile
https://github.com/Eom-Ti

0개의 댓글