상한선 전면 재조정 — '서비스 전 단계' 전제로 잡힌 한도 전수 재검토 (2026-08-30 ~ 08-31)
- 기간: 2026-08-30 ~ 08-31 (2세션 연속, 08-30 실 사용자 저장 차단 사고의 한도 축 후속 — 이슈 #938, 오너 지시 "해결좀 해줘")
- 랜딩: PR #952(Phase 1 본체,
f5237626) · PR #955(Phase 1 D5 + Phase 2·3·4,17534e5b) — 마이그레이션 20260830130000·140000·150000·160000 Productiondb push완료(포스트체크 전부 통과), Vercel 배포 청크 마커(20260830160000·계획 사전검사 문구) 실측, 오너 확인 완료(2026-08-31) — 이슈 #938 [반영완료] 닫음 - 설계서: 이슈 #938 본문(조사 세션의 전수표 + Phase 계획) + Phase 0 오너 결정 코멘트
- 정본:
limits-registry.md(이 트랙이 신설한 한도 장부) ·completedWorkoutWriteContract.ts/appPerformanceBudgets.ts(클라) · schema.sql 마지막 재발행(서버) - 도구: 원격 schema_migrations 실측(push 직전 재확인 — 하루 3개 트랙과 번호 경합, 110000·120000 선점으로 2회 재번호) · 배포 청크 재귀 크롤 ·
scripts/audit-limit-headroom.mjs(신설) - 게이트:
limitsRegistry.test.mjs11건(문서·클라·서버 3층 일치) · pgTAP 3파일 23건(limits_retune_phase1/2·session_search_pagination) · 실경로 관통 단위 테스트(계획 25세트 왕복·휴식 절삭·즐겨찾기 분할·25종목 저장) - 버그리포트: 사고 자체는
bug-041·bug-042(이슈 #942 트랙) — 이 트랙은 그 원인 중 "한도 정책" 축의 수리 - 계약: 쓰기 한도(계획 240·종목 48·종목당 64·reps 2,000·휴식 3,600 절삭)와 읽기 천장(검색 keyset·월로그 900KB·카탈로그 4,096/2.4MB·볼륨 3MB) 전면 개정 — 상세는 한도 장부가 정본
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 0 | 오너 값 결정 D1~D9 (채팅 4건 직접 선택 + 제안값 5건 채택) | ✅ 이슈 코멘트 기록 |
| Phase 1 | 즉시 위험: 계획 24→240세트·reps 500→2,000·휴식 초과 절삭(D1·D3·D4) + 검색 keyset 페이지네이션(D5) + 계획 사전검사 신설 | ✅ PR #952·#955 · Production |
| Phase 2 | 층간 모순: 완료 종목 48/세트 64(D2)·즐겨찾기 분할 병합(D6)·기록방식 2 통일(D9)·사전검사 확장(제목/메모/무게) | ✅ PR #955 · Production |
| Phase 3 | 성장 천장 재예산: 월로그 900KB·카탈로그 4,096행/2.4MB·볼륨 3MB 통일(D7·D8) | ✅ PR #955 · Production |
| Phase 4 | 한도 정본 장부 + 3층 드리프트 게이트 + 여유폭 감사 스크립트 | ✅ PR #955 |
1. 배경
2026-08-30, 실 사용자(acetuna)가 6종목×5세트 계획을 8번 저장 시도해 전부 거부당했다. 거부 사유 중 하나는 A planned session may contain at most 24 sets. — 이 24는 2026-07-21 화면 성능 재설계(PR #252)가 "아직 실제 사용자 서비스 단계가 아니라는 전제"를 본문에 명시하고 잡은 읽기 예산이, 재검토 없이 저장 금지 규칙으로 굳은 것이었다. 조사 세션(71a9d4a7)이 같은 부류를 전수 조사해 위험 한도 6건·층간 모순 4건·성장 시한폭탄 3건을 이슈 #938로 만들었고, 오너가 이 트랙의 진행을 지시했다.
2. 문제 제기
- 사용자가 실제 하는 운동보다 상한이 작았다: 완료 세션 실측 p99가 34세트인데 계획은 24세트에서 거부. 종목당 세트는 실측 최대 30 vs 상한 32 — 여유 2세트.
- 넘치면 안내 없이 죽었다: 검색은 121번째 세션부터 표시 없이 누락(한 사용자의 88%), 즐겨찾기 31개째부터 월 기록표가 화면 에러, 서버 거부 시 "다시 시도해 주세요"만 표시(재시도로 해결 불가능한 상황).
- 천장이 산수와 안 맞았다: 월 기록표는 계약이 허용한 930행의 1/3 크기에서 응답이 죽는 예산(180KB), 볼륨은 층마다 천장이 달라(2.1/1.8MB) 안쪽이 먼저 터지는 구조.
- 같은 한도가 여러 곳에 흩어져 클라 2 vs DB 8(기록방식), 저장 128 vs 호출 30(즐겨찾기) 같은 모순이 생겼고, 그걸 잡는 장부·게이트가 없었다.
3. 해결 방안
원칙(오너 결정, Phase 0): ① 한도 초과는 거부·에러가 아니라 안내·절삭·페이지네이션으로 ② 같은 한도는 한 곳에서 정의하고 클라·서버가 같은 값을 ③ 새 한도는 산정 근거 명기, 여유폭은 주기 감사.
| 대안 | 판단 |
|---|---|
| 상한 전면 철폐 | 기각 — 응답 크기 보호(#252의 원래 목적)는 유효. 값을 현실화하고 초과 처리를 바꾸는 것이 정답 |
| 세션 상세 열람 가드를 '절삭+표시'로 완화(이슈 원안) | 기각(설계 판단) — 상세는 편집 원본이라 잘린 데이터로 편집·저장하면 잘려나간 세트가 사라진다(데이터 손실). 대신 가드를 쓰기 상한과 동일값으로 유지해 위반 세션이 생길 수 없게 함 |
| 검색 상한만 임시 상향 | 기각 — 응답 크기와 충돌. 피드와 같은 keyset 페이지네이션이 정도(채택) |
| 바이트 초과의 전면 '절삭+표시' 전환 | 이번 범위 밖으로 명시 이월 — 재예산으로 즉시 위험 제거, 방향은 한도 장부에 기록 |
4. 적용한 내용
- Phase 1 (20260830130000·140000): 계획 세트 트리거·검증기 24→240 + 계획 상세 읽기 천장 900KB→2MB(저장은 되는데 못 여는 조합 차단), reps CHECK·검증기 500→2,000, 휴식 절삭 BEFORE 트리거(CHECK는 안전망), 검색 keyset 커서(
has_more/next_cursor, 옛 단일 인자 시그니처 drop — 오버로드 모호성 차단) + 컨트롤러 자동 페이지 수집(상한 40페이지), 계획 세트 상한 사전검사(LG_PLAN_SET_LIMIT). - Phase 2 (20260830150000): 완료 구조 검증기·트리거 2종·상세 가드 48/64/240, byte 묶음 CHECK cardinality 8→2, 월 기록표 즐겨찾기 30개 단위 분할 호출·병합(≤5회), 계획 제목/메모/무게 사전검사(
LG_PLAN_TITLE/NOTE/LOAD_LIMIT). - Phase 3 (20260830160000): 월로그 래퍼·코어 900KB, 카탈로그 4,096행·2.4MB, 볼륨 선택자·연간·코어 3MB.
- Phase 4: 한도 장부
limits-registry.md(값·정의 위치·산정 근거·실측) +limitsRegistry.test.mjs3층 일치 게이트 +audit-limit-headroom.mjs(여유 20% 미만 탐지). - 작업 중 드러난 것: ① 같은 날 4개 트랙이 마이그레이션 번호를 경합(110000은 #948, 120000은 #951 선점 → 2회 재번호, #956에는 선점 안내 코멘트) — "push 직전 원격 실측" 절차가 실제로 사고를 막았다. ② 함수 시그니처 변경 시
create or replace는 오버로드를 만들어 PostgREST 호출이 모호해진다 — drop 후 재생성이 정석. ③ 검증기의 완료 캡 계열 계약 테스트가 첫 정의(베이스라인)를 잡고 있어 재발행을 못 보는 사각 — 장부 게이트가 "마지막 정의" 기준으로 보완.
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 6종목×5세트(복합 포함) 계획 저장 | 8회 전부 거부 (사고) | 저장 성공 (상한 240) — pgTAP 25세트 왕복 실측 |
| 세트당 반복 | 500 거부 | 2,000 (실측 최대 215) |
| 휴식 1시간 초과한 날의 운동 | 저장 전체 거부 | 휴식만 3,600초로 잘라 저장 성공 — pgTAP 5,000초→3,600 실측 |
| 검색 대상 세션 (1,014세션 사용자) | 최신 120건 (12%) | 전체 (keyset 자동 수집, 표시·안내 유지) |
| 완료 세션 종목 / 종목당 세트 | 24 / 32 (여유 2세트) | 48 / 64 — pgTAP 25종목·33세트 왕복+상세 열람 실측 |
| 즐겨찾기 31개↑ 월 기록표 | 화면 에러 | 분할 호출 병합 정상 표시 (128 전량) |
| 서버 거부 시 안내 | "다시 시도해 주세요" | 원인 문구 4종 (세트 수·제목·메모·무게) |
| 월 기록표 천장 | 180KB (계약 930행의 1/3에서 사망) | 900KB (만재 650KB + 여유 38%) |
| 종목 선택창 천장 | 2,048행 (52% 소진) | 4,096행·2.4MB |
| 볼륨 리포트 천장 | 층별 상이 (2.1/1.8MB) | 3MB 통일 |
| 한도 정의 | 흩어짐 (모순 4건 발생원) | 장부 1곳 + 3층 일치 게이트 + 여유폭 감사 (현재 전 항목 53~95%) |
| 오너 확인 | — | ✅ 2026-08-31 완료 — 이슈 [반영완료] 닫음 |
6. 이번 개선으로 향상된 것
- 실 사용자를 막던 저장 거부 3종(계획 세트·반복·휴식)이 사라졌고, 남는 거부에는 원인이 보인다.
- 데이터가 쌓여도 죽는 화면(검색 누락·월 기록표·선택창·볼륨)의 시한폭탄이 해제됐다.
- "한도는 어디에 몇으로 왜 있는가"가 장부 1곳으로 모였고, 코드와 어긋나면 CI가 잡는다.
남은 것
- 오너 실기기 확인 → 이슈 #938 닫기.
- 바이트 초과의 전면 "절삭+표시" 전환(D7 후반)·그룹 보드 reps 1,000 정렬·그룹 화면 크기 설계 — 장부 "남은 방향"에 기록, 별도 트랙.
- 카탈로그가 4,096행에 다가가면 선택창 페이지네이션 도입(장부 명기).
후속 랜딩 (2026-08-31, 오너 지시)
완료 운동 저장의 한도 초과도 원인 문구를 갖게 됐다 — PR #968(d45d4b22, Production 청크 실측). 총 세트·종목 수·종목당 세트·반복·무게·제목·메모·세트 메모 8종에 LG_WORKOUT_*_LIMIT 코드를 달아 종료 화면 배너·토스트가 "저장하지 못했어요" 대신 원인을 안내한다. 이 코드들은 값을 고쳐야만 풀리는 영구 실패로 분류해 오프라인 보류 큐 진입을 차단했고, 장부·드리프트 게이트에 "문구 속 숫자 = 정본 값" 검사를 추가했다. 한도 값·서버·DB는 무변경.