공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 22 / 22

성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위

섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 측정 없이 추측 금지 — 성능 개선의 순서

성능 문제는 항상 4단계로 접근합니다. 순서를 건너뛰면 엉뚱한 곳을 고치게 됩니다.

단계 질문 도구
1. 측정 실제로 느린가, 얼마나 느린가 actuator, JFR, 로그
2. 위치 어느 구간이 느린가 구간 타이머, SQL 로그
3. 원인 왜 그 구간이 느린가 실행 계획, JFR 스택
4. 검증 고친 뒤 정말 나아졌는가 같은 측정 재실행

"체감상 느려진 것 같다"는 출발점일 뿐, 결론이 아닙니다. 반드시 숫자로 확인한 뒤에 코드를 고칩니다.

2.2 지표 정의 — 평균이 아니라 분위수

응답 시간을 평균 하나로 보면 위험합니다. 대부분 빠르고 일부만 아주 느려도 평균은 낮게 나와 문제를 감춥니다.

  • p50(중앙값): 절반의 요청이 이보다 빠릅니다. 체감 속도에 가깝습니다.
  • p95: 95% 가 이보다 빠릅니다. "가끔 느리다" 민원과 직결됩니다.
  • p99: 최악에 가까운 사용자 경험입니다. 배치·타임아웃 설계 기준입니다.
  • TPS: 초당 처리 건수입니다. 처리량이 병목인지 확인합니다.
  • 에러율: 느려지다 못해 타임아웃·예외로 실패하는 비율입니다.
  • 자원: CPU·메모리·DB 커넥션 사용량입니다. 원인 쪽 지표입니다.

민원은 보통 p95·p99 구간에서 발생합니다. 평균만 보고 "정상"이라 답하면 민원인의 경험과 어긋납니다.

2.3 측정 계층 4단계

병목은 SQL 부터 OS 까지 여러 층에 있을 수 있습니다. 위층부터 순서대로 내려가며 확인합니다.

  1. SQL 계층: 실행 계획(EXPLAIN PLAN)과 느린 쿼리 로그로 특정 쿼리를 찾습니다.
  2. 애플리케이션 계층: 구간 타이머와 MDC 로그로 요청 안의 어느 메서드가 느린지 찾습니다.
  3. JVM 계층: JFR 프로파일로 CPU·할당·락·GC 중 무엇이 원인인지 찾습니다.
  4. OS 계층: top·vmstat 으로 CPU·메모리·스왑 등 자원 고갈 여부를 확인합니다.

대부분의 실무 문제는 12단계에서 끝납니다. 34단계까지 내려가는 경우는 GC 튜닝이나 스레드 경합처럼 애플리케이션 코드만으로 설명 안 되는 상황입니다.

2.4 JFR — JDK 내장 프로파일러

JFR(Java Flight Recorder)은 JDK 에 내장된 프로파일러입니다. 별도 에이전트나 jar 설치 없이 바로 씁니다. 폐쇄망에서도 추가 반입 없이 쓸 수 있어 실무에 적합합니다.

text
# 애플리케이션 시작 시 60초간 기록
java -XX:StartFlightRecording=filename=app.jfr,duration=60s -jar app.jar

# 이미 떠 있는 프로세스에 원격으로 시작·덤프
jcmd <PID> JFR.start name=app duration=60s filename=app.jfr
jcmd <PID> JFR.dump name=app filename=app_snapshot.jfr

오버헤드는 보통 1~2% 로 작아 운영 중에도 켤 수 있습니다. 장애가 재현될 때까지 계속 기록해 두고, 문제가 생긴 시점의 스냅샷만 덤프하는 방식이 실무에서 흔합니다.

JMC(JDK Mission Control) GUI 없이도 jfr print 명령으로 텍스트만으로 분석할 수 있습니다. 폐쇄망 서버에 GUI 가 없어도 결과 파일만 반출하면 됩니다.

text
jfr print --events jdk.CPULoad app.jfr
text
jdk.CPULoad {
  startTime = 12:03:41.201
  jvmUser = 34.2%
  jvmSystem = 3.1%
  machineTotal = 41.0%
}

CPU 외에 jdk.ObjectAllocationSample(할당 핫스팟), jdk.JavaMonitorEnter(락 경합), jdk.GCPhasePause(GC 정지 시간) 이벤트를 함께 보면 CPU·메모리·락·GC 중 어느 쪽이 원인인지 좁힐 수 있습니다.

2.5 마이크로 벤치마크의 함정

System.nanoTime 으로 코드 앞뒤를 감싸 재는 방식은 간단하지만 세 가지 함정이 있습니다.

  • JIT 워밍업 부족: JVM 은 처음 몇 번은 인터프리터로, 이후 JIT 컴파일된 기계어로 실행합니다. 워밍업 없이 재면 "인터프리터 속도"를 잰 것입니다.
  • 죽은 코드 제거: 결과를 쓰지 않는 계산은 JIT 가 통째로 지워버릴 수 있어, 실제로는 실행되지 않은 코드를 잰 셈이 됩니다.
  • nanoTime 남용: 아주 짧은 코드(수 나노초)를 반복 측정하면 타이머 호출 자체의 오버헤드가 결과를 왜곡합니다.

이 레슨의 Bench 클래스는 워밍업 반복과 결과를 더하는 sink 필드로 앞의 두 함정을 최소한으로 막습니다. 다만 실제 운영 판단에는 JMH 같은 검증된 도구를 권장합니다.

2.6 JMH 문법 — 폐쇄망 반입 시 참고

JMH(Java Microbenchmark Harness)는 워밍업·죽은 코드 제거·통계 처리를 표준화한 벤치마크 도구입니다. Maven Central 접속이 막힌 폐쇄망에서는 jmh-core·jmh-generator-annprocess jar 를 사전에 반입해야 합니다.

java
@State(Scope.Thread)
public class StringBench {
    private String base = "x".repeat(100);

    @Benchmark
    public String concat() {
        return base + base; // 반환값이 있어야 죽은 코드 제거를 피한다
    }

    @Benchmark
    public void concatVoid(Blackhole bh) {
        bh.consume(base + base); // 반환 없는 메서드는 Blackhole 로 소비
    }
}
java
@Fork(2)                 // 별도 JVM 프로세스로 2회 반복 측정
@Warmup(iterations = 5)  // 워밍업 5회
@Measurement(iterations = 5)
public class BenchRunner {
    // Runner.run() 으로 실행하거나 커맨드라인 jar 로 실행
}

@State 는 벤치마크가 공유할 필드를 담고, Blackhole 은 반환값 없는 코드의 죽은 코드 제거를 막습니다. @Fork 는 JVM 을 통째로 새로 띄워 이전 측정의 JIT 상태가 섞이는 것을 막습니다. jar 없이는 이 문법을 실행할 수 없어, 이 레슨의 실행 데모는 Bench 로 대신합니다.

2.7 부하 테스트 — 도구 없을 때 자체 구현

ab·wrk·JMeter 가 폐쇄망 서버에 없다면 두 가지 대안이 있습니다.

  • curl 반복: for 문으로 curl 을 여러 번 실행합니다. 간단하지만 동시성 제어가 어렵습니다.
  • 자체 HttpClient: 스레드 풀로 동시 요청 수를 제어하고, 응답 시간을 모아 p50·p95·TPS 를 직접 계산합니다.

부하 테스트를 설계할 때 확인할 세 가지가 있습니다.

  1. 동시 사용자 수: 실제 운영 피크 시간대의 동시 접속자 수에 맞춥니다.
  2. 램프업: 한 번에 몰아서 쏘지 않고 서서히 늘려, 커넥션 풀 초기화 지연을 배제합니다.
  3. 목표 TPS: 커넥션 풀 크기가 상한입니다. 풀이 20개면 이론상 TPS 는 20 / 평균 응답 초 를 넘기 어렵습니다.

2.8 흔한 병목 순위

실무에서 마주치는 병목은 대략 다음 순서로 자주 나타납니다. 위쪽일수록 흔하고 고치기도 쉽습니다.

  1. N+1 쿼리: 목록 조회 후 각 항목마다 추가 쿼리를 날리는 패턴입니다.
  2. 인덱스 없음: WHERE 절 컬럼에 인덱스가 없어 풀스캔이 도는 경우입니다.
  3. 커넥션 풀 고갈: 동시 요청이 풀 크기를 넘어 대기 큐에 쌓이는 경우입니다.
  4. 동기 외부 호출: 외부 API 를 블로킹으로 호출해 스레드가 묶이는 경우입니다.
  5. 로그 과다: 매 요청마다 큰 객체를 로깅해 I/O 와 GC 압박을 늘리는 경우입니다.
  6. GC: 앞의 다섯 가지를 다 잡았는데도 남는, 마지막에 의심할 항목입니다.

GC 를 먼저 의심하고 힙을 늘리는 것은 흔한 실수입니다. 대개는 1~5번이 원인이고, GC 튜닝은 고급 10 레슨에서 다루는 마지막 수단입니다.

2.9 측정값 보고서 양식

병목을 고친 뒤에는 전·후 수치를 표로 남겨야 "느낌"이 아니라 근거로 보고할 수 있습니다.

지표 개선 전 개선 후
p50 320ms 45ms
p95 1,800ms 180ms
TPS 12 68
에러율 2.1% 0.0%
원인 N+1 쿼리 120회 fetch join 1회

이 표 하나가 "체감상 빨라졌다"보다 훨씬 설득력 있는 보고 자료가 됩니다. 개선 전 수치를 남겨두지 않으면 나중에 비교할 기준이 사라집니다.

2.10 Spring Boot 대응

Spring Boot 는 actuator 로 위 지표 상당수를 코드 추가 없이 노출합니다.

기능 설정·엔드포인트
요청 분위수 /actuator/metrics/http.server.requests
분포 히스토그램 아래 설정 키 참고
SQL 로그 p6spy 또는 MyBatis logging.level.mapper 패키지=DEBUG
JVM 지표 /actuator/metrics/jvm.memory.used 등
text
management.metrics.distribution.percentiles-histogram.http.server.requests=true

spring.jpa.show-sql=true 는 바인딩 파라미터를 보여주지 않아 실무 디버깅에 부족합니다. p6spy 나 MyBatis 로그 설정으로 실제 바인딩 값과 실행 시간을 함께 남기는 편이 낫습니다. JVM 내부 원리는 고급 10 레슨을, 서버 자원 관점의 장애 대응은 리눅스 운영 09 레슨을 함께 참고하세요.

핵심 원리
  • 2.1 측정 없이 추측 금지 — 성능 개선의 순서
  • 2.2 지표 정의 — 평균이 아니라 분위수
  • 2.3 측정 계층 4단계
  • 2.4 JFR — JDK 내장 프로파일러
  • 2.5 마이크로 벤치마크의 함정
  • 2.6 JMH 문법 — 폐쇄망 반입 시 참고
  • 2.7 부하 테스트 — 도구 없을 때 자체 구현
  • 2.8 흔한 병목 순위
  • 2.9 측정값 보고서 양식
  • 2.10 Spring Boot 대응
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제