이끌든지 따르든지 비키든지

Framework/Spring

[Spring] 애플리케이션의 초기 응답 속도 개선하기

SeongHo5 2024. 3. 7. 21:48

들어가기 앞서 - 왜 Spring에서 초기 요청의 응답이 지연될까?


 

Spring으로 개발을 하다 보면, 애플리케이션을 실행하고 초기 요청, 특히 첫 요청의 응답이 유난히 오래 걸린다는 점을 알 수 있습니다.

 

이러한 현상을 'Cold Start'라고 부르는데, 'Cold Start'의 주요 원인으로 추정할 수 있는 상황은 아래와 같습니다.

 

 

 

1. JIT 컴파일러의 작동 방식

 

Java는 'Write Once, Run Anywhere'를 지향하는 플랫폼 독립적인 언어입니다. 그래서, 일반적인 컴파일 언어와 동작 방식에서 약간의 차이가 있는데, Java 프로그램은 컴파일 시점에 바이트 코드로 변환되었다가, 실행 시점에 기계어로 변환되어 실행됩니다.

 

Java 컴파일 과정

 

 

이러한 방식은 프로그램의 이식성을 높이는 대신, 바이트 코드를 실시간으로 기계어로 변환하는 과정에서 상대적으로 실행 속도가 느려지는 단점이 있었고, 이 성능 저하를 최소화하기 위해 JIT(Just In Time) 컴파일러를 도입했습니다.

 

JIT 컴파일러는 런타임 중에 자주 실행되는 부분('HotSpot')을 식별하고, 이를 기계어로 컴파일해 실행 속도를 개선합니다.

 

그러나, 애플리케이션이 구동된 직후라면 이러한 과정이 충분히 진행되지 못해 초기 구동 동안 응답이 다소 지연될 수 있습니다.

 

 

2. JVM 클래스 로더의 Lazy Loading

 

JVM의 구성 요소 중 애플리케이션 실행 시 필요한 클래스를 메모리에 로드하는 작업을 하는 클래스 로더가 있습니다.

이 클래스 로더는 실행 시점에 모든 클래스를 메모리에 로드하지 않고, 필요한 시점까지 로딩을 지연하는 지연 로딩(Lazy Loading) 방식을 사용합니다.

 

애플리케이션이 구동된 직후라면, 대부분의 클래스들이 한 번도 사용되지 않았을 것이기 때문에 메모리에 적재된 클래스가 거의 없을 거고, 이 시점에 요청이 들어오게 되면 지연 로딩으로 인해 지연이 발생할 수 있습니다. 

 

3. 디스패처 서블릿(Dispatcher-Servlet) 초기화

 

Spring에서 HTTP 요청을 가장 먼저 처리하는 디스패처 서블릿도, Spring의 지연 초기화(Lazy-Init) 전략 때문에 애플리케이션 구동 시 초기화되지 않습니다.

 

첫 HTTP 요청 때, 디스패처 서블릿이 초기화되기 때문에, 이 점이 최초 요청에 대한 응답 지연에 영향을 줄 수 있습니다.

 

 

💡 디스패처 서블릿이란?

 

[Spring] Dispatcher-Servlet 알아보기

들어가기 앞서 - 서블릿(Servlet)이란? 동적 웹 페이지(Dynamic Web Page) 생성에 사용되는 서버 사이드 프로그램입니다. HTTP 프로토콜을 사용하며, 클라이언트의 요청에 대한 처리와 그 결과를 클라이

seongho-jo-5.tistory.com

 

 

 

개선할 방법?


 

초기 요청의 지연을 개선하기 위한 방법 중 하나는 애플리케이션이 구동된 후에 'Warm-Up'을 수행하는 것입니다.

 

'Warm-Up'은 애플리케이션의 핵심 부분을 미리 로딩하고 초기화하여, 초기 구동 동안 발생할 수 있는 지연을 줄이는 방법을 말합니다.

 

 

적용해 보기


 

Spring은 애플리케이션 구동 단계에서 ApplicationReadyEvent 등을 발행하기 때문에, 이를 활용해 WarmUp을 구현할 수 있습니다.

 

@Slf4j
@Component
@RequiredArgsConstructor
public class WarmUpRunner {

    public static final int WARM_UP_CALL_COUNT = 30;
    private static final RestTemplate restTemplate = new RestTemplate();
    
    @Value("${warmup.baseurl}")
    private String baseUrl;

    @Value("${warmup.id}")
    private String warmUpAuthId;

    @Value("${warmup.pw}")
    private String warmUpAuthPassword;

    @EventListener(ApplicationReadyEvent.class)
    public void warmUp() {
        logWarmUpStart();
        for (int i = 0; i < WARM_UP_CALL_COUNT; i++) {
            invokeAuthApi();
            invokeGoodsSearchApi();
        }
        logWarmUpEnd();
    }
    
    private void invokeGoodsSearchApi() {
        String url = baseUrl + "/goods";
        String onDiscountCondition = "?on_discount=true";
        String sortPriceDesc = "?sort=DESC";
        restTemplate.getForObject(url, GoodsInfo.class);
        restTemplate.getForObject(url + onDiscountCondition, GoodsInfo.class);
        restTemplate.getForObject(url + sortPriceDesc, GoodsInfo.class);
    } 
}

 

 

WarmUp 전 · 후를 비교하기 위해, 같은 API의 WarmUp 전 응답 속도를 측정했습니다.

 

 

WarmUp 전

 

 

전 · 후를 비교해보니, 초기 지연이 거의 없는 정도로 줄어든 것을 확인할 수 있었습니다.

 

WarmUp 후