## 무엇을 발견했나 - `docs/decisions/slo-baseline.md §4-1`은 gRPC `SavePoseDataBatch`를 "이 경로는 사용자 대면이 아니다"로 분류하고, 근거로 `latency-perception.md §3`이 "휴식 중 처리 — 운동 흐름 무관"으로 분류했다고 인용한다. - 하지만 `docs/decisions/latency-perception.md §3.2`("세트 경계 / 휴식 중")가 실제로 "휴식 중"으로 분류한 RPC는 `ReportFeedbackBatch`(BT-SET)다. `SavePoseDataBatch`는 별개 RPC이고, §3.2에는 등장하지 않는다 — §4-1의 인용이 두 RPC를 혼동했다. - `ai-server/app/observability/metrics.py:46` 주석: *"SavePoseDataBatch 는 rep 마다 오고 CompleteAnalysis 는 세션당 한 번"* — 스스로 "rep마다"라고 적어놨다. - `ai-server/app/api/endpoints/pose.py:359`에서 `spring_client.report_pose_data_batch(...)`(= gRPC `SavePoseDataBatch`)가 **동기 호출**로 실행되고, 그 결과를 받은 뒤에야 `pose.py:419`의 `return PoseResponse(...)`가 나간다 — 이 `PoseResponse`가 단말의 TTS 발화를 트리거하는 바로 그 응답이다. - `docs/decisions/latency-perception.md §10.2`("rep 완료 → 발화", ⭐로 표시된 "핵심 사용자 체감" 시나리오, 100ms 즉시감 목표)의 단계별 타임라인 표에는 이 gRPC 구간이 **아예 없다** — AI 내부 처리(~30ms) 후 바로 PoseResponse를 보내는 것으로 그려져 있다. ## 왜 문제인가 코드상 `SavePoseDataBatch`는 "휴식 중 배치"가 아니라 "rep 완료 → 발화" 핵심 경로에 낀 동기 블로킹 호출이다. 우리가 가진 유일한 이 RPC의 실측(`docs/decisions/slo-baseline.md §4-0`, c=100 동시 부하)은 p50 429ms / p99 488~1,047ms다. 이 지연 규모가 무부하 단일요청에서도 비슷하다면, 매 rep마다 "100ms 즉시감" 목표를 몇 배 초과하게 된다 — 이 프로젝트의 핵심 UX 설계 전제(§10.2)를 어기는 셈이다. ## 미검증 — 확인 안 된 것 - **무부하 단일요청 지연이 얼마인지 측정된 적이 없다.** §4-0의 429~1,047ms는 c=100 동시성 조건의 값이라 그대로 "사용자 1명이 rep 끝낼 때 걸리는 시간"으로 쓸 수 없다. (이걸로 정책 임계값을 만들면 §4-1이 이미 지운 "실측값에 임의 배수" 함정과 같은 순환논리가 된다.) - 실사용자 트래픽에서 이 블로킹이 실제로 체감 TTS 지연을 유발하는지 로그·트레이스로 확인된 적 없다. ## 재현 경로 (코드 근거) 1. `ai-server/app/api/endpoints/pose.py:359` — `spring_client.report_pose_data_batch(...)` 호출 (블로킹) 2. `ai-server/app/api/endpoints/pose.py:419` — 그 호출이 끝난 뒤에야 `return PoseResponse(...)` 3. 대조 ①: `docs/decisions/latency-perception.md §10.2` 타임라인 표 — 이 RPC 구간이 없다 4. 대조 ②: `docs/decisions/slo-baseline.md §4-1` — "사용자 대면 아님" 근거로 §3 인용(오귀속) 5. `ai-server/app/observability/metrics.py:46` — "SavePoseDataBatch는 rep마다 온다" 주석 ## 제안 1. 무부하 단일요청 latency부터 실측한다 (위 "미검증" 항목). 2. 그 값에 따라 갈린다: - 실제로 짧다(예: 수십 ms) → 문서만 정정한다. `slo-baseline.md §4-1` 재분류 + `latency-perception.md §10.2` 타임라인에 이 구간 추가. - 실제로 길다(수백 ms대) → 비동기화(fire-and-forget + 재시도) 검토가 필요하다. 다만 `ai-server/app/grpc/spring_client.py:43` 주석이 이미 *"SavePoseDataBatch 도 실패하면 rep 하나 분량이 통째로 사라진다"*고 적어놨으므로, 비동기화는 유실 방지 설계(현재의 재시도/서킷 로직을 어떻게 유지할지)가 같이 필요하다. ## 관련 - `docs/decisions/slo-baseline.md` §4-0(gRPC 실측 baseline), §4-1(정책 판정선) - `docs/decisions/latency-perception.md` §3.2, §9.2, §10.2 - 이 발견은 loadtest 스크립트에 "정책적 실패"(SLO 위반을 실패로 카운트) 게이트를 추가하는 작업 중 gRPC 경로용 임계값 근거를 찾다가 나왔다 — 그 작업에서는 이 이슈가 닫히기 전까지 gRPC(ghz) 쪽 정책 게이트를 보류한다.