Spring Boot 배포 직후 첫 요청이 느린 이유를 JVM 동작 원리부터 설명합니다.
컴파일러와 인터프리터
컴파일러 언어 (C/C++)
- 전체 코드를 한번에 기계어로 변환
- 실행 속도 빠름
- 플랫폼 종속적
인터프리터 언어 (Python)
- 코드를 한 줄씩 해석하며 실행
- 실행 속도 느림
- 플랫폼 독립적
자바 동작 방식
자바 1990년대 설계 철학
- 이식성: Write Once, Run Anywhere
- 안전성: 메모리 보호, 타입 검증
- 개발 생산성: 자동 메모리 관리
HelloWorld.java (소스코드)
→ javac (컴파일)
→ HelloWorld.class (바이트코드)
→ java (JVM)
→ 실행
단점
바이트코드 인터프리터 실행으로 성능이 느렸습니다.
자바 성능 최적화 전략
JIT (Just In Time)
JIT는 자주 실행되는 코드만 네이티브로 컴파일합니다.
초기 실행: 바이트코드 → 인터프리터 → 실행 (느림)
반복 실행: 바이트코드 → JIT 컴파일 → 네이티브 코드 → 실행 (빠름)
Hot Spot
JIT는 자주 실행되는 코드만 최적화합니다.
판정 기준 (Java 17)
// C1 컴파일러
메서드 호출 200회 → C1 컴파일
// C2 컴파일러
메서드 호출 15,000회 → C2 컴파일
확인 방법:
java -XX:+PrintFlagsFinal -version | grep Threshold
Spring Boot 콜드 스타트
DispatcherServlet - 첫 HTTP 요청 시
- 첫 HTTP 요청 시 초기화
- 설정으로 변경 가능
spring.mvc.servlet.load-on-startup=1
HandlerMapping
- DispatcherServlet과 함께
ViewResolver
- DispatcherServlet과 함께
MessageConverter
- DispatcherServlet과 함께
HikariCP Pool
- 첫 DB 쿼리 시 초기화
- 설정으로 변경 가능
spring.datasource.hikari.initialization-fail-timeout=1
빈 외에 다른 클래스 파일들
- JVM 지연 클래스 로딩: 빈 생성 시 내부 클래스는 미로딩
웜업 전략
최소 웜업
- 대부분의 경우 충분
@Component
public class WarmupRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// DispatcherServlet 초기화
// spring.mvc.servlet.load-on-startup=1
restTemplate.getForObject("http://localhost:8080/actuator/health", String.class);
// DB 커넥션
// spring.datasource.hikari.initialization-fail-timeout=1
jdbcTemplate.execute("SELECT 1");
// 클래스 로딩
userService.findById(1L);
}
}
풀 웜업
- 트래픽 민감한 서비스
- 상황에 따라 C1 또는 C2 최적화 선택
@Component
public class FullWarmupRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// DispatcherServlet 초기화
// spring.mvc.servlet.load-on-startup=1
restTemplate.getForObject("http://localhost:8080/actuator/health", String.class);
// DB 커넥션
// spring.datasource.hikari.initialization-fail-timeout=1
jdbcTemplate.execute("SELECT 1");
// JIT 최적화
int warmupCount = 200; // C1: 200, C2: 15000
for(int i = 0; i < warmupCount; i++) {
userService.findById(1L);
productService.getPopular();
}
// 캐시
cacheService.preloadFrequentData();
}
}
웜업과 헬스체크
1. 애플리케이션 시작
↓
2. 웜업 실행
↓
3. 헬스체크 Ready
↓
4. 트래픽 수신
결론
대부분의 서비스는 최소 웜업(DB 커넥션 + 클래스 로딩)으로 충분합니다. 트래픽이 민감한 서비스만 JIT 최적화까지 진행하면 됩니다.