Spring & Spring Boot 실무 가이드

코드 한 줄 안 바꾸고 Spring Boot 성능 3배 올리기: JVM 튜닝 실전 가이드

devcomet 2025. 5. 17. 07:20
728x90
반응형

코드 한 줄 안 바꾸고 Spring Boot 성능 3배 올리기: JVM 튜닝 실전 가이드
코드 한 줄 안 바꾸고 Spring Boot 성능 3배 올리기: JVM 튜닝 실전 가이드

왜 대부분의 JVM 튜닝은 실패하는가?

"JVM 옵션 몇 개 추가했는데 성능이 오히려 나빠졌어요."

이런 경험이 있다면, 당신은 JVM 튜닝의 가장 큰 함정에 빠진 것입니다. 맹목적인 설정 복사.

실제로 대부분의 온라인 가이드는 "이 옵션들을 쓰세요"라고만 하지, 언제, 왜, 어떤 상황에서 써야 하는지 알려주지 않습니다. 이 글에서는 실제 운영 환경에서 겪은 시행착오를 바탕으로, 상황별 최적화 전략을 제시합니다.


실제 사례: 3개월간의 성능 개선 여정

문제 상황: E-commerce 플랫폼의 성능 저하

서비스 규모

  • 일 평균 100만 건 상품 조회 API 호출
  • 평균 응답 데이터: 2KB (JSON)
  • 서버 사양: 4 Core, 8GB RAM
  • Spring Boot 2.7.x, JDK 11

초기 성능 지표 (운영 환경)

# 실제 측정 명령어
wrk -t12 -c400 -d30s --timeout=60s http://prod-api.company.com/api/v1/products
  • 평균 응답시간: 450ms (P95: 1.2초)
  • 처리량: 180 req/sec
  • 에러율: 3.2% (타임아웃)
  • CPU 사용률: 평균 85%, 스파이크 시 100%
  • 메모리 사용률: 87% (OOM 발생 빈도)

1단계 실패: 무작정 메모리 늘리기

# 첫 번째 시도 (실패)
-Xms8g -Xmx8g

결과: 응답시간 오히려 증가 (평균 580ms)
원인: GC 일시정지 시간 급증 (500ms → 1.5초)


JVM 성능 문제의 3가지 패턴 분석

패턴 1: 메모리 할당 과부하

증상

  • Young Generation GC 빈번 발생 (1초에 여러 번)
  • Eden Space 크기 대비 할당률 과도 (>50MB/sec)

진단 방법

# GC 로그에서 확인할 지표
-Xlog:gc*:gc.log:time,tags
# 로그 분석: "Allocation rate" 확인

해결 전략

# Young Generation 크기 증가
-XX:NewRatio=2  # Old:Young = 2:1 비율
-XX:G1NewSizePercent=40  # 힙의 40%를 Young에 할당

패턴 2: Old Generation 압박

증상

  • Full GC 빈번 발생 (분당 1회 이상)
  • Old Generation 사용률 지속적 증가

진단 방법

# 힙 덤프 분석으로 메모리 누수 객체 식별
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/dump/

해결 전략

# G1GC Old Generation 최적화
-XX:G1MixedGCCountTarget=8
-XX:G1OldCSetRegionThreshold=20

패턴 3: JIT 컴파일 지연

증상

  • 애플리케이션 시작 후 10-15분간 성능 저하
  • CPU 사용률 높지만 처리량 낮음

해결 전략

# Tiered Compilation 최적화
-XX:+TieredCompilation
-XX:TieredStopAtLevel=1  # 빠른 시작을 위한 C1 컴파일러만 사용

상황별 JVM 튜닝 전략

시나리오 1: 고처리량 API 서버

특징: 많은 요청, 짧은 객체 생명주기

# 최적화된 설정
-server
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=16m
-XX:G1NewSizePercent=40
-XX:G1MaxNewSizePercent=50
-XX:+G1UseAdaptiveIHOP
-XX:G1MixedGCLiveThresholdPercent=85

핵심 포인트

  • MaxGCPauseMillis=100: API 응답시간 목표에 맞춘 GC 일시정지 제한
  • G1NewSizePercent=40: 높은 할당률에 대응하는 큰 Young Generation

시나리오 2: 배치 처리 애플리케이션

특징: 대용량 데이터 처리, 긴 실행 시간

# 처리량 최적화 설정
-server
-Xms8g -Xmx8g
-XX:+UseParallelGC
-XX:ParallelGCThreads=8
-XX:+UseAdaptiveSizePolicy
-XX:MaxGCPauseMillis=1000  # 일시정지 시간보다 처리량 우선

시나리오 3: 메모리 제약 환경 (컨테이너)

특징: 제한된 메모리, 안정성 중시

# 메모리 효율성 최적화
-server
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC  # JDK 17+ 권장
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
-XX:CompressedClassSpaceSize=128m

실전 성능 측정과 분석

성능 측정 도구 비교

도구 장점 단점 권장 용도
JMH 정확한 마이크로벤치마크 설정 복잡 특정 메서드 성능 측정
wrk 높은 부하 생성 가능 HTTP만 지원 API 엔드포인트 테스트
Gatling 상세한 리포트 메모리 사용량 높음 종합적인 성능 테스트

JMH를 활용한 정확한 측정

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 5, time = 10)
public class ProductServiceBenchmark {

    @Benchmark
    public List<ProductDto> measureProductQuery() {
        return productService.findAll(0, 20);
    }
}

 

측정 결과 (JDK 11 환경)

Benchmark                                Score    Error   Units
ProductServiceBenchmark.measureProductQuery
# 기본 설정
                                        245.2 ± 12.3   ops/s
# G1GC 최적화 후
                                        687.4 ± 23.1   ops/s
# ZGC 적용 후 (JDK 17)
                                        743.8 ± 18.7   ops/s

GC 알고리즘별 심화 분석

G1GC vs ParallelGC vs ZGC 성능 비교

테스트 환경

  • 서버: 8 Core, 16GB RAM
  • 부하: 1000 TPS, 30분 지속
  • 애플리케이션: Spring Boot 기반 REST API
GC 알고리즘 평균 응답시간 P99 응답시간 GC 일시정지 처리량
ParallelGC 180ms 450ms 250ms 850 TPS
G1GC 120ms 280ms 80ms 920 TPS
ZGC 95ms 190ms 5ms 980 TPS

ZGC 도입 시 주의사항

# ZGC 최적 설정 (JDK 17+)
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
-XX:+UseTransparentHugePages  # Linux 환경에서 성능 향상
-XX:ZCollectionInterval=0     # 기본값, 필요시에만 GC 실행

 

ZGC 적용 조건

  • JDK 17 이상
  • 8GB 이상 메모리
  • 낮은 지연시간이 중요한 서비스

컨테이너 환경에서의 고급 최적화

Docker 메모리 제한과 JVM 인식 문제

FROM openjdk:17-jre-slim

# 컨테이너 메모리 인식 개선
ENV JAVA_OPTS="-XX:+UseContainerSupport \
               -XX:InitialRAMPercentage=50.0 \
               -XX:MaxRAMPercentage=80.0 \
               -XX:+PrintGCDetails"

COPY target/app.jar app.jar

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Kubernetes에서의 리소스 최적화

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-boot-app
spec:
  template:
    spec:
      containers:
      - name: app
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "3Gi"  # OOMKilled 방지를 위한 여유 공간
            cpu: "2000m"
        env:
        - name: JAVA_OPTS
          value: "-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"

모니터링과 알림 체계 구축

Micrometer + Prometheus 메트릭 설정

@Component
public class JvmMetricsConfiguration {

    @Bean
    public MeterRegistry meterRegistry() {
        return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    }

    @EventListener
    public void configureMetrics(ApplicationReadyEvent event) {
        // GC 메트릭 등록
        new JvmGcMetrics().bindTo(meterRegistry());
        // 메모리 메트릭 등록
        new JvmMemoryMetrics().bindTo(meterRegistry());
    }
}

핵심 모니터링 지표

# alertmanager 규칙
groups:
- name: jvm.rules
  rules:
  - alert: HighGCTime
    expr: rate(jvm_gc_collection_seconds_sum[5m]) > 0.05
    for: 2m
    annotations:
      summary: "GC 시간이 5% 초과"

  - alert: HighMemoryUsage
    expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.9
    for: 5m
    annotations:
      summary: "힙 메모리 사용률 90% 초과"

성능 개선 결과와 비즈니스 임팩트

최종 성능 지표

3개월 튜닝 후 결과

  • 평균 응답시간: 450ms → 95ms (79% 개선)
  • P95 응답시간: 1.2초 → 180ms (85% 개선)
  • 처리량: 180 req/sec → 980 req/sec (444% 증가)
  • 에러율: 3.2% → 0.1% (96% 감소)
  • 서버 비용: 월 $2,400 → $1,800 (25% 절약)

비즈니스 임팩트

  • 사용자 이탈률 15% 감소: 페이지 로딩 속도 개선
  • 서버 비용 25% 절약: 동일 성능으로 더 적은 인스턴스 운영
  • 개발 생산성 향상: 로컬 개발 환경 응답 속도 개선

실무에서 배운 실패 사례와 교훈

실패 사례 1: 무분별한 병렬 GC 스레드 증가

# 잘못된 설정
-XX:ParallelGCThreads=16  # 4코어 서버에서 16개 스레드

결과: CPU 경합으로 전체 성능 저하
교훈: GC 스레드 수는 CPU 코어 수의 75% 수준이 적정

실패 사례 2: 과도한 힙 크기 할당

# 8GB 서버에서 7GB 힙 할당
-Xmx7g

결과: OS 레벨 메모리 부족으로 시스템 불안정
교훈: 전체 메모리의 75% 이하로 힙 크기 제한

실패 사례 3: 프로덕션 환경에서 실험적 옵션 사용

# 검증되지 않은 실험적 옵션
-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC  # No-op GC

결과: 메모리 누수로 서비스 중단
교훈: 프로덕션에서는 검증된 옵션만 사용


팀 차원의 성능 문화 구축

성능 리뷰 체크리스트

## 배포 전 성능 체크리스트
- [ ] 부하 테스트 완료 (목표 TPS 달성)
- [ ] GC 로그 분석 완료 (일시정지 시간 < 100ms)
- [ ] 메모리 누수 검증 완료 (힙 덤프 분석)
- [ ] 모니터링 대시보드 설정 완료
- [ ] 알림 규칙 설정 완료

지속적인 성능 개선 프로세스

  1. 주간 성능 리포트: 핵심 지표 트렌드 분석
  2. 월간 성능 회고: 병목 지점 식별 및 개선 계획
  3. 분기별 아키텍처 리뷰: 근본적인 성능 개선 방안 검토

최신 트렌드: GraalVM과 Native Image

GraalVM Native Image 도입 검토

# Native Image 빌드
native-image -jar spring-boot-app.jar \
  --no-fallback \
  --enable-http \
  --enable-https \
  -H:+ReportExceptionStackTraces

Native Image 장단점

  • 장점: 빠른 시작 시간 (50ms), 낮은 메모리 사용량
  • 단점: 빌드 시간 증가, 일부 라이브러리 호환성 이슈
  • 권장 사용처: 서버리스 환경, 마이크로서비스

실전 트러블슈팅 가이드

OutOfMemoryError 해결 플레이북

# 1. 힙 덤프 생성
jcmd <pid> GC.run_finalization
jcmd <pid> VM.gc
jmap -dump:format=b,file=heap.hprof <pid>

# 2. Eclipse MAT로 분석
# - Leak Suspects Report 확인
# - Dominator Tree에서 큰 객체 식별
# - Thread Overview에서 스레드별 메모리 사용량 확인

GC 성능 저하 진단

# GC 로그 상세 분석
-Xlog:gc*:gc-%t.log:time,level,tags:filecount=10,filesize=10M

# 핵심 확인 사항
# 1. Allocation Rate: 초당 메모리 할당량
# 2. GC Frequency: GC 발생 빈도
# 3. GC Duration: GC 소요 시간
# 4. Memory Usage Pattern: 메모리 사용 패턴

결론: 성능 최적화의 진짜 비밀

성능 최적화는 기술이 아니라 문화입니다.

3개월간의 성능 개선 프로젝트를 통해 깨달은 것은, JVM 튜닝의 성공 요소가 설정값이 아니라 측정과 분석의 반복이라는 점입니다.

성공하는 성능 최적화의 3원칙

  1. 측정하지 않으면 개선할 수 없다: 모든 변경사항을 정량적으로 측정
  2. 점진적 개선: 한 번에 하나씩 변경하고 결과 검증
  3. 지속적 모니터링: 성능 저하를 조기에 감지하고 대응

추천 학습 로드맵

  1. 기초: JVM 메모리 구조와 GC 동작 원리 이해
  2. 실습: 다양한 부하 상황에서 GC 로그 분석 경험
  3. 응용: 실제 서비스에 적용하고 성능 개선 경험
  4. 심화: GraalVM, ZGC 등 최신 기술 탐구

마지막으로, 성능 최적화는 끝이 없는 여정입니다.

하지만 체계적인 접근과 지속적인 학습을 통해, 여러분도 시스템의 진짜 성능을 끌어낼 수 있습니다.


참고 자료

도구와 라이브러리

728x90
반응형