Sol Dev Blog

Expert notes on AI trends, frontend engineering, Spring backend architecture, and cloud operations.

2026-02-13 · 6 min read

Category: Spring Backend

Spring Boot 3에서 p95 지연시간을 잡는 실전 노하우

Spring Boot 3 애플리케이션에서 p95 지연시간을 줄이기 위한 비동기 처리, 스레드 풀 조정, JVM 튜닝, 그리고 메트릭 모니터링 방법을 실제 코드와 설정 예시를 통해 풀어봅니다.

"왜 p95 지연시간에 집착해야 할까?"

"우리 서비스, 평균 응답 시간은 괜찮은데 왜 고객 불만이 계속 나올까?" 이런 경험 한 번쯤 있죠? 평균 지연시간만 보면 전체 성능이 좋아 보이는데, 실제로는 몇몇 요청이 훨씬 느려서 사용자 경험을 망치는 경우가 많습니다. 그래서 요즘은 평균 대신 p95, p99 같은 퍼센타일 지연시간에 집중하는 추세예요. 특히 p95는 전체 요청 중 95%가 이 시간 이내에 처리된다는 뜻이라, 서비스 안정성과 직결되죠.

Spring Boot 3는 기본적으로 빠르지만, 대규모 트래픽 환경에서 p95 지연시간을 잡으려면 몇 가지 손봐야 할 부분이 있습니다. 이번 글에서는 제가 직접 겪고 적용해 본 실전 튜닝법들을 공유할게요.


Spring Boot 3에서 비동기 프로그래밍과 스레드 풀 설정이 p95에 미치는 영향

Spring Boot 3 공식 문서에서도 강조하는 부분인데요, 비동기 프로그래밍과 스레드 풀 조절은 p95 지연시간 개선의 핵심입니다Spring Boot Reference Documentation.

처음엔 "그냥 동기 방식으로 하면 간단하지 왜 복잡하게 비동기로 가냐" 싶지만, 실제로 요청이 몰릴 때 동기 처리 스레드가 꽉 차면 대기열이 길어지고, 그게 p95 지연시간을 끌어올립니다. 비동기로 돌리면 스레드가 블로킹되지 않고 다른 작업을 처리할 수 있어 병목이 줄어들죠.

실전 예제: @Async와 ThreadPoolTaskExecutor 설정

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(20); // 기본 스레드 수
        executor.setMaxPoolSize(50);  // 최대 스레드 수
        executor.setQueueCapacity(100); // 대기 큐 크기
        executor.setThreadNamePrefix("AsyncExecutor-");
        executor.initialize();
        return executor;
    }
}

@Service
public class MyService {

    @Async("taskExecutor")
    public CompletableFuture<String> processAsync() {
        // 비동기 처리할 작업
        return CompletableFuture.completedFuture("완료");
    }
}

이렇게 설정하면, 요청이 몰려도 최대 50개의 스레드가 동시에 작업을 처리하고 100개까지 대기열을 유지할 수 있습니다. 단, 너무 큰 스레드 풀은 오히려 컨텍스트 스위칭 비용 때문에 성능 저하를 유발하니, CPU 코어 수와 서비스 특성을 고려해 조절해야 해요.


JVM 튜닝과 G1 GC 최적화로 p95 지연시간을 잡아보자

Spring Boot 성능 튜닝에서 JVM 설정은 빼놓을 수 없습니다. 특히 GC가 자주 발생하거나 멈춤 시간이 길면 p95가 훅 올라가죠. Baeldung에서 소개한 것처럼 G1 GC를 잘 튜닝하면 지연시간 안정화에 큰 도움이 됩니다Baeldung - Spring Boot Performance Tuning.

JVM 옵션 예시

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1ReservePercent=20
-XX:ParallelGCThreads=8
  • MaxGCPauseMillis=200은 GC 멈춤 시간을 200ms 이내로 제한하려는 시도예요. 실제로는 100~300ms 사이에서 튜닝하면서 서비스 특성에 맞게 조절합니다.
  • InitiatingHeapOccupancyPercent=45는 G1 GC가 마크 사이클을 시작하는 힙 점유율 기준입니다. 너무 높으면 GC가 늦게 시작해 긴 멈춤이 발생할 수 있어요.

이 옵션들을 적용하고 나서 실제 서비스 p95 지연시간이 20~30% 줄어드는 걸 경험했습니다. 물론 JVM 튜닝은 서비스마다 다르니, 프로파일링 도구(예: VisualVM, JMC)로 꼭 확인하며 조절하세요.


Micrometer와 Actuator로 p95 지연시간 모니터링하기

"내가 튜닝한 게 효과가 있나?" 바로 확인할 수 있어야 다음 단계를 고민할 수 있죠. Spring Boot 3에서는 Actuator와 Micrometer를 기본으로 지원해서, p95 같은 퍼센타일 메트릭을 쉽게 수집할 수 있습니다Baeldung - Spring Boot Performance Tuning.

application.yml 설정 예시

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    distribution:
      percentiles:
        http.server.requests: 0.5,0.95,0.99
      percentiles-histogram:
        http.server.requests: true

위처럼 설정하면 HTTP 요청 지연시간의 p50, p95, p99 값을 수집하고, 히스토그램도 생성해서 분포를 시각화할 수 있어요.

Prometheus와 Grafana 연동

Actuator에서 Prometheus 포맷으로 메트릭을 노출하면, Prometheus 서버가 이를 스크랩하고 Grafana에서 대시보드를 만들어 실시간 p95 지연시간 추이를 볼 수 있습니다. 이걸 통해 어느 API가 병목인지, 특정 시간대에 지연이 급증하는지 바로 파악 가능하죠.


빈 초기화 최적화와 Lazy Loading으로 시작 시간과 런타임 성능 개선하기

Spring Framework 코어 문서에서도 언급하듯, 빈 초기화 시점과 방식을 잘 조절하면 애플리케이션 시작 시간뿐 아니라 런타임 성능에도 긍정적 영향을 줍니다Spring Framework - Core Technologies.

빈을 지연 초기화(Lazy)로 바꾸기

@Configuration
public class AppConfig {

    @Bean
    @Lazy
    public HeavyService heavyService() {
        return new HeavyService();
    }
}

@Lazy를 붙이면 실제로 해당 빈이 필요할 때까지 초기화를 미룹니다. 덕분에 애플리케이션 시작 시간이 빨라지고, 불필요한 리소스 낭비를 줄일 수 있죠. 특히 무거운 외부 연동이나 초기화 비용이 큰 빈에 효과적입니다.

주의할 점

Lazy Loading은 처음 호출 시 초기화 비용이 몰리기 때문에, 특정 요청에서 지연이 발생할 수 있습니다. 따라서 자주 쓰는 빈은 Lazy로 바꾸지 않는 게 좋고, 모듈별로 전략적으로 적용해야 해요.


캐시 전략과 비동기 이벤트 처리로 대규모 트래픽에서도 p95 지연시간 잡기

GitHub 엔지니어링 블로그에서 대규모 트래픽 환경에서 캐시와 비동기 이벤트 처리로 지연시간을 줄인 사례가 흥미로웠습니다GitHub Engineering Blog. 캐시 적중률을 높이고, 무거운 작업은 이벤트 큐에 넘겨 비동기로 처리하는 패턴이죠.

Spring Boot 3에서도 Redis, Caffeine 같은 캐시를 적극 활용하고, Spring Events나 메시지 큐(RabbitMQ, Kafka)로 비동기 작업을 분리하는 게 효과적입니다.

간단한 캐시 예시

@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#userId")
    public User getUserById(Long userId) {
        simulateSlowService();
        return userRepository.findById(userId).orElse(null);
    }

    private void simulateSlowService() {
        try {
            Thread.sleep(3000L); // 3초 지연 시뮬레이션
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }
}

위처럼 3초 걸리는 DB 조회를 캐시하면, 반복 요청 시 거의 즉시 응답할 수 있어 p95 지연시간이 크게 개선됩니다.


마무리하며: p95 지연시간 줄이기는 계속되는 여정

처음엔 p95 지연시간 최적화가 "복잡하고 귀찮은 작업"처럼 느껴질 수도 있습니다. 하지만 실제로 경험해 보면, 한두 가지 작은 조정이 전체 사용자 경험을 확 바꿔버리기도 하죠.

  • 비동기 처리와 스레드 풀 튜닝은 병목 구간을 줄여줍니다.
  • JVM과 GC 설정은 지연시간 안정화에 필수입니다.
  • Micrometer와 Actuator로 모니터링하며 문제 구간을 정확히 찾아냅니다.
  • 빈 초기화 전략과 캐시, 비동기 이벤트 처리로 전반적인 처리 속도를 높입니다.

무엇보다 중요한 건, "내 서비스의 상황에 맞게" 한 가지 방법만 맹목적으로 적용하지 않는 거예요. 항상 모니터링하고, 작은 변화를 측정하며, 점진적으로 개선해 나가는 게 핵심입니다.


참고 자료

운영에서 바로 점검할 항목 1

  • Spring Boot 3에서는 애플리케이션의 p95 지연시간을 줄이기 위해 비동기 프로그래밍과 적절한 스레드 풀 설정을 권장하며, 이를 통해 요청 처리 병목 현상을 완화할 수 있다. (Spring Boot Reference Documentation) 실제 적용에서는 트래픽 패턴, 장애 허용 범위, 팀의 온콜 역량을 같이 봐야 합니다. 초기에는 전체 전환보다 일부 기능에 먼저 도입하고, 지표가 안정화되는지 확인한 다음 확장하는 방식이 안전합니다. 특히 롤백 기준을 사전에 숫자로 정의해 두면 운영 중 의사결정 속도가 크게 좋아집니다.

  • Spring Boot에서 Actuator와 Micrometer를 활용해 애플리케이션의 성능 메트릭을 수집하고 분석함으로써 p95 지연시간을 모니터링하고 병목 구간을 식별할 수 있다. (Baeldung - Spring Boot Performance Tuning) 실제 적용에서는 트래픽 패턴, 장애 허용 범위, 팀의 온콜 역량을 같이 봐야 합니다. 초기에는 전체 전환보다 일부 기능에 먼저 도입하고, 지표가 안정화되는지 확인한 다음 확장하는 방식이 안전합니다. 특히 롤백 기준을 사전에 숫자로 정의해 두면 운영 중 의사결정 속도가 크게 좋아집니다.

  • JVM 튜닝과 GC 최적화는 Spring Boot 3 애플리케이션의 응답 시간 개선에 필수적이며, 특히 G1 GC를 적절히 조절하면 p95 지연시간 감소에 효과적이다. (Baeldung - Spring Boot Performance Tuning) 실제 적용에서는 트래픽 패턴, 장애 허용 범위, 팀의 온콜 역량을 같이 봐야 합니다. 초기에는 전체 전환보다 일부 기능에 먼저 도입하고, 지표가 안정화되는지 확인한 다음 확장하는 방식이 안전합니다. 특히 롤백 기준을 사전에 숫자로 정의해 두면 운영 중 의사결정 속도가 크게 좋아집니다.

  • Spring Framework 코어 레벨에서의 빈 초기화 최적화와 지연 로딩(Lazy Loading) 설정은 애플리케이션 시작 시간과 런타임 성능 모두에 긍정적인 영향을 미쳐 p95 지연시간 단축에 기여한다. (Spring Framework - Core Technologies) 실제 적용에서는 트래픽 패턴, 장애 허용 범위, 팀의 온콜 역량을 같이 봐야 합니다. 초기에는 전체 전환보다 일부 기능에 먼저 도입하고, 지표가 안정화되는지 확인한 다음 확장하는 방식이 안전합니다. 특히 롤백 기준을 사전에 숫자로 정의해 두면 운영 중 의사결정 속도가 크게 좋아집니다.

  • GitHub 엔지니어링 블로그에서는 대규모 트래픽 환경에서의 캐시 전략과 비동기 이벤트 처리 방식을 통해 지연시간을 효과적으로 줄인 사례를 소개하고 있다. (GitHub Engineering Blog) 실제 적용에서는 트래픽 패턴, 장애 허용 범위, 팀의 온콜 역량을 같이 봐야 합니다. 초기에는 전체 전환보다 일부 기능에 먼저 도입하고, 지표가 안정화되는지 확인한 다음 확장하는 방식이 안전합니다. 특히 롤백 기준을 사전에 숫자로 정의해 두면 운영 중 의사결정 속도가 크게 좋아집니다.

  • Cloudflare 블로그에서는 네트워크 레이어와 애플리케이션 레이어 모두에서 최적화 기법을 적용해 p95 지연시간을 줄인 경험을 공유하며, 특히 분산 캐시와 CDN 활용을 강조한다. (Cloudflare Blog - How We Built It) 실제 적용에서는 트래픽 패턴, 장애 허용 범위, 팀의 온콜 역량을 같이 봐야 합니다. 초기에는 전체 전환보다 일부 기능에 먼저 도입하고, 지표가 안정화되는지 확인한 다음 확장하는 방식이 안전합니다. 특히 롤백 기준을 사전에 숫자로 정의해 두면 운영 중 의사결정 속도가 크게 좋아집니다.

추가로, 배포 전에는 성능과 안정성뿐 아니라 로그 품질까지 확인해야 합니다. 에러 로그가 충분히 구조화되어 있지 않으면 원인 분석 시간이 길어지고, 같은 장애가 반복될 가능성이 높아집니다. 배포 후 24시간 관찰 구간에서 경보 임계치를 임시로 강화해 두는 것도 실무에서 자주 쓰는 방법입니다.

Comments

이 글에 대한 경험이나 의견을 남겨보세요.

댓글 기능을 활성화하려면 Giscus 환경변수를 설정하세요.

README의 Giscus 설정 섹션에서 5분 안에 연결할 수 있습니다.