본문 바로가기

CS/JVM

[JVM] CRaC 완전 정복 (2편) - Spring Boot 실전 적용

시리즈 구성

  • 1편: 개념과 동작 원리
  • 2편: Spring Boot 실전 적용 ← 현재 글
  • 3편: JDK 배포판 비교와 실무 선택 가이드

 


들어가며

1편에서 CRaC의 개념과 동작 원리를 살펴봤습니다.

https://dmoritle.tistory.com/248

 

[JVM] CRaC 완전 정복 (1편) - 개념과 동작 원리

시리즈 구성1편: 개념과 동작 원리 ← 현재 글2편: Spring Boot 실전 적용3편: JDK 배포판 비교와 실무 선택 가이드 들어가며Java 애플리케이션은 왜 뜨는 데 오래 걸릴까요? Spring Boot 기반의 마이크로

dmoritle.tistory.com

 

이번 편에서는 Spring Boot 애플리케이션에 CRaC를 실제로 적용하는 방법을 다룹니다.

 

Spring Boot 3.2 / Spring Framework 6.1부터 CRaC를 공식 지원합니다. 프레임워크가 리소스 라이프사이클을 자동으로 관리해주기 때문에, 대부분의 경우 의존성 추가만으로 체크포인트 적용이 가능합니다.

 


1. Spring Boot CRaC 지원 구조

Spring이 자동으로 처리하는 것

1편에서 Resource 인터페이스를 직접 구현해야 한다고 설명했습니다. Spring Boot 3.2+에서는 이 작업을 Spring이 대신 처리합니다.

 

Spring의 Lifecycle 인터페이스가 CRaC의 beforeCheckpoint / afterRestore와 동일한 역할을 수행합니다.

 

 

Spring은 소켓, 파일 핸들, 스레드 풀 등 제한된 범위(limited scope) 내에서 라이프사이클을 자동으로 관리합니다. Spring에 의존하지 않는 라이브러리가 소켓이나 파일 핸들을 직접 열고 있다면 org.crac.Resource를 별도로 구현해야 할 수 있습니다.

 

의존성 추가

<!-- Maven -->
<dependency>
    <groupId>org.crac</groupId>
    <artifactId>crac</artifactId>
    <version>1.5.0</version>
</dependency>
// Gradle (Kotlin DSL)
implementation("org.crac:crac:1.5.0")

이 의존성 하나가 추가되면 Spring이 체크포인트 전후 소켓·커넥션 처리를 자동으로 조율합니다. Spring Framework 공식 문서 기준으로 org.crac 1.4.0 이상이면 지원됩니다.

참고: Spring Boot 3.2 이전 버전에서는 org.crac 의존성 없이 jcmd로 체크포인트를 시도하면 CheckpointOpenSocketException이 발생합니다. 이 의존성 추가가 해결책입니다.

 


2. 두 가지 체크포인트 모드

모드 1: On-demand 체크포인트 (수동)

실행 중인 애플리케이션에 jcmd로 체크포인트를 명령하는 방식입니다. 웜업 트래픽을 충분히 발생시킨 뒤 체크포인트를 생성할 수 있어 JIT가 최대로 웜업된 상태를 이미지에 담을 수 있습니다.

 

단, 웜업 중에는 DB, 캐시 등 실제 원격 서비스와의 커넥션이 열린 상태이므로, 체크포인트 생성 전에 이를 처리할 수 있는 인프라 환경(예: canary 배포 환경)이 전제됩니다.

# 1. 체크포인트 저장 경로 지정 후 앱 기동
java -XX:CRaCCheckpointTo=/cr -jar app.jar

# 2. 웜업 트래픽 발생 (실제 요청 처리)
curl http://localhost:8080/owners
curl http://localhost:8080/vets

# 3. 체크포인트 생성 (PID 또는 메인 JAR명으로 지정)
jcmd app.jar JDK.checkpoint
# → 앱 프로세스 종료 (정상 동작)

# 4. 이미지로부터 복원
java -XX:CRaCRestoreFrom=/cr

 

복원 후 로그:

Restarting Spring-managed lifecycle beans after JVM restore
Tomcat started on port 8080 (http) with context path ''
Spring-managed lifecycle restart completed (restored JVM running for 50 ms)

 

모드 2: Automatic 체크포인트 (자동)

JVM 시스템 프로퍼티 하나로 기동 중 특정 시점에 자동으로 체크포인트를 생성합니다. jcmd를 별도로 실행할 필요 없어 CI/CD 파이프라인 통합에 유리합니다.

java -Dspring.context.checkpoint=onRefresh \
     -XX:CRaCCheckpointTo=/cr \
     -jar app.jar
# → ApplicationContext refresh 완료 시점에 자동 체크포인트 후 종료

체크포인트가 생성되는 정확한 시점은 다음과 같습니다.

 

즉, 빈 초기화는 완료됐지만 Tomcat 포트 바인딩과 스케줄러 시작은 아직 이루어지지 않은 상태입니다. 초기화 비용만 제거하는 방식이므로, JIT 웜업 효과는 On-demand 방식보다 적습니다.

 

두 모드 비교

 

개발 환경에서 동작 검증하기

CRaC JDK가 없는 Mac/Windows 환경에서도 체크포인트 시점의 동작을 검증할 수 있는 방법이 두 가지 있습니다.

방법 1: spring.context.exit=onRefresh (Spring 제공)

 

체크포인트는 생성하지 않고, 해당 시점에 원격 서비스 커넥션이 발생하는지 확인합니다. CRaC JDK가 전혀 없어도 동작합니다.

java -Dspring.context.exit=onRefresh -jar app.jar
# → onRefresh 시점에 앱 종료 (체크포인트는 생성하지 않음)

 

방법 2: simengine (Azul Zulu 전용)

1편에서 설명한 simengine은 체크포인트 직후 동일 프로세스 내에서 즉시 복원까지 수행합니다.

 

Linux/Windows/macOS 모두 지원하며, beforeCheckpointafterRestore 콜백이 실제로 순서대로 호출되는지 확인할 수 있어 방법 1보다 더 정확한 검증이 가능합니다. Azul Zulu CRaC JDK가 있는 경우 활용합니다.

# Azul Zulu CRaC JDK 사용 시
java -XX:CRaCEngine=simengine \
     -XX:CRaCCheckpointTo=/cr \
     -jar app.jar
# → beforeCheckpoint → afterRestore 콜백 순서대로 실행 후 앱 계속 동작

 


3. 기동 시간 측정 방법

CRaC 적용 효과를 측정할 때는 측정 기준을 명확히 해야 합니다.

측정 기준 3가지

기준 측정 방법 특징
Spring 로그 파싱 Started in N seconds grep 빠르지만 복원 시 수치 왜곡 가능
복원 전용 로그 restored JVM running for N ms grep CRaC 복원 시 정확
HTTP 첫 응답까지 외부 폴링 wall-clock 가장 정확, 공식 벤치마크 표준

왜 Spring 로그는 왜곡되는가?

Started PetClinicApplication in 330.329 seconds처럼 비정상적으로 큰 수치가 나오는 경우가 있습니다. 이는 체크포인트 당시의 시작 시각을 기준으로 경과 시간을 계산하기 때문에, 체크포인트와 복원 사이의 대기 시간이 그대로 더해지기 때문입니다. Azul을 포함한 대부분의 공식 벤치마크는 HTTP 첫 응답까지의 wall-clock time을 기준으로 삼습니다.

 

측정 스크립트 예시

#!/bin/bash
# 기동 시간 측정: HTTP 첫 응답까지 wall-clock

START=$(date +%s%3N)

java -XX:CRaCRestoreFrom=/cr &

until curl -sf http://localhost:8080/actuator/health > /dev/null 2>&1; do
  sleep 0.005  # 5ms 간격 폴링
done

END=$(date +%s%3N)
echo "복원 완료까지: $((END - START)) ms"

 

반복 측정 및 통계

1회 측정은 노이즈가 많습니다. 최소 30~50회 반복 후 P95(95th percentile) 기준으로 비교하는 것이 일반적입니다.

측정 횟수: 50
최솟값:    43 ms
평균:      51 ms
P95:       67 ms   ← 이 수치를 기준으로 비교

 


4. 주요 주의사항

@Scheduled fixedRate 문제

On-demand 방식으로 체크포인트를 생성하면, 복원 시 체크포인트~복원 사이에 실행되지 못한 fixedRate 작업이 한꺼번에 몰려 실행됩니다. Spring Framework 공식 문서에서도 이를 명시적으로 경고하며, fixedDelay 또는 cron으로의 변경을 권장합니다.

// ❌ 위험: 체크포인트 중 누락된 실행이 복원 시 한꺼번에 수행됨
@Scheduled(fixedRate = 5000)
public void syncData() { ... }

// ✅ 안전: 이전 실행 완료 후 5초 대기
@Scheduled(fixedDelay = 5000)
public void syncData() { ... }

// ✅ 안전: cron은 실행 이후 시점 기준으로 다음 실행 계산
@Scheduled(cron = "*/5 * * * * *")
public void syncData() { ... }

 

Hikari allow-pool-suspension

체크포인트 시 다음 경고가 발생할 수 있습니다.

WARN: HikariDataSource is not configured to allow pool suspension.
      This will cause problems when the application is checkpointed.

 

application.properties에 다음을 추가합니다.

spring.datasource.hikari.allow-pool-suspension=true

 

체크포인트 이미지 보안

체크포인트 이미지는 체크포인트 시점 JVM이 처리한 모든 값의 스냅샷입니다. 환경변수, 설정 프로퍼티, API 키, DB 패스워드가 평문으로 파일에 저장될 수 있습니다.

 

컨테이너 이미지에 체크포인트 파일을 포함시킬 경우 특히 주의가 필요합니다.

✅ 권장: 체크포인트 시점에 시크릿을 아직 주입하지 않은 상태
✅ 권장: 이미지 파일 접근 권한을 엄격하게 제한
❌ 위험: DB 패스워드가 주입된 상태에서 퍼블릭 컨테이너 레지스트리에 이미지 push

 


5. Petclinic 기준 실습 흐름

환경

  • CRaC 지원 JDK (Azul Zulu 21 또는 BellSoft Liberica 21)
  • Spring Boot 3.2+ Petclinic
  • Linux (Ubuntu 24.10+의 경우 openjdk-21-crac-jdk-headless 패키지로 설치 가능)

 

전체 흐름 — CRIU 엔진 (기본)

# 1. CRaC JDK JAVA_HOME 설정
export JAVA_HOME=/path/to/crac-jdk

# 2. 빌드
./mvnw package -q

# 3. 체크포인트 경로 지정 후 기동 (최초 1회)
$JAVA_HOME/bin/java -XX:CRaCCheckpointTo=/cr -jar target/app.jar
# → Started PetClinicApplication in 4.657 seconds

# 4. (다른 터미널) 웜업 요청 발생
curl http://localhost:8080/owners
curl http://localhost:8080/vets

# 5. 체크포인트 생성
jcmd target/app.jar JDK.checkpoint
# → 프로세스 종료

# 6. 이후 실행: 이미지 복원
$JAVA_HOME/bin/java -XX:CRaCRestoreFrom=/cr
# → Spring-managed lifecycle restart completed (restored JVM running for 50 ms)

약 4.6초 → 50ms, 약 90배 개선입니다.

BellSoft 공식 튜토리얼(2024년 11월 기준)에서도 동일한 Petclinic 기준으로 5.2초 → 50ms, 약 100배 개선을 확인했습니다.

 

Warp 엔진 사용 시 (Azul Zulu 전용)

1편에서 설명한 것처럼 Warp 엔진은 추가 권한 없이 체크포인트를 생성할 수 있어 Docker 멀티스테이지 빌드에서도 사용 가능합니다. -XX:CRaCEngine=warp만 추가하면 됩니다.

$JAVA_HOME/bin/java \
  -XX:CRaCEngine=warp \
  -XX:CRaCCheckpointTo=/cr \
  -jar target/app.jar

 

체크포인트 이미지를 다른 머신(CPU 마이크로아키텍처가 다를 수 있는 컨테이너 환경 등)에서 복원할 계획이라면 -XX:CPUFeatures=generic 옵션을 함께 지정합니다.

$JAVA_HOME/bin/java \
  -XX:CRaCEngine=warp \
  -XX:CPUFeatures=generic \
  -XX:CRaCCheckpointTo=/cr \
  -jar target/app.jar

Warp 엔진은 복원 시 PID/TID가 변경됩니다. PID에 직접 의존하는 코드가 있다면 ResourceafterRestore에서 처리해야 합니다.

 


6. 프레임워크별 지원 현황

CRaC를 지원하는 프레임워크는 Spring Boot 외에도 있습니다.

프레임워크 지원 버전 특이사항
Spring Boot 3.2+ org.crac 의존성 추가만으로 동작
Quarkus 2.10.0+ 기본 내장 지원
Micronaut crac 피처 추가. Hikari·Redis 조율 내장
AWS Lambda SnapStart CRaC와 유사한 메커니즘. Lambda 전용

 


정리

항목 내용
지원 버전 Spring Boot 3.2+ / Spring Framework 6.1+
의존성 org.crac:crac:1.5.0 추가만으로 대부분 동작
체크포인트 모드 On-demand (jcmd) / Automatic (onRefresh)
측정 기준 HTTP 첫 응답까지 wall-clock이 가장 정확
@Scheduled 주의 fixedRatefixedDelay 또는 cron으로 변경
Hikari 설정 allow-pool-suspension=true 추가
보안 이미지에 시크릿 포함 위험 반드시 인지 필요

 

다음 편에서는 CRaC를 지원하는 JDK 배포판을 비교하고, 상황별 선택 가이드를 정리합니다.


참고 문서