스크립트·파일 자동화

날짜가 붙은 ZIP 백업을 만들고 보관 내용을 검증하기

원본을 변경하지 않고 폴더를 날짜가 포함된 ZIP 파일로 백업한 뒤, 모든 원본 파일이 동일한 내용으로 보관되었는지 확인합니다. 작은 합성 폴더를 이용해 예상 경로, 파일 크기, 전체 크기를 손으로 확인할 수 있습니다.

목차 보기

이런 분께 맞아요ZIP 파일이 생성되었다는 사실만 믿지 않고, 백업 후 실제 보관 내용을 검증하려는 사람을 위한 안내입니다.

준비할 것
  • Python 3.12와 해당 버전을 실행하는 터미널 명령이 준비되어 있어야 합니다.
  • 스크립트가 outputs 아래에 폴더를 만들 수 있는 작업 폴더가 필요합니다.
  • 원본 파일을 읽을 권한과 ZIP 파일을 저장할 충분한 디스크 공간이 필요합니다.
  • Python 표준 라이브러리인 datetime, hashlib, pathlib, zipfile만 사용합니다.

01백업에 무엇을 넣을지 정의하기

이 스크립트는 하나의 원본 폴더에 있는 일반 파일을 보관하며, 하위 폴더의 파일도 포함합니다. ZIP 내부 경로는 원본 폴더를 기준으로 한 상대 경로로 저장합니다. 따라서 reports/week1.txt와 같은 파일은 절대 경로가 아니라 reports/week1.txt로 보관됩니다.

보관 파일명에는 현재 로컬 달력 날짜가 YYYY-MM-DD 형식으로 포함됩니다. 예제를 2026-09-20에 실행한다면 예상 파일명은 backup_demo_2026-09-20.zip입니다. 같은 날짜에 같은 대상 위치로 다시 실행하면 이전 백업을 교체하지 않고 중단합니다.

02합성 원본 폴더 만들기

아래 데이터는 이 글을 위해 작성한 합성 데이터입니다. 준비용 스크립트를 create_backup_demo.py로 저장하세요. 정확한 바이트 내용을 가진 파일 4개를 만들며, 그중 하나가 아니라 두 개가 reports 하위 폴더에 들어갑니다.

python
from pathlib import Path

SOURCE = Path("outputs") / "backup_demo"
FILES = {
    "notes.txt": b"alpha\n",
    "reports/week1.txt": b"beta\n",
    "reports/week2.txt": b"gamma\n",
    "settings.ini": b"mode=test\n",
}

SOURCE.parent.mkdir(parents=True, exist_ok=True)
SOURCE.mkdir()  # Stop if the synthetic source already exists.

for relative_name, content in FILES.items():
    target = SOURCE / relative_name
    target.parent.mkdir(parents=True, exist_ok=True)
    with target.open("xb") as stream:
        stream.write(content)
text
python create_backup_demo.py
상대 경로정확한 내용바이트
notes.txtalpha와 줄바꿈6
reports/week1.txtbeta와 줄바꿈5
reports/week2.txtgamma와 줄바꿈6
settings.inimode=test와 줄바꿈10

원본에는 일반 파일 4개가 있으며 압축 전 내용은 총 27바이트입니다. 계산식은 6 + 5 + 6 + 10입니다. 최종 ZIP 파일 자체의 크기는 이 합계만으로 예측할 수 없습니다. ZIP 압축 결과와 보관 메타데이터에 따른 추가 크기가 있기 때문입니다.

03날짜가 붙는 백업 실행 준비하기

  1. 메인 스크립트를 준비용 스크립트와 같은 위치에 dated_zip_backup.py로 저장합니다.
  2. 이 예제에서는 SOURCE를 outputs/backup_demo로 유지합니다.
  3. ZIP이 원본 폴더 밖에 저장되도록 BACKUP_DIR을 outputs/backups로 유지합니다.
  4. 같은 작업 디렉터리에서 스크립트를 실행합니다.
text
python dated_zip_backup.py

스크립트는 원본이 존재하는지, 그리고 백업 디렉터리가 원본 디렉터리 안쪽에 있지 않은지 확인합니다. 백업의 상위 폴더는 이미 있어도 됩니다. 하지만 날짜가 붙은 ZIP 파일 자체는 존재하지 않아야 합니다.

04ZIP을 생성하고 모든 멤버 검증하기

스크립트는 먼저 원본 파일을 정렬된 목록으로 만듭니다. ZIP 파일은 기존 파일이 있으면 실패하는 배타적 생성 방식으로 만들고, 각 파일을 ZIP_DEFLATED 방식으로 저장합니다. 작성이 끝나면 ZIP을 다시 열어 검증합니다. 검증 과정에서는 의도한 멤버 목록, testzip을 이용한 CRC 무결성, 원본과 ZIP 멤버의 SHA-256 해시를 비교합니다.

python
from datetime import date
import hashlib
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile

SOURCE = Path("outputs") / "backup_demo"
BACKUP_DIR = Path("outputs") / "backups"
CHUNK_SIZE = 1024 * 1024


def hash_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        while chunk := stream.read(CHUNK_SIZE):
            digest.update(chunk)
    return digest.hexdigest()


def hash_member(archive: ZipFile, member_name: str) -> str:
    digest = hashlib.sha256()
    with archive.open(member_name, "r") as stream:
        while chunk := stream.read(CHUNK_SIZE):
            digest.update(chunk)
    return digest.hexdigest()


def main() -> None:
    source_root = SOURCE.resolve(strict=True)
    if not source_root.is_dir():
        raise ValueError("SOURCE must be a directory.")

    backup_root = BACKUP_DIR.resolve()
    if backup_root.is_relative_to(source_root):
        raise ValueError("BACKUP_DIR must be outside SOURCE.")

    files = sorted(
        path for path in source_root.rglob("*")
        if path.is_file() and not path.is_symlink()
    )
    if not files:
        raise ValueError("SOURCE contains no regular files to back up.")

    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    archive_name = f"{source_root.name}_{date.today().isoformat()}.zip"
    archive_path = BACKUP_DIR / archive_name
    if archive_path.exists():
        raise FileExistsError(f"Backup already exists: {archive_path}")

    expected = {}
    with archive_path.open("xb") as raw:
        with ZipFile(raw, "w", compression=ZIP_DEFLATED) as archive:
            for path in files:
                relative = path.relative_to(source_root).as_posix()
                expected[relative] = (path.stat().st_size, hash_file(path))
                archive.write(path, arcname=relative)

    with ZipFile(archive_path, "r") as archive:
        bad_member = archive.testzip()
        if bad_member is not None:
            raise RuntimeError(f"CRC check failed: {bad_member}")

        members = [info for info in archive.infolist() if not info.is_dir()]
        actual_names = [info.filename for info in members]
        expected_names = list(expected)
        if actual_names != expected_names:
            raise RuntimeError("Archive member list does not match the source list.")

        for info in members:
            expected_size, expected_hash = expected[info.filename]
            if info.file_size != expected_size:
                raise RuntimeError(f"Size mismatch: {info.filename}")
            if hash_member(archive, info.filename) != expected_hash:
                raise RuntimeError(f"Hash mismatch: {info.filename}")

    total_bytes = sum(size for size, _ in expected.values())
    print(f"Verified {len(expected)} files totaling {total_bytes} bytes.")
    print(f"Archive: {archive_path.as_posix()}")


if __name__ == "__main__":
    main()

코드는 심볼릭 링크를 건너뛰고 일반 파일만 보관합니다. 예상 해시값은 각 원본 파일을 ZIP에 추가하기 직전에 계산합니다. 실행 중 내용이 바뀌지 않는 일반적인 로컬 폴더라면, 보관 파일을 생성한 뒤 내용을 비교하는 데 유용한 검증 방식입니다.

05예상 ZIP 내용을 손으로 확인하기

스크립트를 2026-09-20에 실행한다면 예상 ZIP 경로는 outputs/backups/backup_demo_2026-09-20.zip입니다. ZIP 안에는 정확히 다음 4개 멤버 경로가 있어야 합니다.

ZIP 내부 멤버압축 전 바이트
notes.txt6
reports/week1.txt5
reports/week2.txt6
settings.ini10

reports 하위 폴더는 파일 멤버의 경로 자체로 표현됩니다. 파일들이 이미 상대 경로를 포함하므로 reports라는 별도 디렉터리 멤버가 반드시 필요하지는 않습니다.

아래 예상 콘솔 출력은 2026-09-20에 실행한다고 가정하고 수동으로 도출한 값입니다. 실제 실행에서 가져온 로그가 아닙니다.

text
Verified 4 files totaling 27 bytes.
Archive: outputs/backups/backup_demo_2026-09-20.zip

06별도의 수동 확인 수행하기

  1. 압축 파일 보기 프로그램으로 ZIP을 열고 예상한 상대 경로의 파일이 정확히 4개 있는지 확인합니다.
  2. ZIP을 별도의 임시 위치에 풀고 텍스트 내용을 원본 4개와 비교합니다.
  3. 백업 완료 후에도 outputs/backup_demo 안의 원본 파일이 그대로 존재하는지 확인합니다.
  4. 같은 날짜에 대상 위치를 바꾸지 않고 백업 스크립트를 다시 실행합니다. 기존 ZIP을 교체하지 않고 FileExistsError로 중단되어야 합니다.
  5. 내용 변경을 검증하고 싶다면 별도의 시험용 원본 사본을 수정하고 다른 ZIP 파일명을 사용해 새 백업을 만들어 비교합니다.

ZIP 파일 자체의 바이트 크기를 주된 검증 기준으로 사용하지 마세요. 압축 때문에 정상적인 ZIP도 데이터와 메타데이터에 따라 압축 전 원본 합계보다 작거나 클 수 있습니다.

07자주 발생하는 오류 확인하기

증상확인할 사항
FileNotFoundErrorSOURCE와 터미널의 작업 디렉터리를 확인하세요. 이 예제에서는 합성 데이터 준비용 스크립트를 먼저 실행해야 합니다.
FileExistsError같은 날짜가 붙은 백업 파일이 이미 존재합니다. 덮어쓰지 말고 기존 백업을 확인한 뒤 다른 목적지나 의도한 다른 백업 날짜를 사용하세요.
PermissionError스크립트가 모든 원본 파일을 읽고 BACKUP_DIR 안에 새 파일을 만들 수 있는지 확인하세요.
CRC check failed해당 ZIP을 실패한 백업으로 취급하세요. 백업으로 의존하지 말고 저장장치나 쓰기 오류를 확인한 뒤 새 ZIP을 만드세요.
멤버 목록 불일치실행 중 원본이 변경되었거나, ZIP 내용이 처음 작성한 파일 목록과 일치하지 않을 수 있습니다.
해시 또는 크기 불일치검증 실패로 취급하세요. 원본을 그대로 유지한 상태에서 원인을 조사한 뒤 새 ZIP을 만드세요.

ZIP 파일을 만든 뒤 예외가 발생하면 일부만 작성되었거나 검증되지 않은 ZIP이 백업 폴더에 남을 수 있습니다. 스크립트는 이를 자동 삭제하지 않습니다. 실패 원인을 조사할 때 필요한 정보를 자동 정리가 숨길 수 있기 때문입니다.

08이 백업이 보장하지 않는 범위 이해하기

이 방식은 파일을 복사해 ZIP으로 보관하는 작업이며, 트랜잭션 방식의 파일 시스템 스냅샷이 아닙니다. 다른 응용 프로그램이 해시 계산이나 ZIP 저장 중 파일을 수정하면 결과가 일관되지 않을 수 있습니다. 중요한 데이터라면 원본에 대한 쓰기를 중지하거나 스냅샷 기능이 있는 백업 시스템을 사용해야 합니다.

이 예제는 파일 시스템의 모든 속성을 보존하지 않습니다. 권한, 소유권, 접근 제어 목록, 확장 속성, 대체 데이터 스트림, 심볼릭 링크, 응용 프로그램별 메타데이터는 다른 백업 도구가 필요할 수 있습니다. ZIP은 이동 가능한 파일 모음을 만드는 데 유용하지만 파일 시스템 전체를 그대로 복제하는 형식은 아닙니다.

검증에 성공한 ZIP도 여전히 하나의 사본일 뿐입니다. 실제 백업 계획에서는 여러 사본, 저장장치 고장, 실수로 인한 삭제, 랜섬웨어, 보존 기간, 암호화, 복구 시험 여부를 고려해야 합니다. 여기서의 검증은 이번 실행에서 관찰한 원본 내용과 ZIP이 일치했다는 점을 확인하는 것이며, 완전한 재해 복구 전략을 의미하지 않습니다.

실행·검증 기록

2026-09-20 · 예제 수동 검토 · 대상: Python 3.12 · 표준 라이브러리: datetime, hashlib, pathlib, zipfile · 실행하지 않음

  • 합성 파일 4개의 크기를 각각 6, 5, 6, 10바이트로 수동 확인했습니다.
  • 압축 전 원본 전체 크기를 27바이트로 수동 계산했습니다.
  • reports 하위의 두 경로를 포함해 예상 ZIP 멤버 경로를 수동으로 정리했습니다.
  • 원본은 읽기만 하고 ZIP은 outputs 아래에 쓰며, 같은 날짜의 ZIP이 이미 있으면 중단하는지 코드를 검토했습니다.
  • CRC 검사, 멤버 목록 비교, 압축 전 크기 비교, SHA-256 비교 로직을 코드로 검토했습니다.
  • 2026-09-20 기준 예상 ZIP 파일명과 콘솔 출력을 손으로 도출했습니다.
검증 한계
  • 이 응답의 작성자는 코드를 실행하지 않았으며, ZIP 파일을 실제로 생성하거나 열지 않았습니다.
  • SHA-256 값과 CRC 결과는 직접 계산하지 않았습니다. 이 예제에서는 비교 로직 자체가 핵심 검증 항목입니다.
  • 실행 중 원본 변경, 권한 오류, 저장장치 손상, 쓰기 중단, 심볼릭 링크, 대규모 디렉터리 성능은 시험하지 않았습니다.
  • 압축된 ZIP 자체의 크기는 압축 결과와 ZIP 메타데이터에 따라 달라지므로 예측하지 않았습니다.
  • 공식 문서 URL은 알려진 문서 위치를 사용했지만 실시간으로 접속해 확인하지 않았습니다.

사이트 전체 작성·검증 원칙

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.