Spring Boot 시작 시간은 설정 목록을 한꺼번에 적용해서 줄이는 문제가 아니다. 먼저 어디까지를 시작 완료로 볼지 정의하고, 빈 생성·자동 설정·외부 연결·JVM 중 어느 구간이 느린지 측정해야 한다. 원인을 모른 채 lazy initialization이나 힙 옵션을 켜면 시작 비용을 첫 요청으로 옮기거나 장애 발견만 늦출 수 있다.
기존 글에 있던 JPA 연관관계의 지연 로딩, Tomcat 연결 타임아웃과 스레드 수는 일반적인 애플리케이션 시작 시간 개선책이 아니다. 아래에서는 시작 경로에 직접 영향을 주는 항목만 남겼다.
먼저 ‘시작 시간’을 같은 기준으로 잰다
프로세스가 생성된 시각, Spring ApplicationContext가 갱신된 시각, ApplicationRunner가 끝난 시각, readiness probe가 성공한 시각은 서로 다르다. Spring Boot는 application runner와 command-line runner가 호출된 뒤 애플리케이션을 ready 상태로 본다.
배포 관점에서는 다음 값을 따로 남기는 편이 좋다.
- 프로세스 시작부터 Spring 로그의
Started까지 - 프로세스 시작부터 readiness가 성공할 때까지
- 첫 실제 요청의 응답 시간
- 재시작이 아닌 깨끗한 프로세스의 cold start
간단한 비교는 동일한 JAR, JVM, CPU·메모리 제한과 외부 의존성 조건에서 반복한다.
/usr/bin/time -p java -jar app.jar
한 번의 최솟값으로 결론내리지 않는다. 여러 번 실행해 중앙값과 느린 구간을 비교하고, 변경 전후에 같은 조건을 유지한다.
Spring 내부 시작 단계를 기록한다
Spring의 ApplicationStartup은 빈 생성과 컨텍스트 수명주기의 시작 단계를 StartupStep으로 기록한다. BufferingApplicationStartup을 사용하면 Actuator의 startup 엔드포인트로 타임라인을 볼 수 있다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MyApplication.class);
app.setApplicationStartup(new BufferingApplicationStartup(2048));
app.run(args);
}
management.endpoints.web.exposure.include=health,startup
curl http://localhost:8080/actuator/startup
Actuator 엔드포인트는 애플리케이션 내부 구조를 드러낼 수 있다. 운영 인터넷에 그대로 노출하지 말고 인증과 네트워크 경계를 적용하거나 검증 환경에서만 연다.
JVM의 클래스 로딩·할당·GC까지 함께 보고 싶다면 Spring Framework가 제공하는 FlightRecorderApplicationStartup과 Java Flight Recorder를 후보로 삼을 수 있다.
의존성과 자동 설정을 먼저 줄인다
Spring Boot 자동 설정은 클래스패스와 정의된 빈, 설정값을 보고 필요한 구성을 선택한다. 사용하지 않는 starter나 라이브러리가 클래스패스에 남아 있으면 조건 평가와 초기화 대상이 늘 수 있다.
확인 순서는 다음과 같다.
- Maven
dependency:tree또는 Gradledependencies로 실제 클래스패스를 본다. - 사용하지 않는 starter와 중복 구현을 제거한다.
--debug로 condition evaluation report를 확인한다.- 필요 없다는 근거가 있는 자동 설정만 제외한다.
java -jar app.jar --debug
특정 자동 설정을 끌 때는 애플리케이션의 META-INF/spring.factories를 임의로 편집하는 방식이 아니다. 공식적으로 지원하는 annotation 또는 property를 사용한다.
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MyApplication {
}
spring.autoconfigure.exclude=fully.qualified.AutoConfigurationClass
위 예시는 데이터베이스를 전혀 사용하지 않는 애플리케이션일 때만 성립한다. 이름만 보고 제외하면 필요한 빈이 사라질 수 있으므로 condition report와 테스트로 확인해야 한다. 자동 설정 클래스의 패키지는 Spring Boot 주요 버전 사이에서 달라질 수 있어 사용하는 버전의 API를 따른다.
컴포넌트 스캔은 패키지 경계부터 확인한다
@SpringBootApplication이 있는 패키지는 기본 컴포넌트 스캔의 기준이 된다. 애플리케이션 클래스를 지나치게 상위 패키지에 두면 의도하지 않은 클래스까지 후보가 될 수 있다.
먼저 메인 클래스를 애플리케이션 루트 패키지에 두고 모듈 경계를 정리한다. 이후 실제 프로파일링에서 스캔이 병목일 때만 scanBasePackages나 명시적 @Import를 검토한다. 스캔 범위를 무작정 좁히면 빈 누락이 런타임 오류로 이어질 수 있다.
Lazy initialization은 비용을 없애지 않는다
spring.main.lazy-initialization=true
지연 초기화를 켜면 빈을 시작할 때 모두 만들지 않고 필요할 때 생성한다. 그래서 Started 로그까지의 시간은 줄 수 있다. 대신 다음 비용이 생긴다.
- 첫 요청에서 빈 생성 지연이 발생할 수 있다.
- 잘못된 빈 설정이 배포 시점이 아니라 실제 사용 시점에 드러날 수 있다.
- 시작 때 만들지 않았을 뿐 전체 메모리 요구량이 사라지는 것은 아니다.
따라서 cold start가 중요한 서버리스·개발 환경에서는 비교 후보가 될 수 있지만, readiness 이후 즉시 일정한 지연이 필요한 서비스라면 핵심 빈을 미리 초기화하거나 워밍업 전략을 함께 설계해야 한다. 시작 시간과 첫 요청 시간을 반드시 같이 잰다.
외부 연결과 마이그레이션을 분리해 본다
시작 중 데이터베이스 스키마 검증·마이그레이션, 시크릿 조회, DNS·TLS 연결, 원격 설정과 캐시 적재를 수행하면 Spring 내부보다 외부 대기가 더 클 수 있다.
이때 JPA 연관관계의 FetchType.LAZY를 바꾸는 것은 해결책이 아니다. 연관관계 fetch 전략은 엔티티 조회 시점을 결정하며, 애플리케이션 컨텍스트 시작 병목과는 다른 문제다.
다음 경계를 로그나 trace로 나눈다.
JVM 시작
→ Spring 컨텍스트 구성
→ DataSource·외부 클라이언트 생성
→ 스키마 마이그레이션
→ runner 실행
→ readiness 성공
외부 작업이 병목이라면 timeout을 줄이는 것만으로 정상 경로가 빨라지는 것은 아니다. 연결 재사용, 불필요한 시작 시 호출 제거, 마이그레이션 실행 주체 분리와 실패 정책을 서비스 요구사항에 맞춰 결정한다.
AOT와 네이티브 이미지는 마지막에 비교한다
Spring Boot는 빌드 시 AOT 처리를 수행한 뒤 생성된 초기화 코드를 JVM에서 사용하거나 GraalVM Native Image로 빌드할 수 있다. 시작 시간이 매우 중요한 환경에서는 큰 선택지가 될 수 있다.
다만 AOT는 클래스패스와 빈 구성을 빌드 시점에 고정한다. profile과 @ConditionalOnProperty처럼 런타임 조건으로 빈 구성을 바꾸는 기능에 제약이 생길 수 있다. 네이티브 이미지는 빌드 시간, 호환성, 디버깅과 운영 방식까지 달라진다.
그래서 적용 순서는 다음이 안전하다.
- JVM 애플리케이션의 실제 병목을 측정한다.
- 불필요한 의존성과 시작 작업을 제거한다.
- AOT 처리된 JVM JAR을 같은 조건에서 비교한다.
- 시작 시간과 메모리 절감이 빌드·운영 복잡도를 상쇄할 때 네이티브 이미지를 검토한다.
DevTools는 운영 cold start 최적화가 아니다
spring-boot-devtools의 restart는 변경되는 애플리케이션 클래스와 고정 라이브러리를 다른 classloader로 나눠 개발 중 재시작을 빠르게 한다. 완전히 패키징된 운영 애플리케이션에서는 기본적으로 비활성화된다.
즉 DevTools는 로컬 피드백 시간을 줄이는 도구다. 운영 배포의 cold start 측정 결과와 섞지 않는다.
우선순위 정리
| 관측 결과 | 먼저 할 일 | 주의점 |
|---|---|---|
| 특정 빈 생성이 오래 걸림 | ApplicationStartup으로 원인 빈 확인 |
원인을 lazy로 숨기지 않기 |
| 사용하지 않는 자동 설정이 많음 | 의존성 제거 후 필요한 설정만 exclude | condition report와 테스트 필요 |
| DB·원격 API에서 오래 대기 | 외부 초기화 경로와 실패 정책 점검 | timeout 축소만으로 해결되지 않음 |
| 로컬 재시작이 느림 | DevTools와 빌드 증분 확인 | 운영 시작 시간과 별도 지표 |
| 서버리스 cold start가 핵심 | lazy, AOT JVM, native를 순서대로 비교 | 첫 요청·빌드·호환성 함께 측정 |
Spring 시작 단계와 CPU 관계는 Spring 애플리케이션 부팅 과정, JVM 메모리 판단은 Spring 애플리케이션 메모리 할당 기준에서 이어서 볼 수 있다. 시작 시간 개선은 옵션 개수보다 측정 경계를 분명히 할 때 재현 가능해진다.
참고 자료
'배움과 성장 > 백엔드·데이터' 카테고리의 다른 글
| CAP 정리 정확히 읽기: 네트워크 분할 때 일관성과 가용성의 선택 (1) | 2024.12.04 |
|---|---|
| Geohash·Quadtree·H3 비교: 위치 검색에 맞는 공간 인덱스 고르기 (2) | 2024.12.03 |
| Spring Boot 메모리 설정 기준: Heap·RSS·컨테이너 limit 함께 보기 (2) | 2024.09.22 |
| Spring Boot 시작이 느릴 때: CPU 제한보다 먼저 측정할 것 (5) | 2024.09.22 |
| BigQuery 기본 구조와 비용 관리: dataset·partition·bytes processed (0) | 2024.08.16 |
댓글