순차 통계의 전 기간 재생을 검증된 시작 상태와 변경 구간으로 전환 (2026-09-10)
- 기간: 2026-09-10, 1개 작업. 오너 요청: “바벨릭 #1417 진행해줘”, “분석 끝나면 바로 진행해”.
- 랜딩: release/v0.18.0 반영 완료, 앱 PR #1522, merge
e2833deed339f342d4edc9cc61de87c560420be4(2026-09-10 16:49 KST). 검증 headc21b4acc, baseb62a358c(D05 #1506·D06 #1518·U03 #1520·D10 #1521 포함). 문서 PR #45. staging/Production 미배포. - 설계서: 분석·5개 Phase 계획.
- 정본: 체크포인트·영향 계약, 기간·달력, 계산 DAG.
- 도구: 앱의
sql:candidate,schema:snapshot,sql:extract,db:model,db:upgrade,stats-session-scope-cost.mjs,stats-isolated-convergence.mjs. 단계별 읽기 계측은 작업 폴더의 비추적 스크립트이며 결과 JSON을 이 문서 저장소에 보존한다. - 게이트: Phase별
npm run check, pgTAP 141파일/2,773단언, D07 독립 full oracle·실제 D06 전달 3건, D06 행동 10건·고정 corpus 66행. 최종 Precheckc21b4acc(기준b62a358c)는 7분21초 통과했다. Merge Check와 release 병합도 성공했다. - 버그리포트: 없음. D07 구조 이식이며 구현 중 드러난 오류는 이 기록에 포함했다.
- 계약: 정책·출처·확정 세대를 검증한 일자 상태, 동일일 전체 재생, 실제 파생 전후 영향, 기존 원자적 발행.
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 변경 전 full oracle·고정 입력·비용 기준선 | 완료, 81b63489 |
| Phase 2 | 체크포인트 검증·세대 보호 | 완료, 03787dfd |
| Phase 3 | PR·근력·최대반복수·점수 재생 | 완료, 6ba8a7f9 |
| Phase 4 | 일자별 저장·발행·D06 실제 영향 연결 | 완료, e3a75a5c → be1979af → 104cd734 |
| Phase 5 | 비용·자동 검증·문서 | 완료, c2f50783 → c21b4acc |
1. 배경
D05가 오늘 세트만 정규화해도 PR·최대반복수·점수 일부는 전 기간을 다시 읽거나 썼다. 이전 최고값·추정 신뢰 상태·점수 참조를 보존하지 않고 날짜 조건만 붙이면, 과거 수정이나 같은 날 순서에 따라 full 결과와 달라질 수 있었다.
2. 문제 제기
기준 fixture의 풀업 4세트 추가는 최대반복수 관측을 240행 삭제하고 244행 삽입한 뒤 488회 갱신했다. 근력에는 suffix 경로가 있었지만 시작 상태에 정책·원본 경계·세대 검증이 없었다. 점수는 출력 날짜를 제한해도 분모 참조를 위해 과거 원본 전체를 정렬했다.
Production 초기 읽기 전용 측정에는 아직 D05 관측 표가 없었다. 운영의 누적 쓰기 카운터를 이번 release의 저장 1건 비용이나 개선율로 해석하지 않았다.
3. 해결 방안
오너의 자동 진행 승인은 기존 수치 정책을 유지하는 D07과 release 통합에 한정했다. e1RM lift-guild.e1rm@3.0.0, curve nuzzo-2024-general-inverse@1.0.0, 최대반복수 barbelic.max-reps@2.0.0, #1401 점수 공식을 보존했다.
| 접근 | 판단 |
|---|---|
| 검증된 일자 완료 상태를 읽고 영향 구간을 같은 정책으로 끝까지 재생 | 채택. 상태가 없거나 불일치하면 같은 엔진의 full 계산 |
| 날짜 조건만 추가 | 기각. 시작 최고값·참조 상태가 없으면 결과가 달라짐 |
| 새 full 엔진과 기존 엔진을 둘 다 활성 유지 | 기각. 정책 중복을 늘림. 과거 SQL은 테스트 전용 oracle로만 보존 |
| 발행 시간 제한 확대 | 기각. 실제 실패를 만든 저장 행 수를 줄이고 기존 예산 유지 |
4. 적용한 내용
Phase 1 — 기존 정책과 비교 기준
같은 canonical UUID·생성시각·D01 anchor 2026-09-07을 사용한다. 기존 SQL 5개를 테스트 fixture에 고정하고, 변경된 기대값을 새 구현에서 생성하지 않는다. 각 저장 단계와 수동 기록 변경 뒤 수치·출처·정렬·null·신뢰 상태를 비교한다.
Phase 2 — 유효한 시작 상태
입력 형식·정책/curve·카탈로그/수동 기록·원본 경계·세대·상태 digest를 검사한다. 영향 날짜보다 엄격히 앞선 일자만 사용한다. 없거나 손상되면 full로 전환한다. 서비스 역할만 저장할 수 있으며 체크포인트 기록 자체는 전역 applied를 바꾸지 않는다.
Phase 3 — 네 순차 계산
PR은 반복수별 최고값과 기존 이벤트 근거, 근력은 accepted/trusted 상태와 최근 참조, 최대반복수는 최근 추정과 누적 한계, 점수는 무게/반복수의 실측/추정 참조를 복원한다. 동일일 중간에서 시작하거나 임의로 조기 종료하지 않는다. 점수 공식은 full/suffix와 canonical fallback이 공유한다.
Phase 4 — 저장·발행과 D06
user_stats_sequential_days가 사용자·종목·완료 일자별 한 행에 네 계산의 독립 상태와 점수 부분을 저장한다. 각 계산의 세대는 별도로 유지하므로 한 계산의 갱신이 다른 계산의 이전 seed를 덮어쓰지 않는다. 기존 private compute와 atomic publish를 그대로 사용한다.
refresh_user_sequential_projections_v1은 재생 구간의 PR·관측·일별 상태·세션 대표·공유 점수를 전후 비교해 D06에 실제 변경 날짜를 보낸다. 그대로인 값은 빈 changes다. 공유 점수가 바뀌면 요청 밖의 구성 종목도 포함한다. 캡처 뒤 원본 경계가 바뀌어 구간이 넓어지면 D06에 full을 요청한다. 기간·달력 표는 D06의 각 writer 하나만 쓴다.
마이그레이션은 20260914103000, 20260914113000, 20260914123000, 20260914133000이다. D06 이후 먼저 반영된 D10 꼬리 20260914093000 뒤로 자기 파일만 이동했다. 고위험 판정의 두 DROP은 이번 작업에서 도입한 재계산 캐시에만 해당하며 원본과 기존 공개 통계는 보존한다.
Phase 5 — 지속되는 검증
DB 검증 레인에 D07 oracle와 D06 기간·달력 행동 검사를 직렬로 연결한다. 기본 단위 실행에서 DB가 없어서 skip된 검사를 합격으로 세지 않는다. 신규 캐시는 일반 제품 snapshot의 숫자 비교 대신 개별 상태 검증과 발행/취소 불변식으로 검사한다.
작업 중 드러난 것
- 초기 세트별 점수·계산 종류별 체크포인트 저장은 큰 이력의 발행 제한 시간을 넘었다. 일자별 저장으로 바꿔 같은 제한 안에서 통과했다.
- 이틀 연속 복합 기록에서 추정 상태가 없는 JSON null을 행으로 복원하려다 실패했다. “상태 없음”을 SQL null로 해석해 기존 결과를 유지했다. scalar·잘못된 메타데이터도 full fallback으로 처리한다.
- 생성 스키마 갱신 전의 작성자 검사, 호출 위치/공백을 고정한 정적 단언 4건, 병합 줄바꿈 2건을 최종 구조에 맞춰 정리한 뒤 재검증했다. 수치나 예산은 완화하지 않았다.
- 업그레이드 예비 실행은 workload 파일 생성 전이라 중단됐다. 준비된 기준 DB에서 workload를 생성해 다시 실행했다. 원천 비교의 권한 표기 2건은 최종 snapshot/extract에서 정리했으며 실제 권한 변경은 없었다.
5. 적용 결과
D06 통합 후, D10 병합 전 동일 260세션·3,120세트의 로컬 단일 측정이다. 운영 p95·사용자 체감 개선율이 아니다. 기준 JSON, 최종 JSON.
| 항목 | 전 → 후 |
|---|---|
| 오늘 무게 종목 추가 전체 유지보수 계산 | 466 → 423ms |
| 오늘 반복수 종목 추가 전체 유지보수 계산 | 454 → 455ms |
| 과거 무게 수정 전체 유지보수 계산 | 1,527 → 2,326ms — 증가 |
| 오늘 반복수 관측 DELETE / INSERT / UPDATE | 240 / 244 / 488 → 0 / 4 / 8 |
| 과거 수정의 최대반복수 관측 DELETE / INSERT / UPDATE | 240 / 236 / 472 → 120 / 116 / 232 |
| 오늘 체크포인트 재사용 | 네 계산 모두 재사용, 무게 9/1·반복수 9/4부터 재생 |
| 원본/수치 정책·provenance | 변경 없음, 기존 full oracle 일치 |
| Production·실기기 | 미검증·미배포 |
단계별 읽기 증거는 계산 종류마다 새 DB 연결을 열어 트랜잭션 안 카운터 차이를 측정했다. 오늘 4세트의 PR/점수/근력/최대반복수 단계에서 원본 세트 읽기는 각각 4/0/25/13행, D05 관측 읽기는 10/18/10/10행이었다. 근력·최대반복수의 참조/FK 확인까지 포함한 값이며 전체 파이프라인 원본 읽기가 0이라는 뜻은 아니다. 전체 원본 prefix를 재정렬하는 대신 검증된 seed와 suffix 입력을 사용한다.
캐시 생성·검증·세대 재인증과 실제 영향 비교 비용이 추가된다. 과거 수정의 전체 시간은 증가했으므로 모든 작업이 빨라졌다고 결론 내리지 않는다. 기존 publisher는 여전히 사용자 출력 전체를 복사/교체한다.
통합 대용량 증거: 최종 Precheck c21b4acc의 660세션·7,920세트, 계산 24,791ms·재계산 22,457ms, 발행 2,637/1,924/1,611ms. 단일 로컬 실행이다. 동시 저장·수정·삭제는 62/45/24ms. 1,976개 일자 캐시의 계산 중 비공개, 발행 후 세대 일치, 취소 후 보존과 full 수렴이 통과했다.
Populated upgrade는 history-4y 1,043세션·원본 33,127행을 보존했다. 3개 파일 총 914ms·최대 371ms·WAL 0.84MB, 조회 오류 0/25·owner probe 20, 공존 break 0, 잠금 대기 최대 86ms였다. 캐시 교체 17/34문장 뒤 rollback/replay를 통과했다. 이후 D06 연결은 함수 재발행만 있는 low migration이다.
최종 통합 게이트
1차 Precheck 37baa08f / 기준 daca15d9: 7분18초 통과. static/build, 단위 3,509 pass·0 fail·63 conditional skip, 마이그레이션 전체 replay·snapshot·pgTAP 141파일/2,773단언·D07/D06·세대·dirty·lease·대용량 검사를 통과했다. 이후 U03·D10 통합에서 번호·생성 장부·테스트 변경 사유를 합쳤고, D10을 포함한 DB를 재생성했다. Phase 5 npm run check는 c21b4acc에서 3,534 pass·0 fail·70 conditional skip으로 통과했다. 최종 Precheck c21b4acc / 기준 b62a358c는 7분21초 통과했다. 단위 3,534 pass·0 fail·70 conditional skip, static/build·migration replay/snapshot·pgTAP 143파일/2,824단언·동시 저장/dirty·D07/D06·D08/D10·대용량 검사가 모두 성공했다. Merge Check 실행이 성공해 16:49 KST에 검증한 merge commit e2833dee를 release에 반영했다. 원격 release가 검증 head를 포함함을 확인했다. 일반 Merge Check는 DB/full CI를 실행하지 않는다.
6. 이번 개선으로 향상된 것
오늘 추가한 세트 때문에 과거 최대반복수 관측을 전부 다시 쓰지 않는다. 시작 상태의 신뢰 조건이 함수 계약과 자동 검사로 남으며, 잘못된 seed를 조용히 재사용하지 않는다. D06은 실제 파생 영향만 받아 기간·달력을 완성하고, 실패한 계산이 확정 세대를 오염시키지 않는다.
남은 것
승인된 release 통합까지 완료했다. staging/Production 배포와 릴리스 전체 full CI는 릴리스 승격에서 별도로 수행한다. D11은 이 체크포인트·영향 DTO·동치/비용 증거와 기존 발행 경계를 이어받는다.