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 최적화까지 진행하면 됩니다.