Skip to content

R03 사본 보유에서 실제 선택 복구 검증까지 (2026-09-11)

  • 기간: 2026-09-10 선행 대기 ~ 2026-09-11, 1세션. 오너: “나한테 물어보지 말고 끝까지 완주하고”. 선행 실제 release 병합을 확인한 뒤 착수했다.
  • 랜딩: 앱 PR #1575 · 607647efb894404e583df910153b2c0dae0e656e · 2026-09-11T06:37:13Z, release/v0.18.0. 마이그레이션·Edge 변경/Production 배포 없음. 공통 #1478 제외는 U06의9f7fa381과 R02의02f8c7a7을 소비했다.
  • 설계서: #1542 Phase 계획·예상 효과. 앱 시작849191b5·문서 시작2746e83e.
  • 정본: 선택 복구 runbook, 복구 영수증, 통계 정책.
  • 도구: 앱 scripts/data-copy/rehearse.mjs가 기존 S10/S11 엔진과 D01/D11 비교기를 호출한다. 실제 행·시험 사본·SQL은 저장소 밖 비공개 산출물이다. 정제 증거.
  • 게이트: 소유자 단위8건·실제 로컬 DB2건, 선택 경합 회귀2건과 해당 통계 묶음29건 각각 통과. 보완 후 npm run check3618통과/0실패/93제외(133241ms). 최종 precheck11분27초, pgTAP121파일2435단언과 모든 DB 묶음 통과. Merge Check 실행34570641421 성공. 브라우저/full CI·Production 결과가 아니다.
  • 버그리포트: bug-120-20260911.md, bug-121-20260911.md.
  • 계약: rollback §8-1/8-2, 원본 불변·owner 사전 거절·독립 재실행·미확보 입력 한계.

Phase 현황

Phase내용상태
Phase 1실제 사본·manifest·격리 대상완료, 현재 Production schema 사본 미확보 명시
Phase 2사전 거절·owner 수리·실제 행 대조완료, 64c2b846
Phase 3손상·복구·실패 재개·통계 동치완료, 세 시나리오 모두 통과
Phase 4실제 인입 가용성완료, 원래 Motra/InBody 파일 replay 미검증 명시
Phase 5독립 반복·RPO/RTO·runbook·인계독립 반복·precheck·Merge Check 통과, 앱 PR #1575 release 반영

1. 배경

S10/S11이 제공하는 복구 계약과 실제 사본을 R01 최종 후보에서 함께 검증했다. 사본 파일이 있다는 사실과 실제 family 복원·통계 수렴은 별도 증거가 필요했다. 직접 선행 R01/S10/S11/D11/I01의 merge SHA가 release/v0.18.0 조상인 것을 확인했다.

2. 문제 제기

실제 사본의 owner가 대상 Auth에 없는데도 S11 계획이 ready·25단계를 반환했다. 삭제된 계정은 적용 전에 거절되어야 한다. 또한 인계된 사본은 구형 schema이며 원래 Motra/InBody 파일과 현재 schema의 실제 사본은 추가 인계되지 않아 전체 무손실 복구를 입증할 수 없었다.

3. 해결 방안

D1(2026-09-11): 오너의 전체 완주 지시에 따라 실제 release 선행 확인 후 기존 복구 도구로 격리 리허설을 끝낸다. 기존 writer/정책 비교기를 채택해 새 복구 엔진을 만들지 않았다. owner 존재를 계획 때 확인하고 적용 트랜잭션의 키 공유 잠금으로 계정 삭제와 직렬화한다. 적용 후 오류만 보고하는 방안은 사전 판정을 충족하지 못하므로 기각했다. 전체 DB 덮어쓰기·원문 추정·합성 입력을 실제 증거로 대체하는 방안도 기각했다.

4. 적용한 내용

Phase 1 — 입력과 격리

R01의 보존 manifest를 대조했다. S10/S11/D11/I01과 R01 merge 모두 기준 release의 조상임을 확인했다. 착수 시 S10 문서 PR16S11 문서 PR28은 다른 담당자의 OPEN PR이었고 고정 head 계약을 읽었다. 이를 문서 main 게시 완료로 세지 않는다.

실제 입력은 daily-data-backup-2026-09-09, artifact10125165975/run34405355357에서 인계된 기존 사본이다. 추출 시각 2026-09-09T21:12:21Z, 추정 schema head 20260914000000, 원래 manifest.txt의 SQL bytes/hash에 연결한 valid_reconstructed다. 새 DB head는 20260914213000, PostgreSQL17.6.1.127. 자기 로컬 샌드박스만 사용했고 source 파일 해시는 적재 전후 같았다.

검사결과한계
원래 SQL bytes/hash·gzip·표별 hash·부모 관계통과구형 사본은 dump 전후 DB metadata가 없음
별도 스키마 적재19표,44,357행,4.7초빈4표 열 정보 없음. 특정 가족 복원·통계 완료 RTO와 구분
실제 인입 목록직접작성84/Motra331/Wodup1623세션,manual body_metrics9행Wodup 자동검사 제외. 원문 컬럼의 존재는 파일 재생 가능성을 증명하지 않음
최신 사본·인입 파일HQ 추가 인계 없음전역 부재로 단정하지 않음. 실제 현재 schema 사본·InBody/Motra 원문 파일 검증은 미확보
S10 복구 원천8표 미포함과거 이력·복원/삭제 영수증·인입 배치 원문은 복구할 수 없음

로컬 Auth 행은 사본 owner ID를 연결하기 위한 샌드박스 준비 자료다. 실제 Auth 자격증명·계정 복구가 아니다. 추가 Production 수집을 수행하지 않았다. 새 스키마에서 재생한 사본을 실제 현재 Production 사본으로 바꾸어 부르지 않는다.

Phase 2 — 삭제된 owner 사전 판정 수리

기존 S11 recovery-plan.mjs는 사본 행의 owner 일치만 확인했다. 실제 사본의 owner가 대상 auth.users에 없는 상태에서도 ready·25단계 insert를 반환했다. 실제 관측으로 #1542의 사전 판정 완료조건을 직접 막는 결함임을 확인했다.

계획에서 owner 존재를 확인해 owner_deleted·0단계로 거절한다. 렌더한 티켓 SQL도 첫 fact 접근 전에 owner를 FOR KEY SHARE로 잠그고 재확인한다. 계획 후 계정 삭제는 적용 전에 거절되며, 적용 중 계정 삭제는 복구 트랜잭션이 끝날 때까지 기다린다. S10 엔진이나 원본 등급·범위·통계 writer는 변경하지 않았다.

단위8건과 실제 로컬 DB2건을 통과했다. DB 검사는 계획 후 계정 삭제의 쓰기0, 두 연결의 삭제 대기, rollback과 자원 정리를 확인했다. 실제 사본으로도 삭제 후 적용 거절을 확인했다. 이 회귀의 합성 입력은 실제 사본 검증과 구분한다.

실제 사본 사전 판정

실제 사본에 대응하는 세 가족45행은 기존 S11 계획·티켓 SQL로 적재했고 원본 등급 값·ID·FK·출처를 PostgreSQL EXCEPT 양방향 비교했다. 차이0이다. 다른 owner, 없는 원문, 없는 부모, Motra 사본의 직접 복원은 각각 사전 거절됐다. 실제 사용자 즐겨찾기의 복합키 두 열과 값이 보존됐다.

원본의 별도 시험 사본에서 dump 누락·gzip 변조·manifest 변조를 주입했고, 모두 검증 실패·격리 스키마 생성0을 확인했다. 기대 schema head 불일치도 실패했다. 원래 source는 수정하지 않았다.

Phase 3 — 격리 복원과 통계 재생

실제 사본에서 선택한 주인 기록1가족25행과 같은 owner의 다른 가족15행, 다른 owner의 가족5행을 대조했다. 두 대조 기록에는 샌드박스에서 새 메모를 기록해 사본보다 최신 상태를 만들었다. 원본 등급·ID·FK·출처는 SQL 지문, 통계는 기존 D01 장부의 제외 열과 D11 writer를 사용했다. 기존 정책 불변식과 요청=적용·stale0도 검사했다.

시나리오복구 및 검증 시간full 재생 포함 구간결과
무게 변경→S10 계획/적용→동일 요청 영수증 재생965ms899ms원본·비대상·통계16표 동일
자식1행 삭제→S10 행 복원926ms868ms원래 ID·부모 연결 복원
가족25행 삭제→자식 insert 실패→같은 계획/요청 재개1069ms908ms실패 때 부모 insert·영수증0, 재개 뒤 전부 동일

시간은 이 작은 선택 범위의 로컬 실행이며 전체 사용자·Production RTO 보장이 아니다. 최초 bootstrap full은826ms였다. 통계 비교는 각 단계에 동일 source·정책·업무 기준일의 전체 결과를 사용하며 기존 불변식도 모두 통과했다. 이력은 샌드박스에서 실제 손상 주입 시 생성된 것으로, 원래 사본에 과거 이력이 있었다는 뜻이 아니다.

첫 통계 비교 실패의 원인

처음 두 실행은 다른 기록의 최신 메모를 마지막에 준비해 두어, 복구 후 대상 기록의 session.updated_at이 더 최신이 됐다. 실제 차이는 PR 요약6행의 summary_rank뿐이었다. 정본 build_user_pr_exercise_summary_rows는 원본 세션의 수정 시각을 최근 활동 정렬에 사용한다. source를 바꾸지 않은 추가 full 재생에서는 차이0이었다.

비교 입력을 같은 최근 활동 순서로 만들기 위해 대조 기록을 먼저 준비하고 대상 세션의 revision 준비를 마지막에 완료했다. 원본 fact는 바꾸지 않았고 순위 열을 제외하거나 단언을 완화하지 않았다. 이후 세 시나리오의16표 전체 비교가 통과했다. 앞선 실패는 새 제품 결함이나 성공으로 소급 표시하지 않는다.

Phase 4 — 실제 인입 자료와 복구 불가 범위

Motra331세션 모두 raw_payload에 provider·file_sha256·normalized 객체가 남아 있다. 파일 hash형식331/331, normalized provider_session_id와 source_ref 일치331/331, owner+source_ref 고유331/331을 실제 사본에서 확인했다. 정규화 세션은 가용하지만 원래 파일은 추가 인계되지 않았다. 기존 S10의 Motra 재생은 미지원이며 사본 경로는 import_origin으로 거절했다. 데이터 존재를 원래 XLSX의 재구성·재생 성공으로 바꾸지 않는다.

body_metrics9행의 출처는 모두 manual이었다. 실제 InBody 원문 파일·해당 사본 행이 없어 raw replay·부분 실패·중복 재생은 미검증이다. I01의 CSV fixture는 합성이므로 그 검사 통과로 빈 근거를 채우지 않았다. Wodup 및 관리자 인입 자동검사는 #1478 제외를 유지한다. 재생 불가 자료를 위해 신규 import 엔진이나 Production 수집을 만들지 않았다.

현재 스키마의 실제 Production 사본과 원래 InBody/Motra 파일의 가용성은 HQ 추가 인계 없음으로 기록한다. 전체 저장소/기기의 부재를 단정하지 않는다. R05는 이 범위에서 무손실·전체 인입 복구 통과를 선언할 수 없다.

작업 중 드러난 것

  • 첫 precheck(9c30268e/base849191b5, 14:40~14:51 KST)는11분58초 뒤 실패했다. D06 statsPeriodCalendarParity.test.mjs의 평균 sum/count·불완전 파생 입력 재시도·동결된 전체 corpus3건이 oracleLoaderstats refresh job failed {status:busy}로 실패했다. 다른 DB 묶음과 큰 이력 격리·동시CRUD·통계 수렴 probe는 통과했다. HQ의 다른 실행에서 관측된40P01 교착과 이번 busy를 같은 원인으로 단정하지 않는다. 전체 실패를 보존해 HQ와 담당 경계를 조율하며 같은 precheck를 무작정 반복하지 않았다.
  • 별도 DB의 해당4파일은28/28 통과했으나, 실제 예약 worker를 포함한 선행26+4 검사 뒤에는 D11 legacy computed cutover가 processing(기대failed)으로 실패했다. 일반 리허설 ID에서 예약 worker guard가 거절한 준비 시도도 별도로 보존했고 guard를 완화하지 않았다. CI와 같은 ID 생성 방식의 새 대상에서 실제 순서를 확인했다.
  • 검사 준비 경계 두 곳을2연결로 재현해 수리전2실패→후2통과(3535ms)를 확인했다. D06은 스케줄 중지만으로 외부 연결의 기존 claim/compute 자리를 막지 못해 같은 fixture 연결에 기존 잠금을 유지했다. D11 비경합 cutover 검사는 대상 job/run행을 같은 트랜잭션에서 잡은 뒤 실제 SKIP LOCKED 이관을 호출한다. 별도 연결이 run행을 잡으면 이전 준비는 즉시 건너뛰고, 새 준비는 해제 후 정확히 이관한다. 제품 SQL·계산식·기대값·순위 열은 변경하지 않았다. 최초 busy의 실제 backend PID는 로그에 없어 그 신원까지 확인했다고 주장하지 않는다.
  • 첫 precheck의 pgTAP은123파일2498단언 PASS였으나, 이후 HQ/R02의 공유로 #1478 제외 누락2파일을 확인했다. wodup_import_durable_worker.test.sql45개와 wodup_import_stale_classification.test.sql18개가 이미 실행된 로그를 보존했다. 이63개를 승인된 통과로 세지 않으며 첫 실행은 정책 미준수다. R02 담당자의 최소 제외 커밋을 소비한 뒤 최종 후보 검증을 구분해 기록한다. 당시 U06의9f7fa381 공통 제외만으로 완전한 제외라고 판단한 사전 확인이 부족했다.
  • 로컬 준비에서 postgres 역할의 로그/cron 직접 설정이 권한 거절됐다. source 적재 전 자기 샌드박스 관리자 역할로 설정을 완료했다.
  • Phase2 첫 check는 추가한 감사 선언의 CRLF, 다음 check는 감사 manifest에 없는 파일의 불필요 선언으로 정적 단계에서 거절됐다. LF로 정리하고 필요 없는 선언을 제거했다. 기존 테스트 단언과 감사 manifest는 보존했다.
  • 실제 Motra 선택 첫 시도는 준비한 두 owner에 해당 인입이 없어서 준비 assertion에 실패했다. 실제 사본의 해당 owner를 샌드박스 Auth에 연결해 가용성 거절을 검사했다. 합성 원문으로 대체하지 않았다.

Phase 5 — 독립 반복과 복구 시간

문서의 네 입력만으로 새 고유 샌드박스에서 재실행했다. source 불변 true, 독립 스택 정리 true, 결과 passed_with_documented_input_limits. 기존 첫 실행의 실제 사본과 동일한19표44,357행을 적재하고 동일한45행/복합키/거절/손상 세 시나리오를 대조했다.

두 실행의 복원 대상5표 원본 지문과 full 통계16표 지문을 다시 대조해 동일함을 확인했다. 비대상 session의 지문은 첫 실행에서 준비 메모를 세 번 덧붙였고 새 실행에서는 한 번 덧붙였으므로 다르다. 첫 교차 비교의 전체 비대상 동일 단언은 실패했고 이 입력 차이를 기록했다. 나머지 비대상4표는 같다. 각 실행 안에서는 준비 완료 시점의 전체 비대상 지문을 그대로 비교했으며 세 복구 경우 모두 변경0이었다. 두 실행 사이의 다른 메모를 동일하다고 보고하거나 해당 열을 비교에서 제외하지 않았다.

측정 구간독립 반복 실측
새 샌드박스 준비(복구와 별도)102385ms
사본 검증·격리 적재·행 확인5423ms
범위 선택·dry-run1424ms
사본 apply·ID/FK/원본 대조47065ms
사전 거절·복합키8340ms
손상 세 시나리오·S10 복구·full 통계 대조4747ms
실패 사본 거절4342ms
실제 인입 목록 검사1300ms
전체 리허설(준비·원문 취득 제외)72646ms

측정 종료 2026-09-11T05:28:44.087Z, 사본 생성 2026-09-09T21:12:21Z, 차이 116183.087초(32.27시간)다. 일일24시간 예산은 초과다. 인계된 이 사본의 나이이며 운영 전체의 최신 사본 RPO가 아니다. 파일 취득은 기존 로컬 인계여서 네트워크 시간을 측정하지 않았다. 전체 리허설에는 실패 주입과 반복 검증이 포함되며 작은 선택 범위의 apply/full 시간과 구분한다. 고정 RTO 수치 예산은 기존 정책에 없으며 서비스 전체 복구시간을 약속하지 않는다.

5. 적용 결과

항목전 → 후한계
삭제된 ownerready25단계 → unavailable0단계계획 후 삭제도 쓰기 전에 거절
필수 사전 검사첫 D06 busy3건 실패 → fixture 보완 후 전부 통과첫 #1478 제외 누락63단언은 정책미준수로 보존
원본·관계·출처 보존사본 파일 검증 → 실제45행 양방향 차이0선택 범위 기준
정상 최신 비대상사본보다 최신 대조 가족 → 세 복구 전후 동일같은 owner/다른 owner 대조
통계 복원손상 주입 →16표 기준 결과 동일·stale0순위 열도 비교, 전체 서비스 성능 보장 아님
인입 원문컬럼 존재 → normalized 가용·원래 파일 미인계 구분Motra/InBody raw replay 미검증
독립 반복/RPO/RTO미측정 → 72646ms 독립 리허설·사본 나이 32.27시간인계 사본의 나이, 운영 최신 RPO 아님

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

복구 담당자는 삭제된 owner를 적용 전에 구분하고, 같은 문서 명령으로 실제 사본에서 정상 최신 기록을 보존하며 선택 복구를 반복할 수 있다. 실패한 apply의 부분 저장0과 같은 요청 재개, 원본·ID·FK·출처 및 통계 대조가 근거로 남았다. 원문이 없는 인입은 성공으로 표시하지 않아 복구 약속의 범위를 확인할 수 있다.

남은 것

현재 schema의 실제 Production 사본과 원래 Motra/InBody 파일은 이번 인계에 없었다. 해당 입력의 raw replay·부분 실패·중복 replay는 미검증이며 전역 부재로 단정하지 않는다. Production 수리·승격·배포는 수행하지 않았다. 이슈는 Production 성공 전 열림 상태를 유지한다. R05 인계용으로 이 한계와 사본 RPO 예산 대조·버전/정제 증거를 HQ에 전달했다. 문서 main 반영 후 정본 링크를 추가 인계한다.