인트로: 왜 JVM 튜닝을 배워야 할까?
"코드는 완벽한데 왜 이렇게 느릴까?"
많은 신입 개발자들이 이런 고민을 합니다. 알고리즘도 최적화했고, 캐싱도 적용했고, DB 인덱스도 생성했는데
여전히 성능이 만족스럽지 않다면?
해답은 JVM에 있을 수 있습니다.
실제 현업에서는 애플리케이션 코드뿐만 아니라 JVM 레벨의 최적화도 중요하게 다룹니다.
이 지식은 여러분을 다른 지원자들과 차별화시킬 수 있는 강력한 무기가 됩니다.
실무 사례: 전자상거래 플랫폼 성능 개선기
최근 제가 담당했던 전자상거래 플랫폼의 상품 조회 API는 다음과 같은 구조였습니다:
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@GetMapping
public ResponseEntity<List<ProductDto>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
// Hibernate를 통한 DB 조회
// DTO 변환 및 JSON 직렬화
return ResponseEntity.ok(productService.findAll(page, size));
}
}
초기 성능 측정
개발 환경에서는 문제없었지만, 스테이징 환경에서 부하 테스트를 진행하니 심각한 성능 이슈가 발견되었습니다:
# Apache Bench를 사용한 부하 테스트
ab -n 10000 -c 200 http://staging-api.company.com/api/v1/products
측정 결과
- 평균 응답시간: 380ms
- 처리량: 260 TPS (Transactions Per Second)
- 메모리 사용률: 85%
- GC 일시정지: 평균 300ms
JVM 메모리 구조 이해하기
최적화를 시작하기 전에 JVM 메모리 구조를 이해해야 합니다:
Heap 영역
- Young Generation: 새로 생성된 객체들이 할당되는 영역
- Eden Space: 객체가 최초로 생성되는 공간
- Survivor Space: Minor GC에서 살아남은 객체들이 이동
- Old Generation: 오래 살아남은 객체들이 이동하는 영역
Non-Heap 영역
- Metaspace: 클래스 메타데이터 저장
- Code Cache: JIT 컴파일된 네이티브 코드 저장
단계별 JVM 튜닝 전략
1단계: 힙 메모리 크기 설정
가장 기본적이면서도 중요한 설정입니다:
# 최소/최대 힙 크기를 동일하게 설정
-Xms4g -Xmx4g
왜 동일하게 설정하나요?
- 런타임 중 힙 크기 조정으로 인한 오버헤드 제거
- 예측 가능한 메모리 사용 패턴
- 컨테이너 환경에서 리소스 관리 용이
2단계: 가비지 컬렉터 선택
JDK 버전별 권장 GC:
- JDK 8: G1GC 또는 ParallelGC
- JDK 11+: G1GC (기본값)
- JDK 17+: ZGC 또는 Shenandoah (초저지연 필요시)
# G1GC 활성화 및 튜닝
-XX:+UseG1GC
-XX:G1HeapRegionSize=16m
-XX:MaxGCPauseMillis=200
3단계: 메타스페이스 최적화
Spring Boot는 런타임에 많은 클래스를 동적으로 생성합니다:
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
4단계: JIT 컴파일러 튜닝
# 계층형 컴파일 활성화
-XX:+TieredCompilation
# 컴파일 임계값 조정
-XX:CompileThreshold=2000
실전 적용: Spring Boot 애플리케이션 최적화
application.yml 설정
spring:
jpa:
properties:
hibernate:
# 배치 처리로 쿼리 최적화
jdbc:
batch_size: 25
batch_versioned_data: true
order_inserts: true
order_updates: true
JVM 옵션 통합
java -jar \
-server \
-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+ParallelRefProcEnabled \
-XX:+UseStringDeduplication \
-XX:+OptimizeStringConcat \
-Xlog:gc*:file=logs/gc.log:time,uptime:filecount=5,filesize=10M \
your-application.jar
모니터링과 진단
JVM 모니터링 도구 활용
- JConsole: 기본 제공 GUI 모니터링 도구
- VisualVM: 상세한 프로파일링 가능
- JMC (Java Mission Control): 프로덕션 레벨 진단
GC 로그 분석
# GC 로그 활성화
-Xlog:gc*:file=gc.log:time,tags:filecount=5,filesize=10M
로그 분석 도구:
- GCViewer
- GCeasy.io (온라인 분석)
- Eclipse MAT (Memory Analyzer)
성능 개선 결과
같은 부하 테스트로 측정한 개선 후 결과:
최종 성능 지표
- 평균 응답시간: 120ms (68% 개선)
- 처리량: 830 TPS (219% 증가)
- 메모리 사용률: 70%
- GC 일시정지: 평균 50ms
개선 효과 분석
- 응답시간 분포 개선
- P50: 100ms
- P95: 150ms
- P99: 200ms
- 시스템 리소스 효율성
- CPU 사용률 더 효율적
- 메모리 관리 최적화
- GC 오버헤드 감소
신입 개발자를 위한 취업 전략
이력서에 추가할 내용
## 프로젝트 경험
### Spring Boot 애플리케이션 성능 최적화
- JVM 튜닝을 통한 응답속도 300% 개선
- G1GC 적용 및 가비지 컬렉션 최적화
- 부하 테스트 기반 성능 측정 및 개선
- 기술스택: Spring Boot, JVM, Docker, K8s
면접 예상 질문과 답변
Q: JVM 튜닝 경험이 있나요?
A: "네, 실제 프로젝트에서 Spring Boot 애플리케이션의 성능을 3배 향상시킨 경험이 있습니다. G1GC를 적용하고 힙 크기를 최적화했으며, GC 로그 분석을 통해 지속적으로 모니터링했습니다."
포트폴리오 구성 팁
- Before/After 성능 그래프
- 적용한 JVM 플래그와 그 이유
- 문제 해결 과정 문서화
- GitHub에 설정 파일 공유
Docker와 Kubernetes 환경에서의 적용
Dockerfile 최적화
FROM openjdk:17-slim
# JVM 옵션을 환경변수로 관리
ENV JAVA_OPTS="-Xms2g -Xmx2g -XX:+UseG1GC"
COPY target/app.jar app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Kubernetes 리소스 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-app
spec:
template:
spec:
containers:
- name: app
image: your-app:latest
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
env:
- name: JAVA_OPTS
value: "-Xms2g -Xmx2g -XX:+UseG1GC"
트러블슈팅 가이드
자주 발생하는 문제들
- OutOfMemoryError
- 힙 덤프 분석:
-XX:+HeapDumpOnOutOfMemoryError
- 메모리 누수 확인: Eclipse MAT 활용
- 힙 덤프 분석:
- 긴 GC 일시정지
- GC 로그 분석으로 원인 파악
- Young Generation 크기 조정
- GC 알고리즘 변경 고려
- 느린 시작 시간
- 클래스 로딩 최적화:
-XX:+TraceClassLoading
- AOT 컴파일 고려 (GraalVM)
- 클래스 로딩 최적화:
마무리: 성능은 측정에서 시작된다
JVM 튜닝은 마법이 아닙니다. 체계적인 접근과 지속적인 모니터링이 핵심입니다:
- 현재 상태 측정
- 병목 지점 파악
- 단계별 최적화
- 결과 검증
- 지속적 모니터링
이 과정을 거치면 여러분도 코드 변경 없이 애플리케이션 성능을 극적으로 향상시킬 수 있습니다.
기억하세요:
좋은 개발자는 코드를 잘 짜지만, 훌륭한 개발자는 시스템 전체를 이해하고 최적화합니다.
'Spring & Spring Boot 실무 가이드' 카테고리의 다른 글
Spring에서 Bean Scope의 차이 – Singleton, Prototype, Request (0) | 2025.05.18 |
---|---|
REST API 예외 처리 패턴 – 글로벌 핸들러 vs 컨트롤러 별 처리 (0) | 2025.05.18 |
실전 코드로 배우는 Redis 캐싱 전략 - TTL, LRU, 캐시 무효화까지 (0) | 2025.05.12 |
Spring Boot에서 Excel 파일 업로드 & 다운로드 처리 – Apache POI 실전 가이드 (0) | 2025.05.10 |
[Java & Spring 실무] JPA Entity 간 N:1, 1:N 관계 설계 베스트 프랙티스 (0) | 2025.05.09 |