메트릭이 필요해진 이유
프로젝트를 운영하다 보면 보통 이렇게 흐르게 됩니다.
- 초반에는 콘솔 로그 + 파일 로그만 봐도 충분합니다.
- 어느 순간부터
- 느려졌다는 제보만 있고, 언제부터 / 어느 API가 / 어느 정도 느려졌는지 잘 안 보입니다.
- 500 에러가 터져도, 그게 1초간 3번인지, 10분간 수백 번인지 감이 안 옵니다.
- 부하 테스트를 해봐도 RPS 50까지는 괜찮고 80부터 깨지는지 숫자로 설명하기 어렵습니다.
로그는 개별 사건을 설명하는 데는 좋지만 서비스 전체의 상태 변화를 숫자로 보여주지는 못합니다.
그래서 메트릭 대시보드가 필요합니다.
1. 메트릭으로 무엇을 보고 싶은가? (로그 대시보드와의 차이)
먼저, 우리가 모니터링하고 싶은 대표적인 시각 자료부터 정리합니다.
1) HTTP 요청 수 (RPS)
- 예: 10초당 요청 20건 → 갑자기 200건
- 의미:
- 트래픽이 정상 범위인지
- 초당 몇 건에서부터 응답 시간이 깨지기 시작하는지
- 공격/디도스 같은 비정상 패턴이 있는지
2) 응답 시간 (P95, P99, 평균)
- 예: P95 응답시간이 평소 250ms → 갑자기 1200ms
- 의미:
- 대부분의 요청(95%)이 어느 정도 시간 안에 끝나는지
- 일부 꼬리 구간(P99)에 이상이 있는지
- 부하 테스트에서 “임계 RPS”를 찾는 기준
3) 에러율(Error Rate)
- 예: 500 에러가 0.2% → 5% → 30%
- 의미:
- 배포 이후 장애가 발생했는 것인지
- 특정 API만 에러율이 높은지
- 외부 연동(API, DB, AI 등) 구간에서 에러가 급증했는지
4) CPU / 메모리 / 디스크 / 네트워크
- CPU 99% 유지 → 특정 구간에서 CPU 바운드 가능성
- DB Connection Pool 20/20 사용 → 커넥션 대기 스레드 발생 가능성
- 메모리 사용량/GC Pause 증가 → 메모리 누수 / 튜닝 필요
5) API별 상세 성능
- /api/tests : 평균 200ms
- /api/tests/{id}/wait : 평균 900ms
어떤 엔드포인트가 실제로 병목인지 한 눈에 드러납니다.
전체적으로 메트릭 대시보드는
“개요 관제 + 이상 징후 감지 + 병목 위치 식별” 에 초점을 둔 층입니다.
로그는 “왜 터졌는지”를, 메트릭은 “언제부터 아팠는지”를 알려준다고 보면 됩니다.
2. 우리 서비스에서 모니터링할 핵심 지표 설계
이제 WebTest 구조에 맞춰서 어떤 메트릭이 필요한지를 정의했습니다.
2.1 HTTP 레벨 지표
| 지표 예시 | 메트릭 | 왜 보는가 |
| P95 / P99 응답시간 | http.server.requests | /api/tests/{id}/wait 같은 핵심 API의 체감 성능 붕괴 지점을 확인하기 위해 |
| 에러율 | http.server.requests에서 status != 2xx 비율 | 배포, 부하, 장애 상황에서 4xx/5xx 비중 추적 |
| RPS(초당 요청 수) | 요청 수 + 시간 윈도우 | “얼마나 들어오냐”와 “얼마부터 버거워하는지”를 확인 |
이 세 가지는 서비스가 아픈지, 언제부터 아픈지를 가장 잘 보여줍니다.
2.2 인프라/리소스 지표
| 지표 | Micrometer 메트릭 | 이유 |
| CPU 사용률 | system.cpu.usage, process.cpu.usage | CPU 90% 이상 고정 시 코드/쿼리 병목 의심 |
| 메모리/GC | jvm.memory.used, jvm.gc.pause | 메모리 누수, GC로 인한 응답 지연 감지 |
| DB 커넥션 풀 상태 | hikaricp.connections.* | 풀 여유가 0이면 DB 병목, 풀 크기 조정 필요 |
느려졌다는 증상이 인프라 한계 때문인지, 애플리케이션 코드/쿼리 때문인지 구분하기 위한 기본 지표입니다.
2.3 비즈니스 도메인 커스텀 지표
여기서 WebTest의 특성이 들어갑니다.
- 중복 요청 차단 횟수
- 메트릭: webtest.duplicate_requests
- 의미:
- 사용자/프론트/확장프로그램에서 같은 URL을 과도하게 재요청하는지
- 레이트 리밋 정책이 너무 빡센지/너무 느슨한지 튜닝 근거
- 파이프라인 단계별 CORE_READY / AI_READY 성공/실패
- webtest.pipeline.core_ready.success
- webtest.pipeline.core_ready.failure
- webtest.pipeline.ai_ready.success
- webtest.pipeline.ai_ready.failure
WebTest에서는 단순 “API 200”보다
테스트 파이프라인을 끝까지 마친 비율이 훨씬 중요합니다.
- CORE_READY까지는 잘 가지만, AI_READY에서 많이 실패하는지
- 높은 트래픽에서 CORE_READY 성공률이 떨어지는지
- 실제 사용자에게 AI 결과까지 도달한 비율이 얼마나 되는지
이런 것들을 수치로 볼 수 있습니다.
3. Actuator + Micrometer로 기본 계측 켜기
이제 실제 Spring Boot 프로젝트에 어떻게 적용했는지 단계별로 봅니다.
3.1 build.gradle 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Prometheus 연동용 (나중에 PLG 스택 사용할 때)
implementation 'io.micrometer:micrometer-registry-prometheus'
}
이 세 줄로 얻는 효과는 다음과 같습니다.
- spring-boot-starter-actuator
- /actuator/health, /actuator/metrics 같은 운영용 엔드포인트가 활성화됩니다.
- Spring이 자동으로 HTTP, JVM, HikariCP, Logback 등을 Micrometer로 계측하기 시작합니다.
- micrometer-registry-prometheus
- Micrometer가 수집한 메트릭을 /actuator/prometheus 형식으로 노출합니다.
- Prometheus + Grafana를 붙이면 바로 대시보드를 만들 수 있는 준비가 됩니다.
즉, 이 단계까지 하면 기본적인 HTTP/리소스 메트릭은 자동으로 수집되는 상태가 됩니다.
다음 단계는 어떤 엔드포인트를 노출할지 / 어떤 통계를 추가로 계산할지를 설정하는 부분입니다.
3.2 application.yml에서 Actuator/메트릭 설정
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
tags:
application: webtest
env: local
distribution:
# HTTP 요청에 대해 P95/P99 계산을 위한 히스토그램 옵션
percentiles:
http.server.requests: 0.5,0.9,0.95,0.99
percentiles-histogram:
http.server.requests: true
핵심 포인트 1: 어떤 엔드포인트를 외부에 노출할지
- exposure.include: health,info,metrics,prometheus
- /actuator/health → 헬스 체크
- /actuator/metrics → 메트릭 목록 + 개별 조회
- /actuator/prometheus → Prometheus 스크랩 엔드포인트
핵심 포인트 2: 공통 태그
metrics:
tags:
application: webtest
env: local
- 모든 메트릭에 application=webtest, env=local 태그가 붙습니다.
- 나중에 dev / staging / prod 환경을 나눌 때
env=prod 같은 태그로 필터링할 수 있습니다.
핵심 포인트 3: HTTP P95/P99 활성화
distribution:
percentiles:
http.server.requests: 0.5,0.9,0.95,0.99
percentiles-histogram:
http.server.requests: true
- http.server.requests 메트릭에 백분위수(P95, P99) 를 계산하는 옵션입니다.
- 이렇게 해야 Grafana 등에서
“/api/tests/{id}/wait의 P95가 1초를 넘는 순간”을 눈으로 볼 수 있습니다.
이 설정까지 적용하면
“HTTP 전반의 지연/에러/트래픽 상태” 를 수치로 관찰할 준비가 끝납니다.
4. MetricsConfig – 공통 태그/필터 설정
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags(
"application", "webtest",
"env", "local"
);
}
@Bean
public MeterFilter denyJvmThreadMetrics() {
// jvm.threads.* 메트릭은 너무 많아서 제외 (예시)
return MeterFilter.denyNameStartsWith("jvm.threads");
}
}
왜 이 Bean이 필요한가?
- 공통 태그(미래 대비)
- YAML에서도 태그를 넣었지만, 코드에서 추가로 공통 태그를 관리할 수 있습니다.
- 예를 들어:
- cluster=prod-k8s-01
- region=ap-northeast-2
- 같은 태그를 일괄로 붙이고 싶을 때 MeterRegistryCustomizer가 유용합니다.
- 불필요한 메트릭 제거
- jvm.threads.* 같은 메트릭은 종류가 많아서 대시보드를 지저분하게 만들 수 있습니다.
- 자주 보지 않을 메트릭은 MeterFilter로 과감하게 제거하는 편이 관리에 좋습니다.
메트릭은 “많으면 무조건 좋은 것”이 아니라
“보고 싶은 것만 선별해서 관리하기 편하게 만드는 것” 이 중요합니다.
5. 커스텀 메트릭 – 우리 도메인에 맞는 지표 붙이기
자동 계측만으로는 “우리 서비스만의 문제”를 보기 어렵습니다.
그래서 WebTest에 맞는 커스텀 메트릭을 붙였습니다.
5.1 중복 요청 차단 횟수 – DuplicateRequestMetrics
@Component
public class DuplicateRequestMetrics {
private final Counter duplicateRequestCounter;
public DuplicateRequestMetrics(MeterRegistry registry) {
this.duplicateRequestCounter = Counter.builder("webtest.duplicate_requests")
.description("중복 요청 차단 횟수")
.tag("type", "test")
.register(registry);
}
public void increment() {
duplicateRequestCounter.increment();
}
}
의도
- “사용자가 동일 URL에 대해 10초 내에 여러 번 요청하는 패턴”을 수치로 보고 싶습니다.
- 프론트/확장프로그램에서 필요 이상으로 재요청을 보내고 있는지 확인하려는 목적입니다.
- 나중에:
- type 태그로 구분(예: type=test, type=security_scan 등)
- 환경별/엔드포인트별 중복 요청 비율 분석 가능
사용 위치 – TestServiceImpl.createTest()
@Override
@Transactional
public TestResponse createTest(CreateTestRequest request) {
var normalizedKey = UrlNormalizer.normalizeUrlForKey(request.getUrl());
if (normalizedKey == null || normalizedKey.isBlank()) {
throw new InvalidRequestException("URL 정규화에 실패했습니다. 입력값을 확인하세요.");
}
var result = rateLimitService.checkAndMark(normalizedKey);
if (!result.allowed()) {
// 🔹 중복 요청 차단 메트릭
duplicateRequestMetrics.increment();
throw new DuplicateRequestException(
"동일 URL로 10초 내 중복 요청이 차단되었습니다. 남은 대기(ms): " + result.remainingMillis()
);
}
// 이하 TestEntity/LogicStatusEntity 생성 + securityVitalsService.scanAndSave...
}
- 레이트리밋에서 실제로 차단이 발생하는 지점에서 카운터를 증가시킵니다.
- 이렇게 하면 /actuator/metrics/webtest.duplicate_requests 로
“실서비스에서 레이트 리밋이 얼마나 자주 작동하는지”를 확인할 수 있습니다.
5.2 파이프라인 지표 – PipelineMetrics
@Component
public class PipelineMetrics {
private final Counter coreReadySuccess;
private final Counter coreReadyFailure;
private final Counter aiReadySuccess;
private final Counter aiReadyFailure;
public PipelineMetrics(MeterRegistry registry) {
this.coreReadySuccess = Counter.builder("webtest.pipeline.core_ready.success")
.description("CORE_READY 응답 성공 횟수")
.register(registry);
this.coreReadyFailure = Counter.builder("webtest.pipeline.core_ready.failure")
.description("CORE_READY 응답 실패/에러 횟수")
.register(registry);
this.aiReadySuccess = Counter.builder("webtest.pipeline.ai_ready.success")
.description("AI_READY 응답 성공 횟수")
.register(registry);
this.aiReadyFailure = Counter.builder("webtest.pipeline.ai_ready.failure")
.description("AI_READY 응답 실패/에러 횟수")
.register(registry);
}
public void incCoreReadySuccess() { coreReadySuccess.increment(); }
public void incCoreReadyFailure() { coreReadyFailure.increment(); }
public void incAiReadySuccess() { aiReadySuccess.increment(); }
public void incAiReadyFailure() { aiReadyFailure.increment(); }
}
의도
단순히 “요청이 200 OK로 끝났는지”보다 중요한 질문은 이것입니다.
- CORE_READY까지 무사히 도달한 테스트가 얼마나 되는지
- AI_READY까지 도달한 테스트가 얼마나 되는지
- 둘 중 어디에서 더 많이 떨어지는지
즉, 파이프라인 단계별 이탈률을 보는 지표입니다.
5.3 CORE_READY에 메트릭 붙이기 – LogicStatusServiceImpl
@Transactional
public void onPartialUpdate(UUID testId, Channel channel) {
// 1) 해당 채널 플래그 마킹
switch (channel) {
case WEB -> repo.markWebReceived(testId);
case SECURITY -> repo.markSecReceived(testId);
}
// 2) scoresMarked 조건 충족 시
boolean scoresMarked = markScoresReadyIfEligible(testId);
if (scoresMarked) {
scoresService.calcAndSave(testId);
TxAfterCommit.run(() -> {
try {
longPollingManager.complete(
new WaitKey(testId, LongPollingTopic.CORE_READY),
new PhaseReadyPayload(LongPollingTopic.CORE_READY, testId, Instant.now())
);
pipelineMetrics.incCoreReadySuccess();
} catch (Exception e) {
pipelineMetrics.incCoreReadyFailure();
log.warn("[METRICS] CORE_READY long-poll complete 실패 testId={}", testId, e);
}
});
}
// 3) aiMarked 조건 충족 시
boolean aiMarked = markAiTriggeredIfEligible(testId);
if (aiMarked) {
aiService.invokeAsync(testId);
TxAfterCommit.run(() -> {
try {
longPollingManager.complete(
new WaitKey(testId, LongPollingTopic.AI_READY),
new PhaseReadyPayload(LongPollingTopic.AI_READY, testId, Instant.now())
);
pipelineMetrics.incAiReadySuccess();
} catch (Exception e) {
pipelineMetrics.incAiReadyFailure();
log.warn("[METRICS] AI_READY long-poll complete 실패 testId={}", testId, e);
}
});
}
}
여기서 중요한 포인트
- TxAfterCommit 사용
TxAfterCommit.run(() -> {
...
});
- DB 트랜잭션이 정상 커밋된 이후에만 complete() + 메트릭 증가를 실행합니다.
- 즉, LogicStatus와 Scores, AI 결과가 실제로 DB에 반영된 뒤에만
“CORE_READY / AI_READY 성공”으로 간주합니다. - 롤백된 경우에는 afterCommit이 실행되지 않기 때문에
잘못된 성공 카운트가 올라가지 않습니다.
- 성공/실패 카운트를 동시에 관리
- long-poll 응답이나 알림 전송 과정에서 예외가 발생하면:
- 성공 카운트 대신 failure 카운트를 증가시킵니다.
- 나중에 success / (success + failure) 비율로
실제 사용자에게 이벤트가 어디까지 도달했는지를 볼 수 있습니다.
6. 1단계까지 정리하면 얻는 그림
지금까지 구성한 1단계 모니터링 단 결과는 다음과 같습니다.
6.1 기본 계측 (자동)
- http.server.requests:
- API별 응답시간(P95/P99)
- 에러율(4xx/5xx 비율)
- RPS
- system.cpu.*, jvm.memory.*, hikaricp.connections.*:
- CPU/메모리/DB 커넥션 풀 상태
6.2 커스텀 계측 (도메인)
- webtest.duplicate_requests:
- 중복 요청/레이트 리밋 작동 횟수
- webtest.pipeline.core_ready.success/failure
- webtest.pipeline.ai_ready.success/failure:
- 파이프라인 단계별 성공률
6.3 설정/구조
- build.gradle:
- Actuator + Micrometer + Prometheus 레지스트리
- application.yml:
- Actuator 엔드포인트 노출
- HTTP P95/P99 계산 활성화
- 공통 태그
- MetricsConfig:
- 공통 태그/불필요 메트릭 필터
- global.monitoring 패키지:
- 커스텀 메트릭 클래스(중복 요청, 파이프라인)
이 상태에 Prometheus + Grafana를 올리면:
- API 대시보드: Latency, Error Rate, RPS
- 인프라 대시보드: CPU, Memory, DB Pool
- 비즈니스 대시보드:
- 중복 요청 비율
- CORE_READY → AI_READY 전환율
까지 한 번에 볼 수 있는 관제 레이어가 만들어집니다.
이것이 “장애 대응을 위한 모니터링 단”의 1단계입니다.
이후 단계에서는 PLG(프로메테우스 + Loki + Grafana) 스택과 연동하고, 알람 룰(Alertmanager)까지 연결하면 장애 탐지 → 알림 → 원인 분석 흐름이 완성됩니다.
'프로젝트 > 웹 성능 테스트' 카테고리의 다른 글
| SSM 기반 무중단 CI/CD (0) | 2025.12.14 |
|---|---|
| 싸피 백엔드 과정: Spring Security (0) | 2025.11.30 |
| 롱폴링 비동기 완료 (0) | 2025.11.27 |
| 비동기 통신 방식 비교와 A/B 테스트 설계 (0) | 2025.11.09 |
| Nginx 도입 2편: 적용 (0) | 2025.11.09 |