코드 한 줄의 기록

Java 성능 프로파일링 시작하기: 샘플러와 애널라이저 완벽 가이드 본문

JAVA

Java 성능 프로파일링 시작하기: 샘플러와 애널라이저 완벽 가이드

CodeByJin 2025. 12. 14. 01:20
반응형

프로젝트를 진행하다 보면 누구나 한 번쯤 마주치는 상황이 있다. 어플리케이션이 예상보다 느리게 동작하거나, 서버 리소스를 과하게 사용하고 있다는 피드백을 받는 경우다. 이때 우리는 어디서 문제가 발생하는지, 왜 성능이 저하되는지 파악해야 한다. 바로 이 시점에서 프로파일링이 필요해진다.

프로파일링이란 무엇인가?

프로파일링은 실행 중인 어플리케이션의 성능 데이터를 수집하고 분석하는 과정이다. 단순히 추측으로 최적화를 시도하는 것이 아니라, 실제 데이터를 기반으로 성능 병목을 찾아내는 방식이다. Java 프로파일링을 통해 우리는 다음과 같은 정보들을 얻을 수 있다.

  • CPU 사용량과 어떤 메서드가 가장 많은 CPU 시간을 사용하는지
  • 메모리 할당 패턴과 메모리 누수 여부
  • 스레드 상태와 락 경합 상황
  • I/O 작업의 병목 지점

하지만 프로파일링을 제대로 이해하지 못하면 오히려 성능을 해치거나 부정확한 결과를 얻을 수 있다. 이 글에서는 Java 프로파일링의 핵심 개념부터 실전 도구 선택까지, 처음 시작하는 개발자가 알아야 할 모든 것을 다루겠다.

두 가지 프로파일링 방식: 샘플링 vs 계측

Java 프로파일링은 크게 두 가지 방식으로 나뉜다. 이 둘의 차이를 이해하는 것이 프로파일러 선택의 첫 번째 단계다.

 

샘플링 프로파일러 (Sampling Profiler)

샘플링 프로파일러는 일정한 시간 간격으로 JVM에게 모든 실행 중인 스레드의 스택 트레이스를 요청한다. 예를 들어 10ms마다 한 번씩 샘플을 수집한다면, 그 시점에 각 스레드가 어느 메서드를 실행 중이었는지 기록하는 방식이다.

 

샘플링의 장점

  • 오버헤드가 매우 낮다. 따라서 프로덕션 환경에서도 사용 가능하다
  • 지속적인 백그라운드 모니터링에 적합하다
  • 빠른 시간 내에 성능 개요를 파악할 수 있다

샘플링의 단점

  • 통계적 근사치이기 때문에 부정확할 수 있다
  • 매우 짧은 시간 내에 실행되는 메서드는 놓칠 수 있다
  • 샘플링 시점에 따라 결과가 편향될 수 있다 (특정 메서드가 실행 중일 때만 샘플이 수집되면 그 메서드의 시간을 과대평가하게 된다)

정확성을 높이려면 더 오래 프로파일링하거나 샘플링 간격을 줄이면 되지만, 간격을 줄일수록 오버헤드가 증가한다는 트레이드오프가 있다.

 

계측 프로파일러 (Instrumentation Profiler)

계측 프로파일러는 어플리케이션의 바이트코드를 수정해서 메서드 시작과 끝에 모니터링 코드를 삽입한다. 따라서 모든 메서드 호출을 정확하게 기록할 수 있다.

 

계측의 장점

  • 메서드 호출 횟수를 정확하게 파악할 수 있다
  • 메서드별 정확한 실행 시간 측정이 가능하다
  • 짧은 메서드도 놓치지 않는다

계측의 단점

  • 오버헤드가 상당하다. 모든 메서드에 추가 코드가 삽입되기 때문이다
  • 프로덕션 환경에 부적합하다
  • 바이트코드 변조로 인해 JIT 컴파일러의 최적화가 방해받을 수 있다

어떤 것을 선택할까?

일반적으로 개발 초기 단계에서 성능 병목을 찾아낼 때는 샘플링을 사용한다. 오버헤드가 낮으므로 실제 환경에 가까운 조건에서 테스트할 수 있기 때문이다. 반면 의심 구간이 명확하고 정확한 메서드 호출 횟수나 시간이 필요할 때는 계측을 사용한다.

 

최고의 방법은 먼저 샘플링으로 핫스팟을 찾은 후, 그 부분을 계측 프로파일러로 더 자세히 분석하는 것이다.

Java 프로파일링 도구들

Java 생태계에는 다양한 프로파일링 도구가 있다. 각각의 특성을 알아보자.

 

Java Flight Recorder (JFR)

JFR은 JDK에 기본으로 포함된 저오버헤드 프로파일링 도구다. Java 11부터는 상용 기능이 아니라 완전히 무료로 제공된다.

 

특징

  • JVM에 깊이 통합되어 있어 매우 효율적이다
  • 커맨드라인으로 간단하게 시작할 수 있다
  • 프로덕션 환경에서도 안전하게 사용 가능하다

기본 사용법

# 30초간 프로파일링 시작
jcmd <PID> JFR.start duration=30s filename=myapp.jfr

# 또는 어플리케이션 시작 시
java -XX:StartFlightRecording=duration=60s,filename=myapp.jfr MyApplication

# 백그라운드 연속 프로파일링 (1시간 최근 데이터만 유지)
java -XX:StartFlightRecording=maxage=1h,settings=profile,name=ContinuousRecording,dumponexit=true -jar MyApp.jar

JFR의 결과물은 .jfr 파일로 저장되며, Java Mission Control이나 다른 분석 도구로 확인할 수 있다.

 

Async Profiler

Async Profiler는 Amazon Corretto 팀에서 만든 오픈소스 샘플링 프로파일러다. JFR의 가장 큰 문제인 "세이프포인트 편향(Safepoint bias)" 문제를 해결했다.

 

세이프포인트란? JVM이 특정 작업을 수행할 때 모든 스레드가 안전한 상태에 도달하기를 기다리는 지점이다. JFR이 기존에 세이프포인트에서만 샘플을 수집했다면, 특정 메서드가 세이프포인트 근처에 있을 때만 샘플이 수집되는 편향이 발생했다.

Async Profiler는 HotSpot 특화 API를 사용해서 세이프포인트 바깥에서도 정확하게 스택을 수집할 수 있다.

 

사용 예

# CPU 프로파일링
asprof -e cpu -d 120 -o jfr -f asyncrec.jfr <PID>

# CPU와 메모리 할당 함께 프로파일링
asprof --all-user -e cpu,alloc -d 120 -o jfr -f asyncrec.jfr <PID>

# 락 경합 분석
asprof -e lock -d 30 -o jfr -f lockprof.jfr <PID>

Async Profiler는 JDK 6부터 지원하므로 레거시 시스템에서도 사용 가능하다는 장점이 있다.

 

VisualVM

VisualVM은 JDK에 포함된 GUI 기반 모니터링 및 프로파일링 도구다. 프로파일링 입문자에게 가장 진입장벽이 낮다.

 

특징

  • 실시간 CPU, 메모리, 스레드 모니터링
  • 힙 덤프 생성 및 분석
  • 원격 프로파일링 지원
  • 계측 기반 프로파일링과 샘플링을 모두 제공

시작하기

jvisualvm

실행 후 로컬 Java 프로세스가 자동으로 나타난다. 해당 프로세스를 더블클릭하면 상세 정보를 볼 수 있고, 프로파일러 탭에서 CPU 또는 메모리 프로파일링을 시작할 수 있다.

 

JProfiler와 YourKit

상용 도구들이다. IDE와 강력하게 통합되어 있고, 원격 프로파일링이 매우 편하다. 기업 환경에서는 이들이 많이 사용된다.

프로파일링 결과 이해하기: 플레임 그래프

프로파일러를 실행한 후 얻는 결과를 어떻게 해석할까? 가장 직관적인 시각화 방법이 플레임 그래프(Flame Graph)다.

 

플레임 그래프란?

플레임 그래프는 샘플링 데이터를 시각화한 것이다. X축은 시간의 흐름을, Y축은 콜스택의 깊이를 나타낸다. 가로 길이가 넓을수록 그 메서드가 더 많은 시간을 소비했다는 뜻이다.

 

그래프 해석하기

┌──────────────────────────────────────┐
│              main()                   │  ← 최상위 (전체)
├──────────────────────────┬────────────┤
│      processData()       │ readFile() │  ← 하위 호출
├────────┬─────────────────┤            │
│ query()│   transform()   │            │  ← 더 하위 호출
└────────┴─────────────────┴────────────┴────
        시간의 흐름 →

가로로 넓은 부분이 성능 병목지점이다. 예를 들어 100ms의 프로파일링 중에 query() 메서드의 가로 길이가 80ms라면, 전체 시간의 80%를 그 메서드가 차지한다는 뜻이다.

 

JFR 파일을 플레임 그래프로 변환하기

Async Profiler는 기본적으로 JFR 형식으로 저장할 수 있으며, 변환 도구를 사용하면 HTML 형태의 플레임 그래프를 생성할 수 있다.

java -cp ./async-profiler/lib/converter.jar FlameGraph output.jfr output.html

결과 HTML 파일을 브라우저에서 열면 마우스로 인터랙티브하게 탐색할 수 있다.

실전 프로파일링 시작하기

이제 실제로 프로파일링을 시작해보자.

 

1단계: 프로파일링 대상 선택

먼저 프로파일링할 시나리오를 정하자. 다음과 같은 상황들이 좋은 대상이다.

  • 사용자가 느리다고 호소하는 기능
  • 서버 CPU 사용량이 높은 시간대
  • 메모리 누수 의심 상황
  • 배포 후 갑자기 느려진 부분

정확한 진단을 위해 프로덕션과 유사한 부하 조건에서 테스트하는 것이 중요하다. 로컬 머신에서의 테스트는 대부분 성능 문제를 드러내지 못한다.

 

2단계: 저오버헤드 샘플러로 시작

첫 번째 단계에서는 항상 샘플링부터 시작하자. 프로덕션 환경이라면 JFR이나 Async Profiler를, 개발 환경이라면 VisualVM의 샘플러를 사용하면 된다.

# 프로덕션 환경: Async Profiler
asprof -e cpu -d 300 -o jfr -f cpu_profile.jfr <PID>

# 개발 환경: VisualVM 샘플러
# (GUI에서 Sampler 탭 선택 후 CPU 샘플링 시작)

샘플링 기간은 충분히 길어야 한다. 최소 1-2분, 가능하면 5분 이상이 좋다. 그래야 충분한 샘플이 수집되어 정확한 통계를 얻을 수 있다.

 

3단계: 결과 분석

샘플링이 완료되면 결과를 시각화하고 분석한다.

  • CPU 시간이 많은 메서드들을 찾자: 상위 10-20% 메서드에 집중
  • 의외의 메서드가 보이는가?: 예상했던 메서드가 아니라면 더 깊이 조사
  • 콜스택 패턴을 파악하자: 어떤 메서드가 다른 메서드를 호출하는가?

4단계: 의심 구간 정밀 분석

샘플링에서 찾은 의심 구간을 VisualVM의 프로파일러(계측)로 정밀하게 분석한다. 또는 Async Profiler로 특정 메서드만 필터링해서 다시 프로파일링한다.

# 특정 메서드를 최상단에 고정해서 분석
asprof -e cpu -d 60 --cstack dwarf -o jfr -f detailed.jfr <PID>

프로파일링할 때 피해야 할 실수들

프로파일링은 도구만 있다고 잘되는 게 아니다. 흔한 실수들을 알아두자.

 

1. 부하 없는 상태에서의 프로파일링

로컬 머신에서 유휴 상태의 어플리케이션을 프로파일링하면 비현실적인 결과를 얻는다. 성능 병목은 대부분 부하가 많을 때 드러난다. 항상 실제 트래픽 수준의 부하를 만들고 프로파일링하자.

 

2. 샘플링 간격을 너무 좁게 설정

오버헤드를 줄이기 위해 기본값(보통 10ms)을 그대로 사용하는 것이 낫다. 1ms 이하로 설정하면 오버헤드가 급증해서 결과가 왜곡될 수 있다.

 

3. 프로파일링 시간이 너무 짧음

30초 이하의 짧은 프로파일링은 통계적으로 의미 있는 샘플을 수집하기 어렵다. 최소 1-2분, 가능하면 5분 이상 수집하자.

 

4. 결과를 절대값으로 해석

프로파일링 결과는 상대적 비교다. "이 메서드가 50ms 걸렸다"는 정확하지 않지만, "전체 시간의 30%를 차지한다"는 의미 있는 정보다.

성능 개선으로 이어지기

프로파일링은 수단이지 목적이 아니다. 최종 목표는 성능 개선이다.

 

우선순위 정하기

Pareto 법칙을 따라 상위 20%의 병목이 전체 성능의 80%에 영향을 미친다.

  1. 가장 CPU 시간을 많이 사용하는 메서드부터 최적화
  2. 콜 트리에서 깊은 곳의 작은 메서드보다 상위의 주요 메서드 우선

최적화 검증

최적화 후에는 다시 프로파일링해서 개선 효과를 정량적으로 측정하자. 추측이 아닌 데이터로 증명해야 한다.

 

Java 성능 프로파일링은 처음엔 어렵게 느껴질 수 있지만, 핵심 개념을 이해하면 매우 강력한 도구가 된다. 샘플링과 계측의 차이를 알고, 상황에 맞는 도구를 선택하고, 결과를 올바르게 해석하는 것 - 이 세 가지만 기억하면 된다.

 

특히 중요한 점은 프로파일링은 일회성 활동이 아니라 지속적인 습관이어야 한다는 것이다. 큰 기능을 개발한 후, 배포 전, 성능 저하 신호가 보일 때마다 정기적으로 프로파일링을 하다 보면 어느 순간 성능 이슈의 원인을 빠르게 파악할 수 있게 된다.

 

처음에는 VisualVM으로 시작해서 기본기를 익히고, 실전에서는 JFR과 Async Profiler를 번갈아 가며 사용하면서 각 도구의 특성을 체득하길 바란다. 그리고 프로파일링 결과를 팀과 공유하고 논의하는 문화를 만들면, 성능 개선이 단순한 기술 활동에서 팀의 공동 책임이 되고, 그것이 바로 지속적으로 빠른 어플리케이션을 만드는 비결이다.

 

프로파일링을 통해 얻는 인사이트는 단순히 메서드 실행 시간을 아는 것을 넘어, 아키텍처 설계의 문제점을 발견하고, 알고리즘 선택의 영향을 정량적으로 파악하고, 리팩토링의 필요성을 증명할 수 있게 해준다. 이것이 프로파일링을 배우는 진정한 가치라고 생각한다.

 

 

Java GC 기초부터 로그 분석까지: 세대별 GC와 마크-스윕 완벽 이해하기

처음 Java를 배울 때 가장 신기한 부분 중 하나가 메모리 관리였다. C나 C++처럼 직접 free() 함수를 호출하거나 포인터를 관리할 필요가 없다는 게 정말 편했다. 하지만 실제 프로덕션 환경에서 고

byteandbit.tistory.com

 

반응형