Skip to content

chore: 성능 측정 스킬의 관측 수단 정비(#108) - #109

Merged
xunssoie merged 5 commits into
devfrom
chore/108-perf-skill-observability
Aug 30, 2026
Merged

chore: 성능 측정 스킬의 관측 수단 정비(#108)#109
xunssoie merged 5 commits into
devfrom
chore/108-perf-skill-observability

Conversation

@xunssoie

Copy link
Copy Markdown
Member

PR Summary

optimize-performance 스킬의 관측 수단을 정비합니다. 쿼리 출처 매핑을 서브에이전트 위임 없이 메인 컨텍스트에서 하고, k6 부하 중 JVM, 커넥션 풀, 캐시, Redis 상태를 주기 샘플링해 병목 판정 재료에 넣고, 실행계획 표에 노드별 실제 소요 ms를 계산해 보여줍니다. 겸해서 #104 이후 어긋난 perf 프로파일 템플릿을 없애고 src 파일 하나를 유일한 정의로 둡니다.


Problem

문제 1 - 쿼리 출처 매핑 위임이 시간과 토큰을 더 씀

쿼리 통계 가공본을 쓸 때 각 쿼리의 출처(리포지토리 메서드)를 찾는 일을 query-source-mapper 서브에이전트에 맡기고 있었습니다. Grep 흔적이 메인 컨텍스트에 남지 않게 하려는 의도였는데, 실제로는 워커가 템플릿, 기록 문서, k6 요약, 직전 가공본을 전부 다시 읽어야 해서 며칠 써 본 결과 오히려 느리고 토큰을 더 먹었습니다. 메인은 Phase 1에서 예상 쿼리 목록을 이미 확인한 상태라 그 정보를 버리고 처음부터 다시 찾는 구조였습니다.


문제 2 - 부하 중 JVM과 풀 상태를 보지 않음

actuator와 prometheus 레지스트리는 이미 있고 perf 프로파일에서 8081로 열려 있었지만, Phase 2의 히스토그램 점검에만 쓰고 측정 중에는 아무것도 수집하지 않았습니다. 그래서 heap, GC 일시정지, HikariCP 대기 같은 DB 밖 병목을 근거 없이 추정하게 되고, "쿼리는 빠른데 API가 느리다"는 상황에서 원인을 가를 재료가 없었습니다. Micrometer의 _max 계열은 최근 창에서 감쇠하는 값이라 측정이 끝난 뒤 한 번 긁는 방식으로는 아예 잡히지 않습니다.


문제 3 - 실행계획의 소요 시간을 직접 읽을 수 없음

MySQL EXPLAIN ANALYZEactual time=A..B는 loops당 평균이라 Nested Loop 안쪽 노드가 실제로 얼마나 걸렸는지 보려면 B x loops를 손으로 계산해야 합니다. Phase 6은 칼럼 설명만 붙이고 계산은 하지 않아, 판정할 때마다 같은 산수를 반복했습니다.


문제 4 - perf 프로파일 정의가 두 곳에 있어 어긋남

application-perf.yml이 스킬 템플릿과 src/main/resources 두 곳에 있었습니다. 템플릿은 src에 파일이 없던 시절 "없으면 생성"하려고 둔 것인데, #100에서 src 파일을 커밋한 뒤로는 그 경우가 없어졌는데도 남아 있었습니다. #104가 src에만 Redis와 캐시 설정을 넣으면서 둘이 어긋났고, 스킬 곳곳의 "애플리케이션 캐시 없음" 전제도 더는 사실이 아니게 됐습니다.


Solution

해결 1 - 출처 매핑을 메인이 직접, Grep은 예상 밖 쿼리에만

서브에이전트를 삭제하고 Phase 4, 8에서 메인이 가공본을 직접 씁니다. Phase 1에서 확인한 예상 쿼리 목록에 있는 쿼리는 Grep 없이 매핑하고, 새로 넣은 JVM 가공본의 리포지토리 메서드별 호출 증분이 digest의 호출 수와 맞아떨어지면 그것을 출처 확인으로 씁니다. Grep은 목록에도 표에도 없는 쿼리에만 하고, 못 찾으면 미상으로 둡니다.


해결 2 - /actuator/prometheus 주기 샘플링과 관측값 기준 구획

_shared/jvm-sampler.sh를 두고 k6 측정과 나란히 5초 간격으로 긁습니다. 게이지(heap, GC 최장 정지, blocked 스레드, HikariCP active/pending, 처리 중 요청 수, CPU)는 CSV 시계열로, 라벨이 가변인 카운터(리포지토리 메서드별, 캐시명별, Redis 명령별)는 측정 시작과 끝의 원문 두 장에서 증분으로 계산합니다. 새 지표군이 생겨도 칼럼을 늘릴 필요가 없는 구조입니다.

캐시와 Redis 구획은 설정이 아니라 관측값이 정합니다. 측정 구간에 cache_gets, lettuce 증분이 0이면 구획을 한 줄로 접고, 캐시 대상인데 hit 증분이 0이면 그 자체가 진단 근거가 됩니다. 스킬이 코드를 읽고 "이 대상은 캐시를 탄다"고 정하는 방식은 @CacheEvict처럼 간접적으로 닿는 경우를 놓치므로 택하지 않았습니다.

Tomcat 스레드 지표는 mbeanregistry 설정과 재기동이 필요해 이번엔 빼고, 서버 포화는 설정 없이 노출되는 http_server_requests_active(처리 중 요청 수)로 봅니다. 가공본은 스크립트가 완성하고 스킬은 읽기만 합니다.


해결 3 - 노드별 소요 ms 칼럼과 쿼리 전체 한 줄

Phase 6의 노드별 표에 소요 ms = B x loops 칼럼을 넣고, 표 위에 루트 노드의 B를 쿼리 전체로 적습니다. 원본 actual time 칼럼은 첫 행 도달 시간 정보를 위해 그대로 둡니다. 이 값은 결과 전송을 뺀 서버 실행 시간이라 digest의 mean_ms보다 작은 것이 정상이며, 둘의 차가 크면 응답 크기를 보라는 주의를 붙였습니다.


해결 4 - 템플릿 삭제, src 단일 정의

템플릿을 지우고 src/main/resources/application-perf.yml을 유일한 정의로 둡니다. 처음엔 둘을 동기화하고 Phase 2가 diff로 감시하는 안으로 구현했으나, 같은 정의를 두 곳에 두고 게이트로 드리프트를 막는 건 덧대기라 존재 이유가 없는 쪽을 없애는 것으로 바꿨습니다. Phase 2는 그 파일을 읽어 풀 크기, SQL 로깅, Redis 접속을 확인하고, 게이트에 Redis ping과 샘플러 지표 점검을 더했습니다. "캐시 없음" 전제는 "Redis 캐시는 워밍업이 채운다"로 바꿨습니다.


Related Issue

@xunssoie xunssoie self-assigned this Aug 30, 2026
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Test Results

290 tests   290 ✅  4s ⏱️
 93 suites    0 💤
 93 files      0 ❌

Results for commit 59d3205.

♻️ This comment has been updated with latest results.

@xunssoie
xunssoie merged commit b0d9564 into dev Aug 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore: 성능 측정 스킬의 관측 수단 정비

1 participant