Skip to content

v0.18.0 G04 — "저장이 몇 초 걸리고 지난 릴리스보다 느려졌는가" 를 아무도 답할 수 없던 것에서 진단 규격·조건이 적힌 workload·기준 SHA 실측·릴리스 예산까지 (2026-09-07)

  • 기간: 2026-09-07 ~ 2026-09-07 (세션 1개 08b11132, 오너 지시 "1284 진행해줘" → 분석·계획 게시 → "맞음 ㄱ")
  • 랜딩: PR #1315(Phase 1~5 한 PR, squash) — 마이그레이션·엣지 함수·화면 변경 없음. 앱 번들에 실리는 변경은 appErrorReporter.ts 의 릴리스 식별 한 함수(주입값 없으면 종전 동작)와 새 모듈 performanceEvidenceReport.ts(import 없음 → 트리셰이킹)·appPerformanceBudgets.ts 의 상수 추가뿐. 선행 G02 b0e5076a·G03 e94f0d34·스텝 1-3 D03 16b8e996 머지 확인 뒤 착수
  • 설계서: 없음 — 분석·Phase 계획은 이슈 #1284 댓글("예상 효과·개선사항" 절 포함)
  • 정본: docs/gates/performance-diagnostics.md(열쇠·채널 대응표·금지 필드·증거 schema v1) · docs/gates/performance-baseline.md(기준 SHA 16b8e996 실측·조건·미측정 표·재실행 절차) · docs/gates/performance-budgets.md(상한 18개·추세·회복·관찰 기간·RPO/RTO 평가법·변경 절차) · 코드 src/react/services/appPerformanceBudgets.ts RELEASE_PERFORMANCE_BUDGETS · src/react/services/performanceEvidenceReport.ts · src/react/services/appErrorReporter.ts resolveReleaseIdentity · 총괄 문서 2026-09-07-v0-18-0-architecture-roadmap.md §12 G04 행
  • 도구: scripts/performance/workload/{profiles,generate,load}.mjs(프로필 6종·결정적 생성·샌드박스 적재) · scripts/performance/measure/{db,browser,summarize}.mjs(읽기/쓰기 p95·큐 age·freshness·lock wait·WAL·backlog 소진 / cold·warm 초기 비용 / 표) · scripts/performance/probe/production.mjs(Management API 읽기 전용) · scripts/performance/lib/{stats,hygiene}.mjs · scripts/performance/evidence/schema.json(생성물은 .gitignore)
  • 게이트: 새 행동 테스트 tests/react/performance{EvidenceReport,Workload,Probes,BudgetsRelease}.test.mjs 22건(전부 npm test 자동 편입). 로컬 npm run check 전체 통과(정적 게이트 14·테스트 2,742). 마이그레이션·pgTAP·e2e 미접촉(측정용 샌드박스는 ci:local --only db 로 만들어 별도 사용)
  • 버그리포트: 없음(수리 건 아님 — 발견한 결함 3건은 §4 "작업 중 드러난 것" 과 HQ 갱신안으로)
  • 계약: 새 계약 문서 3(위). 기존 APP_SCREEN_PERFORMANCE_BUDGETS(CI mock 예산)·관측 로깅 문서·ADR 무변경. package.json·vite.config.mjs·workflow 무변경(SHA 주입 한 줄은 HQ 갱신안)

Phase 현황

Phase내용상태
Phase 0조사(클라 계측·서버 잡·백업·CI 예산 실태)·Production 읽기 전용 프로브 4회·계획 게시✅ 이슈 댓글
Phase 1진단 규격(열쇠 7·채널 11·금지 필드·증거 schema v1)·릴리스 SHA resolver·증거 조립/위생 모듈c96f4582
Phase 2workload 프로필 6종·결정적 생성기·샌드박스 적재기·조건 manifeste41fe1ea
Phase 3측정 러너 3·Production 프로브·증거 스키마·6프로필 실측·브라우저 초기 비용·기준선 문서0cb57677·e1244386
Phase 4릴리스 예산 문서·RELEASE_PERFORMANCE_BUDGETS·대조 테스트7e7b16d1
Phase 5장부 재생성·이 기록·등록 2곳·전체 검사·PR·인계·HQ 갱신안✅ PR #1315

1. 배경

v0.18.0 은 통계 단일 작성자(D01~D11)·저장 codec(S 계열)·앱 분리(A 계열)·UI 토큰(U 계열)을 동시에 진행한다. "사용자가 많아도 안정적" 이라는 목표를 나중에 증명하려면 바꾸기 전의 숫자와 허용 비용이 먼저 있어야 한다(총괄 F23·F29·C08). 기존 예산(appPerformanceBudgets.ts)은 바이트·행 상한이고 시간 예산 5개는 in-process mock 으로 재는 CPU 시간이라 "실제로 몇 초 걸리는가" 를 말하지 않았다(이슈 보존 규칙: 기존 RPC budget 을 계측 없는 성능 보장으로 인용하지 않는다).

2. 문제 제기

시간 측정이 세 곳에 따로 있고 잇는 열쇠가 없었다

브라우저 RPC 디버그(sessionStorage 120건, operationId)·read-model 메트릭(메모리 200건)·서버 잡 행·cron 이력이 각자 있고 서버로 올라가는 것은 오류 이벤트뿐. release 는 청크 파일명(30일 58종)이라 커밋과 잇지 못했다.

잡 처리 시간이 항상 0 이었다

관측 뷰가 processed_at − processing_started_at 을 빼는데 두 값이 같은 트랜잭션의 now(). Production 30일 1,362건 전부 0, 샌드박스도 같다.

부하 조건을 적어 둔 곳이 없었다

10년 fixture 는 메모리 전용, 여러 owner·같은 owner 여러 기기·인입 혼합 없음, DB 사양·요청률·worker 한도 기록 없음.

매분 cron 이 20일 동안 WAL 46GB 를 쓰고 있었다(발견)

run_user_exercise_stats_refresh_cron 27,966회·평균 302ms·최대 66초, 전체 WAL 53GB 의 85%. 투영 테이블은 살아 있는 행 1만 개에 갱신 1,467만·삭제 669만 회. 사용자 5명 규모에서.

3. 해결 방안

원칙 (오너 결정 없음 — 전제로 진행, 2026-09-07)

  • 전제 1: 이 이슈는 기준선과 측정 수단만 만든다. 발견 사항은 담당(D08/D11/HQ)에 넘긴다. 숫자를 보고 상한을 넓히지 않는다.
  • 전제 2: 새 SQL 객체·마이그레이션 없음. 서버 집계는 스크립트가 기존 표를 읽는다. Production 은 읽기 전용.
  • 전제 3: SHA 주입·새 이벤트 kind·DB 설정은 HQ 갱신안으로. 이 PR 은 resolver 만.
  • 전제 4: fixture 는 G02 도구 위(결정적 uuid·고정 종료일·payload v5). 조건은 workload.json 에 같이.
  • 전제 5: 기준 측정은 전용 로컬 샌드박스(같은 Postgres 이미지)·브라우저는 미리보기 빌드. 미측정은 미측정으로.
  • 전제 6: 상한 = 측정값 × 배율, R04 전에 고정. RPO/RTO 는 평가 방법만.
  • 전제 7: CI 1회.

접근

내용채택
A. 규격 + 결정적 fixture + 러너 + 예산 표열쇠·금지 필드를 규격으로, 조건이 적힌 workload 를 생성기로, 측정을 러너로, 상한을 기준선 × 배율로채택 — 같은 절차를 R04 가 그대로 돌려 전후 비교
B. 새 계측을 켜고(APM·OpenTelemetry) 서버로 올린다정상 경로 시간을 전부 수집불채택(보류) — 측정 경로 비용·새 의존성·Free 플랜 밖 서비스. 기존 오류 채널에 열쇠만 맞추고 정상 경로 증거는 러너가 파일로
C. Production 에 부하를 걸어 잰다실제 인프라 숫자불채택 — 이슈가 production 변경 승인을 포함하지 않고, 5명 규모라 요청률이 smoke 가 만든다. 읽기 전용 프로브 + 샌드박스 fixture
D. CI mock 예산(maxMs)을 실측으로 바꾼다한 표로 통일불채택 — CI 는 네트워크·DB 없이 돌아 실측과 다른 물건. 별도 RELEASE_PERFORMANCE_BUDGETS 로 두고 문서가 차이를 설명

4. 적용한 내용

Phase 1 — 진단 규격 (c96f4582)

docs/gates/performance-diagnostics.md(열쇠 표 7·릴리스 식별·금지 필드 두 층·채널 대응표 11·증거 schema·측정 경로 비용). resolveReleaseIdentity(주입 SHA 7~40 hex 우선). performanceEvidenceReport.ts(RPC 디버그·read-model·Navigation/Resource Timing → allowlist 조립, assertEvidenceHygiene). 테스트 5건.

Phase 2 — workload (e41fe1ea)

프로필 6종(history-1y/4y/10y·same-owner-two-devices(재생 20%·충돌 10%)·multi-owner-10·mixed-read-write-import(인입 근사 500)), 생성기(seed 결정적, owner id 는 프로필·owner 순번 접두), 적재기(저장 문 서버 시간·재생/충돌 기대 검사), manifest(사용자·기기·요청률·동시 인입·기록량·환경 칸). 테스트 8건.

Phase 3 — 측정·기준선 (0cb57677·e1244386)

measure/db.mjs(owner 로서 RPC 9종 20회·backlog 소진(열린 잡 대기 재시도)·큐/세대 통계·lock wait 샘플링·WAL/행 델타) · measure/browser.mjs(일회용 사용자 + 온보딩 완료 + 세션 120 시드, 모바일 뷰포트·CPU 4배·cold/warm) · probe/production.mjs(읽기 전용 15쿼리) · summarize.mjs. 샌드박스를 리셋한 뒤 6프로필 전부 재측정. 결과 = docs/gates/performance-baseline.md.

Phase 4 — 릴리스 예산 (7e7b16d1)

docs/gates/performance-budgets.md + RELEASE_PERFORMANCE_BUDGETS(reads 9·writes 1·stats 4·browser 4·trend 3) + 대조 테스트 4건.

Phase 5 — 장부·기록·PR

coverage-inventory.json 규칙 5줄(성능 스크립트·테스트·증거 모듈·G03 의 경계 테스트 누락분) + --render, 이 기록, 사이드바·README 등록.

주요 결정과 그 근거

  • 기준선 = 이력 프로필 중 가장 큰 p95(프로필 이름 명시): "어느 조건의 상한인가" 를 잃지 않으려고. 4년이 10년보다 큰 항목(홈·검색)이 있어 캐시 순서 효과가 있다 — 재측정 두 번 규칙을 예산 문서에.
  • 서버 시계로 잰다(세 문장: 시작 시각 set_config → 호출 → 차): 한 문장 lateral 은 STABLE 함수를 planner 가 먼저 평가해 0 이 나왔다. 네트워크·PostgREST 는 브라우저 러너가 따로.
  • 인입은 근사(저장 문 연속 호출)로 표시: 저장 문은 source 를 받지 않고 정식 인입 경로는 I01.
  • CI mock 예산은 그대로: 다른 물건이라 통일하면 둘 다 거짓이 된다.
  • RPO/RTO 는 숫자를 쓰지 않았다: 리허설 0회. 사본 존재를 달성으로 쓰지 않는다(이슈 규칙).

작업 중 드러난 것

  • duration_ms 결함(전부 0) — Production·샌드박스 동일. clock_timestamp() 로 바꿔야 한다(D08).
  • Production pg_stat_statements 는 PostgREST 래핑 때문에 RPC 이름을 못 붙인다(track_functions none). 함수별 시간은 track_functions = pl 뒤에만(D08/HQ DB 설정).
  • 매분 cron WAL 46GB/20일·투영 통째 재작성(D08/D11). cron.job_run_details 127k행 정리 없음.
  • get_volume_overview 는 4년부터 CI 예산 1,000ms 를 넘고 10년 3,104ms(1.5MB). get_session_search 4년 804ms. 통계 backlog 소진은 세션 수 비례(10년 128초 = worker 한 라운드가 Production statement_timeout 120초 초과).
  • 혼합 프로필의 저장 max 26,465ms 1건(인입 근사 트랜잭션 중) — 원인 미상, D08 조사 대상.
  • 기본 체크아웃 node_modules 에서 @babel/core 가 사라져 lint 가 죽음(재발) → npm i --no-save @babel/core.
  • Docker 호스트에 다른 세션의 Supabase 컨테이너 130여 개가 떠 있어 측정 잡음 요인 — 조건 표에 적음.
  • 히어독으로 JSON 규칙을 끼워 넣으면 \\. 이 망가진다 → .cjs 파일로.

5. 적용 결과

항목전 → 후
잇는 열쇠 규격없음 → 7종 + 채널 대응표 11행
릴리스 식별청크 파일명 → resolver(주입 시 커밋 SHA; 주입은 HQ 갱신안)
부하 조건 기록없음 → workload.json(사용자·기기·요청률·인입·기록량·환경) 6프로필
서버를 지나는 fixture메모리 mock → 샌드박스 적재 6프로필(최대 5,715세션·68,580세트)
기준 SHA 실측없음 → 읽기 9 RPC × 6프로필 p95·쓰기 p95·큐 age·freshness·WAL·소진·브라우저 cold/warm·Production 표
릴리스 상한maxMs 5개(mock) → 실측 기반 18항목 + 추세 3 + 회복·관찰·RPO/RTO 평가법
telemetry 위생 검사수동 → 이름 기반 검사(단어·정확 일치 두 층) + 테스트
테스트— → 22건, npm run check 통과
앱 동작 변화0(주입 전 release 동작 동일)
미검증Vercel 엣지·실기기 초기 비용, lock wait 시간(ms), 정식 인입 경로, RTO, Production compute 등급(오너 확인 대기), 함수별 Production 시간

6. 이번 개선으로 향상된 것

"전보다 좋아졌다" 를 증명할 출발점이 생겼다

R04 가 같은 fixture·같은 절차로 다시 재면 전후 비율이 나온다. 숫자 없이 "최적화했다" 고 말할 수 없게 됐다.

구조 문제가 숫자로 드러났다

볼륨 리포트의 연차 비례 성장, 통계 재계산의 전체 이력 재작성(WAL 46GB), 잡 시간 측정 결함 — 전부 D08/D11/U05 의 입력으로 넘어간다.

구조적으로 남는 것

진단 규격·증거 스키마·workload 생성기/적재기·측정 러너·Production 프로브·예산 상수와 대조 테스트·변경 절차(넓히려면 오너 결정).

남은 것

  • HQ: vite.config.mjs define 한 줄(SHA 주입), 총괄 §12 G04 행, DB 설정 3종(track_functions·track_io_timing·pg_stat_statements.track), 장부 도구 실패(타 트랙 미분류 13).
  • D08: duration_ms 결함, cron WAL, job_run_details 정리, 저장 max 26초 1건. D11: 볼륨 리포트·소진 세션 비례. U05/B02: 실기기·엣지 초기 비용. I01: 정식 인입 fixture. S11/R03: 복원 리허설(RTO). R04: 전후 비교.
  • 오너 손: Production compute 등급 확인(급하지 않음).
  • 릴리스 v0.18.0 뒤 [v0.18.0 반영완료] + 닫기.