STEP 01복구에서 필요한 데이터를 먼저 정하기

“백업 파일이 있다”는 말에는 언제까지의 데이터가 있는지, 실제로 읽을 수 있는지에 대한 답이 없습니다. 백업을 만들기 전에 서비스가 반드시 되찾아야 하는 데이터 기준부터 정합니다. 글 서비스라면 마지막으로 확인한 글 ID와 제목, 주문 서비스라면 특정 완료 주문의 상태처럼 내용으로 대조할 수 있는 기준이 필요합니다.

계속 쓰기가 일어나는 DB에서는 복구 검사 시점의 현재 원본과 백업의 행 수가 다를 수 있습니다. 백업 시점에 맞는 기준 자료를 남기거나, 쓰기를 통제한 실험에서 먼저 절차를 확인합니다. 정합성 기준 없이 두 숫자가 다르다는 이유만으로 백업 실패를 판단하지 않습니다.

백업 파일 이름에는 시점을 구분할 정보를 담고, 검사 결과에는 원본 경로·백업 경로·검사 시각을 따로 기록합니다. 파일명 날짜가 맞는지보다 해당 파일이 어떤 데이터를 포함하는지가 더 중요합니다.

STEP 02WAL 모드에서 DB 파일 하나만 복사하지 않기

WAL 모드에서는 커밋된 변경이 WAL에 남아 있다가 체크포인트로 본 DB에 반영될 수 있습니다. 실행 중인 서비스에서 본 DB 파일만 복사하면 최신 내용이 빠질 수 있습니다. 본 DB와 WAL을 서로 다른 시점에 임의로 복사하는 방식 역시 일관된 스냅샷을 보장하는 방법으로 삼지 않습니다.

기존 격리 실험메모 수무결성 검사
원본16ok
본 DB만 복사한 파일12ok
Backup API로 만든 파일16ok
백업에서 별도로 복원한 파일16ok

이 값은 2026-09-12의 통제된 메모 DB 실험에서 나온 것입니다. 무결성 검사 ok와 최신 내용 보존은 다른 질문이라는 점을 보여줍니다. 모든 WAL DB의 본 파일에 항상 네 행이 빠진다는 의미가 아닙니다.

STEP 03공식 Backup API를 이용하는 경로 마련하기

Python sqlite3의 Connection.backup은 SQLite의 백업 기능을 사용할 수 있게 합니다. 아래 코드는 동작을 이해하기 위한 핵심 예시입니다. 백업 저장 공간, 접근 권한, 실행 시간 제한, 실패한 파일 처리와 원격 보관은 별도의 운영 설계가 필요합니다.

from pathlib import Path
import sqlite3

source = Path('source.db').resolve()  # 먼저 실험 DB를 사용합니다.
target = Path('backup-new.db').resolve()
if not source.is_file():
    raise SystemExit('원본 DB가 없습니다.')
if source == target:
    raise SystemExit('원본과 백업 경로가 같습니다.')
# 기존 파일이 있으면 중단합니다. 반복 실행마다 새 이름을 사용합니다.
with target.open('xb'):
    pass
src = sqlite3.connect(source.as_uri() + '?mode=ro', uri=True)
dst = sqlite3.connect(target)
try:
    src.backup(dst)
finally:
    dst.close()
    src.close()

원본이 없을 때 빈 DB를 새로 만들어 백업 성공처럼 보이지 않도록 읽기 전용 연결을 사용합니다. 예외가 발생하면 target에 불완전한 파일이 남을 수 있으므로 그 파일을 완료된 백업으로 취급하지 않습니다. 실제 자동화에서는 검사까지 끝난 파일만 완료 목록에 올리는 방식이 필요합니다.

읽기 전용 연결에도 해당 환경의 WAL·공유 메모리 접근 조건이 영향을 줄 수 있습니다. 권한 오류나 잠금이 생기면 공식 문서와 SQLite 잠금 실험을 참고해 원인을 나누고, 무작정 반복 실행하지 않습니다.

STEP 04별도 복구본에서 구조와 내용을 두 번 검사하기

완료된 백업을 새 경로에 복사해 검사합니다. 운영 DB를 복구 대상으로 덮어쓰지 않습니다. 첫 검사는 파일 구조를 보는 integrity_check이고, 두 번째는 서비스에서 필요한 내용을 보는 조회입니다. 아래 복구 예시는 앞 단계에서 연결을 닫고 완료한 backup-new.db를 사용합니다.

from pathlib import Path
import shutil
import sqlite3

backup = Path('backup-new.db')
restored = Path('restore-check.db').resolve()
# 복구본도 기존 파일을 덮어쓰지 않습니다.
with backup.open('rb') as src, restored.open('xb') as dst:
    shutil.copyfileobj(src, dst)
con = sqlite3.connect(restored.as_uri() + '?mode=ro', uri=True)
try:
    print(con.execute('PRAGMA integrity_check').fetchall())
    print(con.execute(
        "SELECT name FROM sqlite_schema WHERE type='table'"
    ).fetchall())
finally:
    con.close()

두 출력만으로 최신 데이터를 보장하지 않습니다. 실제 스키마에 맞는 조회를 추가해야 합니다. 예를 들어 메모 테이블의 행 수, 마지막으로 보관해야 할 ID의 내용, 첨부파일과의 연결 여부를 대조합니다. 조회문이 성공했는지와 조회 결과가 필요한 기준에 맞는지를 구분합니다.

2026-10-17의 후속 복구 실험에서는 행 수와 마지막 ID가 16으로 같아도 제목이 달랐고, 다른 복구본은 사용자 참조가 깨졌습니다. 선언된 외래 키는 foreign_key_check로, 내용은 동일한 기준 시점의 업무 데이터로 대조합니다. integrity_check는 외래 키 오류를 검출하지 않습니다.

STEP 05자동 실행에서는 겹침과 실패를 별도로 다루기

수동 백업이 맞아도 예약 실행은 다른 사용자·작업 디렉터리·환경 변수를 사용할 수 있습니다. 원본과 목적지는 절대 경로로 정하고, 이전 백업이 진행 중일 때 다음 실행이 겹치지 않는지 확인합니다. flock 실험은 중복 진입을 막는 한 가지 방법의 동작 범위를 보여줍니다.

검사를 실패한 새 백업으로 마지막 정상 백업을 교체하지 않습니다. 백업 생성, 별도 복구 검사, 완료 표시를 구분하면 어느 단계에서 중단됐는지 기록할 수 있습니다. 이는 완성된 스냅샷만 교체하는 실험과 연결되는 운영 원칙이며, DB 백업 파일에 동일한 코드만 적용하면 모든 문제가 해결된다는 뜻은 아닙니다.

같은 서버에만 있는 백업은 서버나 저장 장치를 잃는 사고에 대비하지 못합니다. 별도 보관 위치와 접근 권한, 보관 기간을 정하고, 다른 환경에서도 복원할 수 있는지 확인해야 합니다. 이 글의 로컬 실험은 원격 보관까지 검증한 자료가 아닙니다.

STEP 06복구 가능 판정을 한 줄의 성공 로그로 끝내지 않기

  • 백업을 만들 때 기준 시점과 필요한 데이터 항목을 남겼는가.
  • 본 DB 파일의 임의 복사 대신 일관된 백업 절차를 사용했는가.
  • 운영 DB를 덮지 않는 별도 경로에서 복원했는가.
  • 무결성 검사와 필요한 데이터 내용 대조가 모두 통과했는가.
  • 실패한 새 파일과 마지막 정상 백업이 구분되는가.

실제 운영 복구에서는 쓰기를 멈출 시점, 애플리케이션 스키마 버전, DB 밖의 파일도 함께 검토해야 합니다. 이 가이드는 복구를 준비하는 확인 절차입니다. 운영 DB의 즉시 교체를 지시하는 절차로 사용하지 않습니다.

백업 검사를 처음 만든다면 근거 글의 재현 스크립트로 16행·12행의 차이를 먼저 확인하고, 자기 서비스에서 무엇을 대조해야 할지 정하는 순서가 좋습니다. 복구한 서비스가 실제로 응답하는지까지 확인할 때는 연결 진단 가이드로 돌아가 공개 요청을 검사합니다.

함께 확인한 공식 문서

작성 기록

최초 작성 2026-10-15 · 검토 2026-10-17 · 기존 관찰 자료와 후속 실험을 대조해 확인 순서와 판단표를 정리했습니다. 관찰 당시의 결과이며 현재 모든 환경의 동작을 보장하지 않습니다. 정정 요청