운영 중인 SQLite의 백업은 Python 표준 라이브러리의 Connection.backup()으로 새 스냅샷을 만들고, 그 파일을 다시 열어 검사하는 흐름부터 시작하는 것이 좋습니다. 여기에 별도 환경에서 애플리케이션을 띄워 보는 복구 연습까지 더하면, 작은 팀도 백업이 실제로 쓸 수 있는지 확인할 수 있습니다.
기준일: 2026년 10월 6일. 단일 서버의 관리 도구, 작은 웹 서비스, 배치 작업처럼 SQLite를 직접 운영하는 Python 개발자를 위한 실무 가이드입니다. 예제는 Python 3.12 이상을 기준으로 작성했습니다. 특정 최신 버전의 신기능 소개가 아니며, 별도 패키지를 설치하지 않습니다.
1. 실행 중인 DB에서 파일 하나만 복사하면 놓치는 것
SQLite를 파일 기반 데이터베이스라고 부르지만, 실행 중인 데이터가 언제나 app.sqlite3 한 파일에만 들어 있는 것은 아닙니다. WAL 모드에서는 커밋된 변경 내용이 app.sqlite3-wal에 남아 있을 수 있습니다. 이때 본체만 복사하면 커밋한 데이터가 빠지거나 손상된 복사본을 만들 위험이 있습니다. SQLite 공식 WAL 문서도 WAL 파일을 데이터베이스의 영속 상태 일부로 설명합니다.
그렇다고 본체와 WAL을 차례대로 복사하는 명령을 붙이면 해결되는 것도 아닙니다. 복사하는 사이에 상태가 바뀔 수 있기 때문입니다. 서비스를 완전히 멈추고 연결을 정상 종료한 뒤 복사하는 방식과, 쓰기가 계속되는 데이터베이스의 온라인 백업은 조건이 다릅니다. 이 글에서는 후자를 다룹니다.
SQLite Online Backup API는 데이터베이스를 읽으며 일관된 스냅샷을 다른 데이터베이스에 만듭니다. Python의 sqlite3.Connection.backup()이 이 기능을 제공합니다. 원본 파일 형식과 잠금 처리를 직접 흉내 내는 대신 데이터베이스 엔진에 복사를 맡기는 접근입니다.
2. 최소 예제에도 넣어 둘 안전장치
아래 예제의 목표는 하나입니다. 기존 원본과 이전 백업을 덮어쓰지 않고, 검사에 통과한 새 백업만 구별해 둡니다. 실행할 때마다 새 디렉터리를 만들고, 원본은 읽기 전용 URI로 엽니다. 백업 연결을 닫은 후 새 연결로 검사하며, 모든 검사를 통과했을 때만 VERIFIED.txt를 기록합니다.
사용자가 관리하는 로컬 디스크와 신뢰할 수 있는 애플리케이션 DB를 전제로 한 교육용 예제입니다. 암호화 DB, 전용 확장 모듈이나 사용자 정의 함수가 필요한 스키마는 해당 환경에 맞는 연결 설정과 검증이 추가로 필요합니다. 네트워크 파일시스템 위의 활성 WAL DB는 이 예제의 대상이 아닙니다.
backup_sqlite.py:
"""Python 3.12+: create and validate a new SQLite snapshot.
This example never overwrites the source or an existing backup.
"""
from contextlib import closing
from datetime import datetime, timezone
from pathlib import Path
import sqlite3
import sys
import tempfile
def make_backup(source_path: Path, backup_root: Path) -> Path:
source_path = source_path.resolve(strict=True)
if not source_path.is_file():
raise ValueError("Source must be an existing database file")
backup_root = backup_root.resolve()
backup_root.mkdir(parents=True, exist_ok=True)
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
run_dir = Path(tempfile.mkdtemp(prefix=f"snapshot-{stamp}-", dir=backup_root))
snapshot = run_dir / "snapshot.sqlite3"
source_uri = source_path.as_uri() + "?mode=ro"
with closing(sqlite3.connect(source_uri, uri=True, autocommit=True)) as src:
with closing(sqlite3.connect(snapshot, autocommit=True)) as dst:
src.backup(dst, pages=256)
snapshot_uri = snapshot.as_uri() + "?mode=ro"
with closing(sqlite3.connect(snapshot_uri, uri=True, autocommit=True)) as check:
if check.execute("PRAGMA integrity_check").fetchall() != [("ok",)]:
raise RuntimeError("Snapshot failed integrity_check")
if check.execute("PRAGMA foreign_key_check").fetchone() is not None:
raise RuntimeError("Snapshot failed foreign_key_check")
(run_dir / "VERIFIED.txt").write_text(
f"SQLite {sqlite3.sqlite_version}\n"
"integrity_check=ok\nforeign_key_check=ok\n",
encoding="utf-8",
)
return snapshot
if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit("Usage: python backup_sqlite.py SOURCE_DB BACKUP_DIR")
print(make_backup(Path(sys.argv[1]), Path(sys.argv[2])))
실행 형태는 다음과 같습니다. 처음에는 운영 파일 대신 테스트 DB 경로를 넣어 확인합니다.
python backup_sqlite.py ./demo.sqlite3 ./backups
성공하면 새 snapshot.sqlite3 경로가 출력됩니다. 원본이 없으면 즉시 실패하므로, 경로 오타로 빈 DB를 생성한 뒤 그것을 백업하는 실수를 줄입니다. 백업 대상은 mkdtemp()로 만든 새 디렉터리 안에 둡니다. 이 함수가 만든 디렉터리는 종료 시 자동 삭제되지 않으므로, 예제가 끝난 뒤에도 백업이 남습니다.
backup() 공식 문서에서 pages는 한 번에 복사하는 페이지 수입니다. pages=256은 전체 데이터 중 256페이지만 백업하라는 뜻이 아닙니다. 이 숫자는 예시일 뿐이며, 잠금 대기나 서비스 영향이 없다는 보장은 아닙니다. 실제 DB 크기와 부하를 관찰하면서 조정해야 합니다. backup() 자체는 Python 3.7에 추가됐고, 위 예제가 3.12 이상을 요구하는 이유는 autocommit 인자를 명시했기 때문입니다.
연결 정리에는 contextlib.closing()을 사용했습니다. SQLite 연결의 일반적인 with con:은 트랜잭션 처리를 위한 문맥이며 연결 종료를 대신하지 않습니다. 파일을 다시 열어 검사하거나 다른 위치로 옮기기 전에는 연결 수명도 분명히 관리해야 합니다.
3. 검사 통과가 의미하는 범위를 분명히 하기
PRAGMA integrity_check는 내부 구조와 여러 제약 조건의 일관성을 검사합니다. 정상일 때 반환하는 한 행의 ok 값을 코드에서 확인합니다. 다만 외래키 위반은 이 검사에 포함되지 않으므로, PRAGMA foreign_key_check도 별도로 실행합니다. 외래키 검사는 위반 사항마다 행을 반환하기 때문에, 예제는 한 행이라도 나오면 실패 처리합니다.
두 검사를 통과해도 주문 상태가 올바른지, 업로드 파일이 모두 있는지, 애플리케이션이 정상 기동하는지까지 알 수는 없습니다. 정상적으로 저장된 잘못된 값도 백업에 그대로 들어갑니다. DB 안의 외래키 선언이 없는 업무 관계 역시 별도 쿼리로 확인해야 합니다.
VERIFIED.txt는 이 예제의 두 검사에 통과했다는 표시입니다. 원격 보관 완료나 재해 복구 가능성을 인증하는 파일은 아닙니다. 실패하거나 강제 종료된 실행의 디렉터리는 남을 수 있으므로, 파일이 존재한다는 이유만으로 복구 후보에 넣지 않습니다. 운영 작업에서는 종료 상태와 검사 결과를 함께 기록하고, 마지막 정상 백업을 보존하는 정리 규칙을 따로 두는 편이 좋습니다.
4. 복구 연습은 운영 DB와 떨어진 곳에서
가장 중요한 다음 단계는 복구 리허설입니다. 백업 파일을 만들던 서버에 그대로 두기만 하면, 그 서버나 디스크를 잃었을 때 함께 사라질 수 있습니다. 팀에서 승인한 별도 저장소에 보관한 사본을 실제로 내려받는 과정까지 연습 범위에 넣어야 합니다.
- 복구할 백업을 고릅니다. 생성 시각, 검사 결과, 당시 애플리케이션 버전을 함께 확인합니다. 허용할 수 있는 데이터 손실 시간을 기준으로 백업 간격을 정합니다.
- 새로운 격리 경로에서 엽니다. 운영 중인 DB 위에 덮어쓰지 않습니다. 내려받은 사본에도 동일한 무결성 검사를 다시 수행합니다.
- 서비스에 중요한 읽기 동작을 확인합니다. 대표 고객 조회, 특정 주문과 상세 항목의 연결, 배치 재시작 위치 등 실제 스키마에 맞는 확인 목록을 만듭니다. 원본이 계속 바뀌는 상황에서는 현재 운영 DB와 행 수가 같아야 한다는 기준을 쓰지 않습니다.
- 격리된 애플리케이션을 기동합니다. 테스트 환경에서 외부 메일, 결제, 웹훅과 예약 작업이 실행되지 않도록 환경을 준비합니다. DB 외부의 파일과 설정도 복구에 필요한지 확인합니다.
- 복구에 걸린 시간을 기록합니다. 백업 선택부터 서비스 확인까지 걸린 시간이 팀의 목표 복구 시간 안에 들어오는지 판단합니다.
실제 장애 대응 시 운영 전환은 별도의 절차로 관리해야 합니다. 모든 관련 프로세스와 연결을 어떻게 멈출지, 기존 DB와 WAL 상태를 어떻게 보존할지, 전환 실패 시 어떤 경로로 돌아갈지 먼저 정합니다. 특히 새 DB 본체 옆에 이전 DB의 WAL 파일을 임의로 섞어 두는 식의 복구는 피해야 합니다.
5. 작은 팀이 운영에 붙일 때의 한계
- 백업 작업의 실행 시간을 제한합니다. 원본에 쓰기가 계속되면 백업이 재시작되어 오래 걸릴 수 있습니다. 공식 Backup API 설명도 잦은 변경 때문에 완료되지 못하는 상황을 다룹니다. 연결의 잠금 대기 시간과 전체 작업의 최대 실행 시간은 별개로 관리합니다.
- 읽기 전용 접근 실패를 편법으로 넘기지 않습니다. WAL DB는 보조 파일과 디렉터리 접근 조건이 필요할 수 있습니다. 활성 DB에
immutable=1을 붙이면 잠금과 변경 감지를 생략합니다. SQLite URI 문서는 실제 파일이 변경될 경우 잘못된 결과나 오류가 날 수 있다고 경고합니다. - 백업 파일도 원본과 같은 수준으로 보호합니다. 계정 정보나 업무 데이터가 그대로 담길 수 있습니다. 보관 위치의 권한, 전송 경로, 암호화와 삭제 정책을 함께 점검합니다. 예제는 원격 업로드나 암호화를 구현하지 않습니다.
- 백업 경계를 확인합니다. 이 코드는 기본
main데이터베이스 하나를 복사합니다. 여러 DB와 외부 파일을 묶어 동일 시점으로 복구해야 하는 서비스라면, 별도의 일관성 설계가 필요합니다.
따라서 이 방식은 단일 SQLite DB를 쓰는 작은 서비스가 자동 백업의 출발점을 만들 때 권할 만합니다. 데이터 손실 허용 시간이 매우 짧거나 다중 DB·다중 서버 복구가 핵심 요구사항이면, 이 스크립트만으로 운영 요구를 충족한다고 판단해서는 안 됩니다.
2026년 운영 점검: Python이 사용하는 SQLite 버전도 확인
백업 절차와 별개로, WAL을 운영 중이라면 런타임의 SQLite 수정 사항도 확인할 필요가 있습니다. SQLite는 드문 동시 쓰기·체크포인트 조건에서 손상을 유발할 수 있는 WAL-reset 버그를 3.51.3, 2026년 3월 13일 릴리스에서 수정했습니다. 공식 WAL 문서에는 3.44.6과 3.50.7의 백포트도 안내되어 있습니다.
터미널의 별도 SQLite 실행 파일 버전과 Python이 연결한 라이브러리 버전은 다를 수 있습니다. 먼저 sqlite3.sqlite_version을 확인하고, 사용 중인 Python 배포판이나 OS 패키지의 수정 반영 내역을 대조합니다. 예제의 검증 파일에도 이 값을 남깁니다. 버전 문자열만으로 배포판의 개별 백포트 여부까지 단정하지 않는 것이 좋습니다.
도입 순서는 간단합니다. 테스트 DB에서 새 스냅샷과 검사를 확인하고, 운영 부하를 관찰하며 주기 실행에 붙인 뒤, 별도 보관 사본으로 복구 연습을 해 보세요. 마지막 정상 백업의 시각과 마지막 복구 성공 기록을 함께 볼 수 있으면 운영 판단이 한결 명확해집니다.
작성 안내: 공식 Python·SQLite 문서를 확인하고 AI의 도움을 받아 구성과 예제 코드를 작성했습니다. 동일한 예제 코드를 Python 3.12.14·SQLite 3.53.1에서 자체 임시 DB로 실행해 WAL 백업, 데이터 재조회, 기존 백업 보존, 검사 실패 처리와 잘못된 입력 경로 처리를 확인했습니다. 성능 및 운영 복구 시간을 측정한 결과는 아닙니다. 실제 환경의 파일 권한, 확장 모듈, 저장 장치와 동시 쓰기 부하는 별도로 검증해야 합니다.
댓글
댓글 쓰기