원본 변경으로 안전 백업 재개가 막히는 문제 해결

이 글의 목차 7개 항목
  • 분야: DB
  • 기술 스택: TypeScript · Cloudflare D1 · SQLite · R2
  • 관련 기술: 일관된 스냅샷 · 트랜잭션 · 기본 키 순서 · 고정 페이지

문제와 원인

실기 콘텐츠 반영 전 안전 백업은 여러 요청에 걸쳐 원본 테이블을 읽었다. 첫 페이지를 저장한 뒤 다음 페이지를 수집하는 동안 원본 행이 추가·삭제되면, 처음 세어 둔 행 수와 실제 수집 결과가 달라졌다. 백업 검사는 이를 원본 변경으로 판단해 재개를 차단했다.

정렬된 커서로 페이지를 읽는 것만으로는 시작 시점의 데이터가 유지되지 않는다. 이미 읽은 행의 값이 바뀌거나 아직 읽지 않은 행이 삭제되면, 앞뒤 페이지가 서로 다른 시점의 데이터를 담게 된다. 콘텐츠 교체 직전의 복원 기준은 백업을 시작한 한 시점으로 고정해야 했다.

수정 전 코드

안전 백업의 페이지 수집

페이지를 재개할 때마다 원본 테이블을 다시 조회했다.

const query = stablePageQuery(table, next.cursor);
const rows = await allRows(query.sql, query.values);

커서는 중복·누락 없는 순서에 사용됐지만 조회 대상은 계속 바뀌는 원본이었다. 원본 변경 감지를 제거하면 일관되지 않은 백업이 완성될 수 있었다.

수정 후 코드

frozenCaptureCommands()

백업 대상 53개 테이블을 같은 D1 트랜잭션에서 automatic_backup_rows로 복사했다. 복원에 사용하는 열을 명시하고 기본 키 순서로 row_no를 부여한다. 복합 기본 키도 같은 순서로 처리한다.

return tables.map(table => {
  const { spec, keys } = tableSpec(table);
  const columns = spec.columns
    .flatMap(column => [`'${column}'`, identifier(column)]).join(", ");
  return {
    sql: `INSERT INTO automatic_backup_rows
      (snapshot_id, source_table, row_no, payload)
      SELECT ?, ?, row_number() OVER (ORDER BY ${keys.map(identifier).join(", ")}),
        json_object(${columns}) FROM ${identifier(table)}`,
    values: [id, table],
  };
});

소유자·작업 lease·콘텐츠 기준 상태를 검사한 뒤 백업 메타데이터 생성, 53개 복사문, 고정 행 수 저장을 한 배치로 커밋한다. 중간 조건 하나라도 실패하면 메타데이터와 복사 행 모두 롤백된다.

frozenPage()

후속 요청은 원본 대신 고정한 행을 100개씩 읽는다. JSON 복원 뒤 row_no가 연속인지도 검사한다.

const result = await getD1().prepare(`
  SELECT row_no, payload FROM automatic_backup_rows
  WHERE snapshot_id = ? AND source_table = ? AND row_no > ?
  ORDER BY row_no LIMIT ?
`).bind(id, table, rowStart, BACKUP_PAGE_ROWS).all();
rows = next.frozenSource
  ? await frozenPage(backupId, table, next.rowStart)
  : await allRows(query.sql, query.values);

외부 객체 저장·체크섬 확인이 끝나면 해당 백업의 고정 행 삭제와 완료 상태 변경도 같은 트랜잭션으로 처리한다. 다른 백업의 고정 원천은 건드리지 않는다.

검증 결과

로컬 SQLite의 실제 스키마에서 설정 250행과 사용자·복합 키 데이터를 준비한 뒤, 53개 테이블의 고정 복사를 실행했다. 이후 원본 설정 250행을 모두 삭제하고 사용자 이름을 하나에서 live edit으로 변경했다.

DBeaver에서 원본과 고정 행을 직접 조회하면 원본 설정은 0행, 백업 원천은 250행이다. 변경된 사용자도 백업에는 기존 이름 하나로 남았다.

원본 설정 0행과 고정 백업 250행, 사용자 이름 변경 전 값 보존원본 보기 (새 창)

원본 설정 0행과 고정 백업 250행, 사용자 이름 변경 전 값 보존

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.

조회·실패 조건실행 결과
원본 설정 전체 삭제 후 원본 조회0행
같은 시점의 고정 백업 조회250행
페이지 시작 위치 0·100·200·250100행·100행·50행·0행
원본 이름 변경 후 백업 이름하나 유지
복합 키 정렬(a,a) → (a,b) → (b,a) → (b,b) 유지
NULL·한글·따옴표·줄바꿈 복원원본 값과 일치

53개 복사문 뒤에 실패 조건을 넣은 실행에서는 백업 메타데이터 0행, 고정 복사 행 0행으로 롤백됐고 원본 설정 250행은 남았다. 한 백업의 고정 원천을 정리한 뒤에도 별도 백업의 250행은 유지됐다.