CRaC란?
CRaC(Coordinated Restore at Checkpoint)은 실행 중인 JVM의 상태를 파일로 스냅샷하고, 이후 해당 스냅샷에서 즉시 복원하여 애플리케이션을 시작하는 기술입니다. Spring Boot 3.2부터 공식 지원되며, 기존 수십 초 걸리던 시작 시간을 수십 밀리초로 단축할 수 있습니다.
GraalVM Native Image와 달리 표준 JVM(OpenJDK) 위에서 동작하므로, 리플렉션이나 동적 프록시 같은 JVM 기능을 그대로 사용할 수 있습니다.
| 구분 | 일반 JVM | GraalVM Native | CRaC |
|---|---|---|---|
| 시작 시간 | 5~30초 | 50~200ms | 30~100ms |
| 빌드 시간 | 빠름 | 매우 느림 (수 분) | 빠름 + 체크포인트 |
| 런타임 성능 | 최적 (JIT) | 제한적 (AOT) | 최적 (JIT 유지) |
| 리플렉션 | 자유 | 설정 필요 | 자유 |
| 메모리 | 높음 | 낮음 | 높음 (스냅샷 크기) |
의존성과 JDK 설정
CRaC는 Azul Zulu, Liberica, 또는 CRaC 패치된 OpenJDK가 필요합니다. Spring Boot 3.2+에서는 별도 라이브러리 없이 프레임워크 레벨에서 지원합니다.
# build.gradle.kts
plugins {
id("org.springframework.boot") version "3.3.0"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
// CRaC API (Spring Boot에 포함, 명시적 추가 불필요)
// implementation("org.crac:crac:1.4.0")
}
# Dockerfile — Azul Zulu CRaC JDK 사용
FROM azul/zulu-openjdk:21-crac AS builder
WORKDIR /app
COPY build/libs/app.jar app.jar
체크포인트 생성 흐름
CRaC의 핵심은 워밍업 → 체크포인트 → 복원 3단계입니다.
# 1단계: 애플리케이션 시작 + 워밍업
java -XX:CRaCCheckpointTo=/checkpoint -jar app.jar &
# 워밍업 요청 (JIT 컴파일 유도)
curl http://localhost:8080/health
curl http://localhost:8080/api/warmup
# 2단계: 체크포인트 생성 (JVM 상태 스냅샷)
jcmd $(pgrep java) JDK.checkpoint
# 3단계: 스냅샷에서 복원 (수십 ms)
java -XX:CRaCRestoreFrom=/checkpoint
체크포인트 시점에 열려 있는 파일 디스크립터, 소켓, 스레드 등은 모두 정리되어야 합니다. Spring Boot 3.2+는 이 과정을 자동으로 처리합니다.
Spring Boot의 자동 리소스 관리
Spring Boot는 체크포인트 시점에 다음 리소스를 자동으로 닫고, 복원 시 재생성합니다.
- 웹 서버: Tomcat/Netty 서버 소켓 닫기 → 복원 시 재바인딩
- 데이터소스: HikariCP 커넥션 풀 닫기 → 복원 시 재생성
- JMS/Kafka: 메시지 리스너 컨테이너 중지 → 복원 시 재시작
- 로깅: 파일 핸들 닫기 → 복원 시 재오픈
// application.yml — CRaC 활성화
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
# CRaC 체크포인트 관련 설정
checkpoint:
restore: on-refresh # ApplicationContext 새로고침 시 자동 복원
커스텀 리소스 정리: Resource 인터페이스
직접 관리하는 리소스(파일, 외부 연결 등)가 있다면 org.crac.Resource 인터페이스를 구현하여 체크포인트/복원 콜백을 등록합니다.
@Component
public class ExternalConnectionManager implements Resource, InitializingBean {
private Connection externalConn;
@Override
public void afterPropertiesSet() {
// CRaC 글로벌 컨텍스트에 리소스 등록
Core.getGlobalContext().register(this);
this.externalConn = createConnection();
}
@Override
public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
// 체크포인트 전: 외부 연결 정리
if (externalConn != null) {
externalConn.close();
externalConn = null;
}
log.info("체크포인트 전 외부 연결 정리 완료");
}
@Override
public void afterRestore(Context<? extends Resource> context) throws Exception {
// 복원 후: 외부 연결 재생성
this.externalConn = createConnection();
log.info("복원 후 외부 연결 재생성 완료");
}
private Connection createConnection() {
// 외부 시스템 연결 로직
return ExternalClient.connect("ext-service:5555");
}
}
Docker 멀티 스테이지 빌드
프로덕션에서는 Docker 이미지 빌드 시 체크포인트를 미리 생성하여, 배포 시 복원만 하도록 구성합니다.
# Stage 1: 빌드
FROM azul/zulu-openjdk:21-crac AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar -x test
# Stage 2: 체크포인트 생성
FROM azul/zulu-openjdk:21-crac AS checkpoint
WORKDIR /app
COPY --from=builder /app/build/libs/app.jar app.jar
# CRIU 권한 필요
RUN java -XX:CRaCCheckpointTo=/checkpoint -jar app.jar &
sleep 10 &&
curl -s http://localhost:8080/actuator/health &&
jcmd $(pgrep java) JDK.checkpoint &&
sleep 2
# Stage 3: 복원 전용 이미지
FROM azul/zulu-openjdk:21-crac
WORKDIR /app
COPY --from=checkpoint /checkpoint /checkpoint
COPY --from=checkpoint /app/app.jar app.jar
ENTRYPOINT ["java", "-XX:CRaCRestoreFrom=/checkpoint"]
이 이미지로 컨테이너를 시작하면 수십 밀리초 만에 요청을 처리할 수 있습니다.
K8s 환경 적용
CRaC는 K8s HPA 오토스케일링과 결합하면 극적인 효과를 발휘합니다. 스케일 아웃 시 새 Pod가 밀리초 단위로 트래픽을 받을 수 있습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 2
template:
spec:
containers:
- name: app
image: order-service:crac
securityContext:
# CRIU 복원에 필요한 권한
capabilities:
add: ["CHECKPOINT_RESTORE", "SYS_PTRACE"]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 1 # CRaC 덕분에 1초면 충분
periodSeconds: 2
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"
시크릿 처리 주의사항
체크포인트 파일에는 JVM 메모리 전체가 포함되므로, 환경 변수나 시크릿이 스냅샷에 남을 수 있습니다. 이를 방지하는 전략이 필요합니다.
@Component
public class SecretCleaner implements Resource, InitializingBean {
@Value("${db.password}")
private String dbPassword;
private char[] sensitiveData;
@Override
public void afterPropertiesSet() {
Core.getGlobalContext().register(this);
this.sensitiveData = dbPassword.toCharArray();
}
@Override
public void beforeCheckpoint(Context<? extends Resource> context) {
// 체크포인트 전 민감 데이터 제거
Arrays.fill(sensitiveData, ' ');
sensitiveData = null;
log.info("민감 데이터 정리 완료");
}
@Override
public void afterRestore(Context<? extends Resource> context) {
// 복원 후 시크릿 재로드 (Vault, K8s Secret 등에서)
this.sensitiveData = loadFromVault("db/password").toCharArray();
log.info("시크릿 재로드 완료");
}
}
- 체크포인트 이미지는 신뢰할 수 있는 레지스트리에만 저장합니다
- 시크릿은 복원 시점에 Vault나 K8s Secret에서 동적으로 로드합니다
- 환경별 설정은 복원 후
@RefreshScope로 갱신합니다
Actuator 체크포인트 엔드포인트
Spring Boot Actuator를 통해 HTTP 요청으로 체크포인트를 트리거할 수도 있습니다.
// application.yml
management:
endpoint:
checkpoint:
enabled: true
endpoints:
web:
exposure:
include: health,checkpoint
# 체크포인트 트리거
curl -X POST http://localhost:8080/actuator/checkpoint
Spring Boot Lifecycle 통합
Lifecycle 인터페이스를 구현한 빈은 체크포인트 시 자동으로 stop()이 호출되고 복원 시 start()가 호출됩니다.
@Component
public class ScheduledTaskManager implements Lifecycle {
private volatile boolean running = false;
private ScheduledExecutorService scheduler;
@Override
public void start() {
scheduler = Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(this::syncData, 0, 30, TimeUnit.SECONDS);
running = true;
log.info("스케줄러 시작");
}
@Override
public void stop() {
if (scheduler != null) {
scheduler.shutdown();
}
running = false;
log.info("스케줄러 중지 (체크포인트 준비)");
}
@Override
public boolean isRunning() {
return running;
}
private void syncData() {
// 주기적 데이터 동기화 로직
}
}
성능 벤치마크
| 시나리오 | 일반 JVM | CRaC 복원 | 개선율 |
|---|---|---|---|
| 단순 Web App | 3.2초 | 45ms | 71× |
| JPA + Redis + Kafka | 12.5초 | 85ms | 147× |
| 대규모 모놀리스 | 28초 | 120ms | 233× |
제한사항과 실전 팁
- CRIU 권한: CRaC 내부적으로 CRIU를 사용하므로
CAP_CHECKPOINT_RESTORE권한이 필요합니다. Docker에서는--cap-add또는--privileged가 필요할 수 있습니다 - 체크포인트 크기: JVM 힙 전체가 저장되므로 이미지 크기가 클 수 있습니다. 불필요한 캐시를 체크포인트 전에 정리하세요
- 난수 재시드: 복원 후
SecureRandom이 같은 시드를 사용할 수 있으므로, 복원 콜백에서 재시드합니다 - 시간 기반 로직: 체크포인트와 복원 사이 시간 차이를 고려해야 합니다.
Instant.now()는 복원 후 정상 동작하지만, 캐시 TTL 등은 검증이 필요합니다 - JIT 캐시 유지: CRaC의 큰 장점은 JIT 컴파일 결과가 보존된다는 것입니다. 워밍업 후 체크포인트를 생성하면 복원 즉시 최적화된 코드로 실행됩니다
마무리
CRaC는 GraalVM Native Image의 제약 없이 JVM의 풍부한 생태계를 유지하면서도 밀리초 단위 시작을 달성하는 실용적인 기술입니다. Spring Boot 3.2+의 네이티브 지원 덕분에 기존 애플리케이션에 최소한의 변경으로 적용할 수 있으며, K8s HPA와 결합하면 서버리스에 준하는 탄력성을 확보할 수 있습니다.