CDS와 AOT, Layered JAR로 Spring Boot 시작 시간, 메모리 최적화하기

2024. 12. 23. 21:21·Spring

개인적으로 테스트나 서비스 운영 목적으로 라즈베리파이를 활용해 소규모 서버를 운영하고 있습니다. 리소스가 매우 제한적인 화경에서 운영하다 보니, 자원을 조금이라도 효율적으로 활용하기 위해 다양한 시도를 해왔습니다. 이 과정에서 CDS(Class Date Sharing)와 AOT(Ahead-of-Time Compilation)라는 기술을 알게 되었고, 실제로 적용한 결과 애플리케이션의 시작 시간과 메모리 사용량에서 유의미한 개선을 확인할 수 있었습니다.

 

Spring 공식 블로그에서 언급된 것처럼 극적인 개선은 이루어지지 않았지만, 리소스가 제한된 환경에서도 충분히 의미 있는 성과를 얻을 수 있었습니다. 특히, 이번에 적용한 프로젝트는 규모가 작은 편이었지만, 프로젝트 규모가 클수록 이러한 기술의 효과는 더 크게 나타날 것으로 예상됩니다.

 

본론에 들어가기 전에, 이번 글에서 다룰 내용을 이해하기 위해 지난번에 소개했던 Docker Multi-stage Builds와 Layered JAR에 대한 개념을 먼저 숙지하시길 권장합니다. 아래 참고 링크를 통해 관련 내용을 확인하실 수 있습니다.

 

참고: Docker Multi-stage Builds, Layered JAR로 빌드 성능과 이미지 최적화하기

 

0. 프로젝트 환경

  • Raspberry Pi 5
  • Ubuntu 24.04.1 LTS
  • Docker 27.3.1
  • Spring Boot 3.3.4
  • Gradle 8.11.1
  • JDK 21

 

1. Class Data Sharing(CDS)

CDS는 JDK 5에서 처음 도입된 JVM의 기능으로, JVM 시작 시간을 단축하고 메모리 사용량을 감소시키는 데 중점을 둔 기술입니다. Java 애플리케이션의 표준 라이브러리 클래스 메타데이터를 공유 가능한 아카이브 파일로 저장하고, 이를 여러 JVM 프로세스가 읽기 전용 메모리로 공유하여 성능을 최적화합니다.

 

동작 방식

1. 시스템 JAR 파일에서 기본 클래스 집합을 로드

JVM 실행에 필수적이고 자주 사용되는 표준 라이브러리 클래스의 메타데이터를 아카이브 파일로 저장합니다. 이렇게 생성된 메타데이터는 JVM이 클래스 로딩 시 반복적으로 수행해야 하는 작업(파싱, 초기화 등)을 미리 처리하여, JVM 시작 시간을 단축하고 메모리 효율을 높입니다.

  • 시스템 JAR 파일을 JDK 8 이하의 버전에서는 `rt.jar`, JDK 9 이상의 버전에서는 `lib/modules` 파일을 의미합니다.
  • 메타데이터에는 클래스 구조, 메서드 시그니처, 필드 정보 등이 포함됩니다.

 

2. 공유 아카이브 파일 공유

생성된 공유 아카이브 파일은 여러 JVM 프로세스 간에 공유됩니다. JVM은 공유 아카이브 파일을 읽기 전용 메모리에 매핑하여, 여러 프로세스에서 재사용합니다. 동일한 메타데이터를 메모리에 중복 저장할 필요가 없으므로, 메모리 사용량이 감소됩니다.

 

3. 클래스 로딩 과정 최적화

클래스 파일을 디스크에서 로드하지 않고 메모리에 매핑된 공유 아카이브에서 바로 가져옵니다. 여러 JVM 프로세스가 동일한 아카이브 파일을 참조하여 중복 메모리 사용을 방지합니다. 또한, 클래스 로딩 과정에서 발생하는 파싱, 링크 등의 작업을 생략하여 클래스 로딩 속도가 향상됩니다.

 

2. Application Class Data Sharing(AppCDS)

AppCDS는 JDK 10에서 도입된 CDS의 확장 기능으로, 애플리케이션 클래스와 사용자 정의 클래스 로더까지 아카이브에 포함합니다. 표준 라이브러리 클래스 외에도 애플리케이션 특화된 클래스를 공유할 수 있어, 기존의 CDS 방식보다 JVM 시작 시간이 단축되며 메모리 사용량이 감소됩니다. Spring Framework 6.1과 Spring Boot 3.3 이상의 버전에서 공식적으로 지원됩니다.

 

CDS with Spring Framework 6.1

Spring Boot CDS support and Project Leyden anticipation

 

동작 방식

1. 애플리케이션 클래스와 사용자 정의 클래스 로더를 포함한 아카이브 생성

표준 라이브러리 클래스 외에도 애플리케이션 클래스(`@Service`, `@Repository` 등의 애너테이션이 붙은 클래스)와 사용자 정의 클래스 로더(`RestartClassLoader` 등)가 로드하는 클래스까지 아카이브 파일에 포함하여 저장합니다.

 

2. 클래스 메타데이터 공유

생성된 아카이브 파일은 기존 CDS와 동일하게 읽기 전용 메모리에 매핑되어 JVM 프로세스 간에 공유됩니다. 이를 통해 스프링 빈과 같은 애플리케이션 특화 클래스의 메타데이터를 중복 저장하지 않게 되어 메모리 사용량이 줄어듭니다.

 

AppCDS와 기존 CDS의 차이점

구분 CDS AppCDS
도입 버전 JDK 5 JDK 10
아카이브 대상 표준 라이브러리 클래스 표준 라이브러리 + 애플리케이션 클래스
클래스 로더 기본 시스템 클래스 로더 사용자 정의 클래스 로더까지 포함
Spring 지원 여부 공식 지원 없음 Spring Boot 3.3+에서 공식 지원

 

3. Spring AOT (Ahead-of-Time)

Spring AOT는 Spring Framework에서 제공하는 성능 최적화 기능입니다. 런타임 작업을 애플리케이션 빌드 시점에 미리 수행하여 애플리케이션의 시작 시간을 단축하고 메모리 사용량을 최적화합니다. Spring 애플리케이션을 GraalVM Native Image로 배포하는 데 초점을 두고 있지만, 표준 JVM에서도 사용이 가능합니다.

 

AOT와 기존 동작의 차이점

구분 기존 동작 AOT 동작
ApplicationContext 초기화 런타임에 빈 정의, 의존성, 설정을 분석하고 초기화함. 빌드 시점에 ApplicationContext를 분석하여 최적화된 정적 코드를 생성하고 초기화 시간을 단축함.
리플렉션 호출 런타임에 리플렉션 호출을 통해 클래스 정보를 동적으로 처리함. 리플렉션 호출을 최소화하고 정적 코드를 생성하여 리플렉션으로 인한 성능 저하를 방지함.
빈 정의 등록 방식 빈 정의와 의존성을 런타임에 동적으로 확인하고 등록함. 정적 코드로 빈 정의를 등록하고, 불필요한 동적 검사를 제거함.
프로파일 및 조건부 설정 런타임에 활성화된 프로파일 및 조건부 설정을 평가함. 빌드 시점에 활성화된 프로파일과 조건부 설정을 평가하여 고정된 설정으로 컴파일함.
클래스 로딩 속도 클래스 로딩 작업이 런타임에 수행되어 시작 시간이 길어질 수 있음. 정적 코드와 사전 생성된 메타데이터를 활용하여 클래스 로딩 속도를 크게 향상시킴.
GraalVM Native Image 런타임 로직이 복잡해 네이티브 이미지 생성 시 수작업으로 힌트를 제공해야 할 수 있음. GraalVM Native Image를 위해 필요한 힌트를 자동 생성하여 네이티브 이미지 생성이 간편해짐.
처리 방식 런타임 동작 중심으로 모든 클래스가 동적으로 처리됨. 정적 코드 생성으로 고정된 클래스 경로 및 빈 정의를 기반으로 최적화된 실행 환경을 제공함.

 

4. Docker 빌드 프로세스에 적용하기

빌드 프로세스 적용은 다음과 같이 총 5단계로 구성됩니다. AOT와 CDS 방식 각각에 대한 이해도를 높이고, 필요에 따라 최적화 기법을 조합하여 활용할 수 있도록 각 단계를 세분화하여 설명합니다. 본 글에서는 제가 진행 중인 멀티 모듈 구조의 프로젝트를 기준으로 설명하며, 단일 모듈 환경을 사용하시는 분들은 아래 저장소의 코드를 참고하시면서 글을 읽어 나가는 걸 권장드립니다.

 

저장소: spring-boot-docker-optimizations

 

4-1. 기본 설정

1. 빌드 단계 정의

FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl AS build
WORKDIR /app

`FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl AS build`

Liberica JDK 21 공식 이미지를 사용하여 빌드 단계를 정의합니다. 프로젝트와 JDK 버전이 다를 경우, Docker Hub Liberica 공식 이미지에서 적합한 이미지를 선택해 사용할 수 있습니다. 최적화 기법에 따른 성능 비교를 위해 모든 빌드 프로세스에서 동일한 이미지를 사용하며, 본 글에서 CDS(Class Data Sharing) 적용에 관한 내용을 다루기 때문에, CDS 기능이 포함된 컨테이너 이미지를 사용합니다.

  • `AS build`: `AS` 키워드를 사용해 단계명(스테이지명)을 지정합니다. 이를 통해 다른 단계에서 `COPY --from=build`와 같은 방식으로 이 단계를 참조할 수 있습니다. `AS`를 사용하지 않는 경우, Docker는 자동으로 0부터 시작하는 숫자 ID를 부여합니다.

 

`WORKDIR /app`

컨테이너 내부에서 작업 디렉토리를 `/app`으로 설정합니다. 이후 모든 명령은 이 디렉토리에서 실행됩니다. 작업 디렉토리를 명시적으로 설정하면, 경로 관리가 간편해지고 예기치 않은 경로 문제를 방지할 수 있습니다.

 

2. Gradle Wrapper 복사 및 실행 권한 부여

COPY gradlew /app/
COPY gradle /app/gradle
RUN chmod +x ./gradlew

`COPY gradlew /app/`, `COPY gradle /app/gradle`

Gradle Wrapper 스크립트(`gradlew`)와 관련 디렉토리(`gradle/`)를 컨테이너의 `/app` 디렉토리에 복사합니다. Gradle Wrapper를 통해 프로젝트에 지정된 Gradle 버전을 사용하도록 합니다.

 

`RUN chmod +x ./gradlew`

`gradlew` 스크립트에 실행 권한(`+x`)을 부여하여 Docker 컨테이너 내에서 Gradle 명령을 실행할 수 있도록 합니다.

 

3. Gradle 설정 파일 및 buildSrc 복사 후 의존성 설치

COPY build.gradle.kts settings.gradle gradle.properties /app/
COPY buildSrc /app/buildSrc
RUN ./gradlew dependencies --parallel --stacktrace

`COPY build.gradle.kts settings.gradle gradle.properties /app/`, `COPY buildSrc /app/buildSrc`

Gradle 설정 파일과 `buildSrc` 디렉토리를 컨테이너의 `/app` 디렉토리에 복사합니다. 프로젝트의 의존성과 빌드 설정을 관리하기 위한 파일이며, 프로젝트에 따라 필요하지 않은 파일 또는 디렉토리를 스크립트에서 제거할 수 있습니다.

 

`RUN ./gradlew dependencies --parallel --stacktrace`

Gradle의 `dependencies` 태스크를 실행하여 프로젝트의 의존성을 다운로드하고 캐시에 저장합니다.

  • `--parallel`: 의존성이 없는 작업을 병렬로 실행하여 처리 속도를 향상시킵니다.
  • `--stacktrace`: 태스크 실패 시 상세한 오류 정보를 출력하여 디버깅을 용이하게 합니다.

 

4. 소스 코드 복사 및 애플리케이션 빌드

COPY . /app
RUN ./gradlew bootJar --no-daemon --build-cache --stacktrace

`COPY . /app`

프로젝트의 소스 코드를 컨테이너의 `/app` 디렉토리에 복사합니다. 단일 모듈 프로젝트의 경우 `COPY src /app/src`로 대체 가능합니다. 소스 코드가 변경되면 이 단계부터 빌드가 다시 실행됩니다.

 

`RUN ./gradlew bootJar --no-daemon --build-cache --stacktrace`

Gradle의 `bootJar` 태스크를 실행하여 Spring Boot 애플리케이션의 실행 가능한 JAR 파일을 생성합니다.

  • `--no-daemon`: Gradle 데몬을 비활성화하여 각 빌드가 독립적으로 실행되도록 설정합니다.
  • `--build-cache`: Gradle 빌드 캐시를 활성화하여 빌드 속도를 향상시킵니다. 이전에 실행된 빌드 작업과 입력값이 동일한 경우, 작업 결과를 캐시에서 재사용하여 불필요한 재실행을 방지합니다.

 

5. 런타임 단계 정의

FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl
WORKDIR /app

`FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl`

런타임 단계에서는 빌드 작업 없이 애플리케이션 실행만 포함되므로, 경량화된 JRE 이미지를 사용하여 런타임 환경을 구성합니다.

CDS를 사용할 경우, 빌드 단계 이미지와 런타임 단계 이미지가 다르면 메타데이터 불일치로 인해 최적화가 이루어지지 않을 수 있습니다. 이를 방지하기 위해, 빌드 단계와 동일한 JIT 컴파일러를 사용하는 JRE 이미지를 적용합니다.

 

6. 빌드된 JAR 파일 복사

COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

빌드 단계(`build`)에서 생성된 `stempo-api.jar` 파일을 런타임 단계의 `/app` 디렉토리에 복사합니다. 단일 모듈 프로젝트의 경우 JAR 파일은 일반적으로 `/app/build/libs` 디렉토리에 생성됩니다.

 

7. 포트 설정 및 애플리케이션 실행

EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]

`EXPOSE 8080`

컨테이너가 외부와 통신할 수 있도록 8080 포트를 노출합니다.

 

`ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]`

컨테이너가 시작될 때 실행할 명령어를 정의합니다.

  • `-Dspring.profiles.active=prod`: `prod` 프로파일을 활성화합니다.

 

Dockerfile

# 1. 빌드 단계
FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl AS build
WORKDIR /app

# Gradle Wrapper 복사 및 실행 권한 부여
COPY gradlew /app/
COPY gradle /app/gradle
RUN chmod +x ./gradlew

# Gradle 설정 파일 및 buildSrc 복사 후 의존성 설치
COPY build.gradle.kts settings.gradle gradle.properties /app/
COPY buildSrc /app/buildSrc
RUN ./gradlew dependencies --parallel --stacktrace

# 소스 코드 복사
COPY . /app

# 애플리케이션 빌드
RUN ./gradlew bootJar --no-daemon --build-cache --stacktrace


# 2. 런타임 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl
WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

# 포트 설정 및 애플리케이션 실행
EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]

 

4-2 CDS 적용

Dockerfile

# 1. 빌드 단계
FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl AS build
WORKDIR /app

# Gradle Wrapper 복사 및 실행 권한 부여
COPY gradlew /app/
COPY gradle /app/gradle
RUN chmod +x ./gradlew

# Gradle 설정 파일 및 buildSrc 복사 후 의존성 설치
COPY build.gradle.kts settings.gradle gradle.properties /app/
COPY buildSrc /app/buildSrc
RUN ./gradlew dependencies --parallel --stacktrace

# 소스 코드 복사
COPY . /app

# 애플리케이션 빌드
RUN ./gradlew bootJar --no-daemon --build-cache --stacktrace


# 2. CDS 아카이브 생성 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl AS cds
WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

# CDS 아카이브 생성
RUN java -XX:ArchiveClassesAtExit=./stempo-cds.jsa \
         -Dspring.profiles.active=cds \
         -Dspring.context.exit=onRefresh \
         -jar stempo-api.jar


# 3. 런타임 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl
WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

# CDS 아카이브 파일 복사
COPY --from=cds /app/stempo-cds.jsa ./

# 포트 설정 및 애플리케이션 실행
EXPOSE 8080
ENTRYPOINT ["java", "-XX:SharedArchiveFile=stempo-cds.jsa", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]

변경 사항

1. CDS 아카이브 생성

RUN java -XX:ArchiveClassesAtExit=./stempo-cds.jsa \
         -Dspring.profiles.active=cds \
         -Dspring.context.exit=onRefresh \
         -jar stempo-api.jar

Spring Boot 애플리케이션을 실행하면서 CDS 아카이브 파일(`stempo-cds.jsa`)을 생성합니다. 생성된 아카이브는 AppCDS 방식을 사용하며, 표준 라이브러리 클래스와 애플리케이션 클래스, 사용자 정의 클래스 로더 등의 메타데이터를 포함합니다.

  • `-Dspring.profiles.active=cds`: `cds` 프로파일을 활성화하여 CDS 아카이브 생성에 필요한 설정을 제공합니다. 데이터베이스 연결이 필요한 경우, 도커 컨테이너 내부에서 외부 환경에 접근할 수 있으므로, H2 Database와 같은 인메모리 데이터베이스를 이용해야 합니다.
  • `-Dspring.context.exit=onRefresh`: Spring 컨텍스트가 새로 고침(refresh)될 때 애플리케이션이 종료되도록 설정합니다. 이 과정에서 CDS 아카이브 파일이 생성됩니다.

 

H2 Database 설정 예시

`build.gradle`

implementation("com.h2database:h2")

`application-cds.yml`

spring:
  jpa:
    hibernate:
      ddl-auto: none
  datasource:
    url: jdbc:h2:mem:testdb
    username: sa
    password:
    driver-class-name: org.h2.Driver

 

2. 애플리케이션 실행

ENTRYPOINT ["java", "-XX:SharedArchiveFile=stempo-cds.jsa", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]

`-XX:SharedArchiveFile=stempo-cds.jsa` 옵션을 사용하여 애플리케이션 실행 시 이전 단계에서 생성된 CDS 아카이브 파일을 로드합니다.

 

4-3 AOT 적용

build.gradle

plugins {
    id("org.springframework.boot.aot") version ${springBootVersion}
}

tasks.named<BootJar>("bootJar") {
    dependsOn("processAot")

    layered {
        enabled = true
    }

    mainClass.set("com.stempo.ApiApplication")
}

tasks.named<ProcessAot>("processAot") {
    enabled = true
    dependsOn("classes")
}

tasks.named<ProcessTestAot>("processTestAot") {
    enabled = false
}

1. Spring AOT 플러그인 추가

plugins {
    id("org.springframework.boot.aot") version ${springBootVersion}
}

Spring AOT 플러그인을 추가하여 AOT 컴파일을 활성화합니다. 플러그인 버전을 Spring Boot 버전과 동일하게 설정하여 호환성을 보장합니다.

 

2. Gradle bootJar 태스크 정의

tasks.named<BootJar>("bootJar") {
    dependsOn("processAot")

    layered {
        enabled = true
    }

    mainClass.set("com.stempo.ApiApplication")
}

`dependsOn("processAot")`

`bootJar` 태스크 실행 전에 `processAot` 태스크가 실행되도록 설정합니다. 이를 통해 AOT 컴파일 과정을 선행한 후 JAR 파일을 생성하도록 합니다.

 

`layered { enabled = true }`

Layered JAR 기능을 활성화합니다. JAR 파일을 4개의 레이어로 나눠 Docker 빌드 프로세스에서 캐싱을 최적화합니다. 이는 뒤에서 다룰 Layered JAR 최적화에서 사용됩니다.

 

`mainClass.set("com.stempo.ApiApplication")`

애플리케이션의 메인 클래스를 설정하여 애플리케이션의 진입점을 명확히 합니다.

 

3. Gradle processAot 태스크 정의

tasks.named<ProcessAot>("processAot") {
    enabled = true
    dependsOn("classes")
}

`enabled = true`

`processAot` 태스크를 활성화합니다.

 

`dependsOn("classes")`

`processAot` 태스크 실행 전에 `classes` 태스크가 실행되도록 설정합니다. AOT 컴파일은 클래스 파일을 기반으로 수행되므로, 클래스 파일을 만드는 작업이 선행되어야 합니다.

 

4. Gradle processTestAot 태스크 정의

tasks.named<ProcessTestAot>("processTestAot") {
    enabled = false
}

`processTestAot` 태스크를 비활성화하여 테스트 과정에서 불필요한 AOT 컴파일 단계를 제거합니다.

 

Dockerfile

# 1. 빌드 단계
FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl AS build
WORKDIR /app

# Gradle Wrapper 복사 및 실행 권한 부여
COPY gradlew /app/
COPY gradle /app/gradle
RUN chmod +x ./gradlew

# Gradle 설정 파일 및 buildSrc 복사 후 의존성 설치
COPY build.gradle.kts settings.gradle gradle.properties /app/
COPY buildSrc /app/buildSrc
RUN ./gradlew dependencies --parallel --stacktrace

# 소스 코드 복사
COPY . /app

# 애플리케이션 빌드
RUN ./gradlew bootJar -Pspring.aot.enabled=true --no-daemon --build-cache --stacktrace


# 2. 런타임 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl
WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

# 포트 설정 및 애플리케이션 실행
EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.aot.enabled=true", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]

변경 사항

1. 애플리케이션 빌드

RUN ./gradlew bootJar -Pspring.aot.enabled=true --no-daemon --build-cache --stacktrace

`-Pspring.aot.enabled=true` 프로퍼티를 통해 Spring AOT 컴파일을 활성화하여 애플리케이션을 미리 컴파일합니다. 최적화된 클래스 데이터가 생성됩니다.

 

2. 애플리케이션 실행

ENTRYPOINT ["java", "-Dspring.aot.enabled=true", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]

`-Dspring.aot.enabled=true` 옵션을 사용하여 Spring AOT를 활성화합니다. 이를 통해 이전 단계에서 생성된 최적화된 클래스 데이터를 사용하여 애플리케이션을 실행합니다.

 

4-4. CDS + AOT 적용

CDS와 Spring AOT를 함께 적용하여 애플리케이션을 최적화합니다.

 

Dockerfile

# 1. 빌드 단계
FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl AS build
WORKDIR /app

# Gradle Wrapper 복사 및 실행 권한 부여
COPY gradlew /app/
COPY gradle /app/gradle
RUN chmod +x ./gradlew

# Gradle 설정 파일 및 buildSrc 복사 후 의존성 설치
COPY build.gradle.kts settings.gradle gradle.properties /app/
COPY buildSrc /app/buildSrc
RUN ./gradlew dependencies --parallel --stacktrace

# 소스 코드 복사
COPY . /app

# 애플리케이션 빌드
RUN ./gradlew bootJar -Pspring.aot.enabled=true --no-daemon --build-cache --stacktrace


# 2. CDS 아카이브 생성 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl AS cds
WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

# CDS 아카이브 생성
RUN java -XX:ArchiveClassesAtExit=./stempo-cds.jsa \
         -Dspring.aot.enabled=true \
         -Dspring.profiles.active=cds \
         -Dspring.context.exit=onRefresh \
         -jar stempo-api.jar


# 3. 런타임 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl
WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

# CDS 아카이브 파일 복사
COPY --from=cds /app/stempo-cds.jsa ./

# 포트 설정 및 애플리케이션 실행
EXPOSE 8080

ENTRYPOINT ["java", "-XX:SharedArchiveFile=stempo-cds.jsa", "-Dspring.aot.enabled=true", "-Dspring.profiles.active=prod", "-jar", "stempo-api.jar"]

 

4-5. CDS + AOT + Layered JAR 적용

Dockerfile

# 1. 빌드 단계
FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl AS build
WORKDIR /app

# Gradle Wrapper 복사 및 실행 권한 부여
COPY gradlew /app/
COPY gradle /app/gradle
RUN chmod +x ./gradlew

# Gradle 설정 파일 및 buildSrc 복사 후 의존성 설치
COPY build.gradle.kts settings.gradle gradle.properties /app/
COPY buildSrc /app/buildSrc
RUN ./gradlew dependencies --parallel --stacktrace

# 소스 코드 복사
COPY . /app

# 애플리케이션 빌드
RUN ./gradlew bootJar -Pspring.aot.enabled=true --no-daemon --build-cache --stacktrace


# 2. Layered JAR 추출 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl AS extract
WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=build /app/stempo-api/build/libs/stempo-api.jar /app/stempo-api.jar

# Layered JAR 추출
RUN java -Djarmode=layertools -jar /app/stempo-api.jar extract


# 3. CDS 아카이브 생성 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl AS cds
WORKDIR /app

# Layered JAR 복사
COPY --from=extract /app/dependencies/ ./
COPY --from=extract /app/spring-boot-loader/ ./
COPY --from=extract /app/snapshot-dependencies/ ./
COPY --from=extract /app/application/ ./

# CDS 아카이브 생성
RUN java -XX:ArchiveClassesAtExit=./stempo-cds.jsa \
         -Dspring.aot.enabled=true \
         -Dspring.profiles.active=cds \
         -Dspring.context.exit=onRefresh \
         org.springframework.boot.loader.launch.JarLauncher


# 4. 런타임 단계
FROM bellsoft/liberica-runtime-container:jre-21-cds-slim-musl
WORKDIR /app

# Layered JAR 복사
COPY --from=extract /app/dependencies/ ./
COPY --from=extract /app/spring-boot-loader/ ./
COPY --from=extract /app/snapshot-dependencies/ ./
COPY --from=extract /app/application/ ./

# CDS 아카이브 파일 복사
COPY --from=cds /app/stempo-cds.jsa ./

# 포트 설정 및 애플리케이션 실행
EXPOSE 8080
ENTRYPOINT ["java", "-XX:SharedArchiveFile=stempo-cds.jsa", "-Dspring.aot.enabled=true", "-Dspring.profiles.active=prod", "org.springframework.boot.loader.launch.JarLauncher"]

변경사항

1. Layered JAR 추출

RUN java -Djarmode=layertools -jar /app/stempo-api.jar extract

생성된 JAR 파일의 내용을 레이어별로 추출합니다. 작업 디렉토리(`/app`)에 레이어별 디렉토리가 생성됩니다.

  • `-Djarmode=layertools`: JAR 파일을 `layertools` 모드로 실행하여 내부 콘텐츠를 레이어별로 추출합니다.
  • `extract`: JAR 파일을 네 개의 레이어(`dependencies`, `spring-boot-loader`, `snapshot-dependencies`, `application`)로 나눕니다.

 

2. Layered JAR 복사

COPY --from=extract /app/dependencies/ ./
COPY --from=extract /app/spring-boot-loader/ ./
COPY --from=extract /app/snapshot-dependencies/ ./
COPY --from=extract /app/application/ ./

Layered JAR 추출 단계에서 생성된 4개의 레이어를 런타임 이미지에 복사합니다. 변경 가능성이 낮은 레이어부터 순서대로 복사하여 Docker 빌드 캐시를 효율적으로 활용합니다.

 

`dependencies` > `spring-boot-loader` > `snapshot-dependencies` > `application`

 

3. CDS 아카이브 생성 및 애플리케이션 실행

RUN java -XX:ArchiveClassesAtExit=./stempo-cds.jsa \
         -Dspring.aot.enabled=true \
         -Dspring.profiles.active=cds \
         -Dspring.context.exit=onRefresh \
         org.springframework.boot.loader.launch.JarLauncher

ENTRYPOINT ["java", "-XX:SharedArchiveFile=stempo-cds.jsa", "-Dspring.aot.enabled=true", "-Dspring.profiles.active=prod", "org.springframework.boot.loader.launch.JarLauncher"]

`org.springframework.boot.loader.launch.JarLauncher`를 이용하여 추출된 레이어를 기반으로 Spring Boot 애플리케이션을 실행합니다.

 

5. 성능 비교

애플리케이션 시작 속도

조건 시작 시간 (s) 개선율 (%)
기본 설정 23.176 0.00%
CDS 18.918 18.37%
AOT 20.033 13.56%
CDS + AOT 17.14 26.04%
CDS + AOT + Layered JAR 15.241 34.24%

 

 

메모리 사용량

조건 메모리 사용량 (MB) 개선율 (%)
기본 설정 321.8 0.00%
CDS 308.3 4.19%
AOT 310.9 3.39%
CDS + AOT 278.2 13.54%
CDS + AOT + Layered JAR 294.3 8.55%

 

 

6. 마치며

공식 문서에는 CDS와 AOT에 대한 설명이 간략히 나와 있어, 이틀 정도면 충분히 적용할 수 있을 것이라고 생각하며 작업을 시작했습니다. 하지만 이는 저의 오판이었습니다. 비교적 최근에 도입된 기능인만큼 자료가 부족했고, 적용 과정에서 예상치 못한 문제들이 발생하며 작업 시간이 다소 길어졌습니다. 작업 중 직면했던 주요 문제는 다음과 같습니다.

 

문제점

1. CDS 관련 문제

  • 빌드 단계와 런타임 단계에서 동일한 이미지를 사용했음에도 애플리케이션 실행 시 CDS가 적용되지 않음.
  • CDS 아카이브 생성 시 JAR 파일의 경로와 실행 시 경로가 일치하지 않아 CDS가 적용되지 않음.
  • 데이터베이스 연결 정보와 환경 변수를 알 수 없어 CDS 아카이브 생성에 실패함.

2. AOT 관련 문제

  • Main 메소드를 찾지 못함.
  • 일부 테스트 코드에서 AOT 컴파일 오류가 발생해 빌드에 실패함.
  • `processAot` 태스크 실행에서 컴파일이 되지 않음.

3. CDS와 Layered JAR 혼합 문제

  • JAR 파일을 기반으로 CDS 아카이브를 생성한 후, 추출한 레이어를 사용해 애플리케이션을 시작할 때 CDS가 정상적으로 적용되지 않음.

 

문제점들을 하나하나 해결해 나가는 과정이 쉽지는 않았지만 결과적으로 유의미한 개선이 이루어져 다행인 것 같습니다. 제가 도출한 결과물이 참고 자료가 되어 비슷한 문제를 겪는 분들에게 도움이 됐으면 좋겠습니다.

'Spring' 카테고리의 다른 글

다중 플랫폼을 지원하는 웹훅 시스템 설계하기  (1) 2025.01.06
N-gram과 유사도 측정으로 검색 정확도 높이기  (6) 2024.11.21
'Spring' 카테고리의 다른 글
  • 다중 플랫폼을 지원하는 웹훅 시스템 설계하기
  • N-gram과 유사도 측정으로 검색 정확도 높이기
limehee
limehee
https://github.com/limehee 개발 과정에서 얻은 경험과 지식을 누구나 쉽게 이해하고 적용할 수 있도록 알기 쉽게 전달합니다.
  • limehee
    기록하는 습관
    limehee
  • 전체
    오늘
    어제
    • 분류 전체보기 (5)
      • Spring (3)
      • DevOps (2)
      • DB (0)
      • OS (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    layered jar
    multi stage builds
    이벤트 기반 아키텍처
    ngram
    디자인 패턴
    JaCoCo
    checkstyle
    알림 시스템
    sonarqube
    캐싱
    CdS
    코사인
    AOT
    도커
    Github Actions
    스프링
    레이어드jar
    오블완
    파이프라인
    codecov
    형태소분석
    유사도
    최적화
    sonarcloud
    Eda
    CI/CD
    멀티스테이지빌드
    자카드
    웹훅
    티스토리챌린지
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
limehee
CDS와 AOT, Layered JAR로 Spring Boot 시작 시간, 메모리 최적화하기
상단으로

티스토리툴바