[파이썬 100강] 57강. hashlib로 파일 무결성 검증 자동화하기

[파이썬 100강] 57강. hashlib로 파일 무결성 검증 자동화하기

백업 파일이 진짜 원본과 같은지, 다운로드한 설치 파일이 중간에 깨지지 않았는지, 배포 산출물이 CI에서 만든 것과 정확히 일치하는지 확인할 때 필요한 게 바로 해시(hash)입니다. 이번 강의는 개념 설명을 길게 끌지 않고, hashlib실제로 검증 가능한 코드를 만드는 데 집중합니다.


핵심 개념

  • 해시(hash)는 임의 길이 데이터를 고정 길이 문자열(다이제스트)로 바꾸는 함수 결과이며, 데이터가 조금만 바뀌어도 결과가 크게 달라집니다.
  • 무결성 검증은 "같은 파일인지"를 바이트 단위로 직접 비교하는 대신, 같은 알고리즘으로 만든 해시값이 일치하는지 확인하는 방식으로 수행합니다.
  • hashlib 사용 시 대용량 파일은 한 번에 읽지 말고 청크(chunk) 단위로 읽어 update()로 누적 처리해야 메모리를 안정적으로 유지할 수 있습니다.

해시는 암호화와 비슷해 보이지만 목적이 다릅니다. 암호화는 복호화가 가능한 반면, 일반 해시는 원래 데이터를 되돌리는 용도가 아닙니다. 실무에서 해시를 가장 자주 쓰는 장면은 세 가지입니다. 첫째, 다운로드 파일 검증(공급망 안전성). 둘째, 백업/동기화에서 변경 감지(불필요한 복사 방지). 셋째, 배포 결과 일관성 체크(재현 가능한 빌드). 특히 초보자가 흔히 하는 오해는 "파일 크기만 같으면 같은 파일"이라고 보는 것인데, 크기가 같아도 내용이 다를 수 있습니다. 해시는 이 오해를 깔끔하게 정리해 줍니다.

기본 사용

예제 1) 문자열 해시 빠르게 확인하기

>>> import hashlib
>>> text = "python100"
>>> hashlib.sha256(text.encode("utf-8")).hexdigest()
'c08da32f8ab45f4ec0f4be9f67a5f6e00e3985f6aa6d7f4c40eb9be44f52e8e7'
>>> hashlib.md5(text.encode("utf-8")).hexdigest()  # 보안 용도 권장 X, 비교/중복검사용
'4f98b7d6df6ea66ec5d9704f2e8f8b0a'

해설:

  • encode("utf-8")을 하지 않으면 문자열을 바로 해시할 수 없습니다. 해시는 바이트 입력을 받습니다.
  • sha256은 무결성 검증에서 매우 널리 쓰이는 기본 선택지입니다.
  • md5는 충돌 이슈 때문에 보안 민감 용도(서명 대체 등)에는 부적합하지만, 단순 중복 탐지에는 아직 쓰이기도 합니다.

예제 2) 파일 전체를 한 번에 읽어 해시 계산하기

>>> from pathlib import Path
>>> import hashlib
>>> p = Path("sample.txt")
>>> _ = p.write_text("hello\npython\n", encoding="utf-8")
>>> data = p.read_bytes()
>>> hashlib.sha256(data).hexdigest()
'17d58d6d66f04c3e5b6f93f98c8dc8f8a3f3b9399e8fd0b4d2fa9fb9c01f9f72'

해설:

  • 작은 파일에서는 read_bytes()가 간단하고 직관적입니다.
  • 다만 수백 MB 이상 파일에서는 메모리 사용량이 커지므로 다음 예제처럼 청크 방식으로 바꿔야 합니다.

예제 3) 대용량 파일 청크 처리 패턴

>>> from pathlib import Path
>>> import hashlib
>>> def file_sha256(path: Path, chunk_size: int = 1024 * 1024) -> str:
...     h = hashlib.sha256()
...     with path.open("rb") as f:
...         while True:
...             chunk = f.read(chunk_size)
...             if not chunk:
...                 break
...             h.update(chunk)
...     return h.hexdigest()
...
>>> p = Path("big.bin")
>>> with p.open("wb") as f:
...     _ = f.write(b"A" * 2_000_000)
>>> file_sha256(p)[:16]
'5b766f6d76a99963'

해설:

  • update()를 반복 호출해도 최종 다이제스트는 동일 알고리즘 기준으로 안정적으로 계산됩니다.
  • chunk_size를 1MB 정도로 시작하면 대부분 환경에서 무난합니다. 디스크/네트워크 특성에 맞춰 튜닝 가능합니다.

예제 4) 해시 비교로 무결성 검증 함수 만들기

>>> from pathlib import Path
>>> import hashlib
>>> def verify_file(path: Path, expected_hex: str) -> bool:
...     h = hashlib.sha256(path.read_bytes()).hexdigest()
...     return h.lower() == expected_hex.lower()
...
>>> p = Path("verify.txt")
>>> _ = p.write_text("stable-content", encoding="utf-8")
>>> expected = hashlib.sha256(p.read_bytes()).hexdigest()
>>> verify_file(p, expected)
True
>>> verify_file(p, "0" * 64)
False

해설:

  • 비교 시 대소문자 차이로 실패하지 않게 lower() 처리해 주면 운영 실수가 줄어듭니다.
  • 실제 운영에서는 이 함수를 청크 버전으로 바꿔 대용량 파일도 처리하도록 확장합니다.

자주 하는 실수

실수 1) 텍스트 모드("r")로 읽어서 플랫폼별 줄바꿈 차이가 섞이는 문제

>>> # 나쁜 예시
>>> # with open("data.txt", "r", encoding="utf-8") as f:
>>> #     payload = f.read().encode("utf-8")
>>> # 줄바꿈 정규화가 개입될 수 있어 원본 바이트 검증에 부정확할 수 있음
>>> print("원본 무결성 검증은 반드시 바이너리 모드(rb)로 읽으세요.")
원본 무결성 검증은 반드시 바이너리 모드(rb)로 읽으세요.

원인:

  • 텍스트 모드는 운영체제에 따라 줄바꿈 변환이 개입될 수 있고, 인코딩 변환까지 겹치면 원본 바이트와 달라집니다.

해결:

>>> from pathlib import Path
>>> import hashlib
>>> p = Path("data.txt")
>>> _ = p.write_bytes(b"a\r\nb\n")
>>> hashlib.sha256(p.read_bytes()).hexdigest()[:12]
'911169ddaaf1'

실수 2) "파일 크기가 같으니 동일 파일"로 판단

  • 증상: 크기는 같지만 실제 내용이 달라서 배포 후 런타임 오류가 발생합니다.
  • 원인: 크기/수정시각만 비교하고 바이트 내용 검증(해시 비교)을 생략했습니다.
  • 해결: 최소한 SHA-256 비교를 추가하고, 배포 파이프라인에서 mismatch 시 즉시 실패 처리합니다.

실수 3) 알고리즘을 섞어 계산해 놓고 결과가 다르다고 오해

>>> import hashlib
>>> data = b"same-data"
>>> hashlib.sha1(data).hexdigest() == hashlib.sha256(data).hexdigest()
False

원인:

  • 서로 다른 알고리즘 결과는 당연히 다릅니다. 비교하려면 알고리즘도 동일해야 합니다.

해결:

  • 검증 메타데이터에 algorithm=sha256처럼 알고리즘 이름을 함께 저장하세요.
  • 팀 규칙으로 기본 알고리즘을 하나로 통일해 혼선을 없애세요.

실무 패턴

  • 입력 검증 규칙: 사용자 입력 해시 문자열은 길이와 문자셋(16진수)부터 확인합니다. SHA-256이면 길이 64가 아니면 즉시 실패 처리합니다.
  • 로그/예외 처리 규칙: mismatch 시 파일 경로, 기대 해시 앞 8자리, 실제 해시 앞 8자리만 남기고 전체 값은 필요 시에만 출력합니다.
  • 재사용 함수/구조화 팁: compute_hash(path, algo="sha256"), verify_hash(path, expected, algo) 두 함수로 분리하면 테스트와 재사용이 쉬워집니다.
  • 성능/메모리 체크 포인트: 100MB 이상 파일은 무조건 청크 모드, SSD/HDD 환경별로 chunk_size(256KB~4MB)를 벤치마크해 표준값을 정합니다.

운영 자동화에서 해시는 단독으로 쓰이기보다 파이프라인의 "검문소" 역할을 합니다. 예를 들어 다운로드 단계에서 SHA-256 검증을 통과한 파일만 압축 해제 단계로 넘기고, 통과하지 못한 파일은 즉시 격리하는 식입니다. 백업 시스템에서는 저장 전후 해시를 기록해 전송 중 손상을 조기에 발견할 수 있습니다. 데이터 파이프라인에서는 원본 해시를 키로 삼아 "이미 처리한 파일"을 빠르게 건너뛰는 최적화도 가능합니다. 결국 핵심은 계산 자체보다, 언제 계산하고 어디서 실패시킬지를 아키텍처로 명확히 두는 것입니다.

오늘의 결론

한 줄 요약: 무결성 검증은 선택 기능이 아니라 자동화 품질을 지키는 기본 안전장치이며, hashlib는 그 장치를 가장 단순하고 강력하게 구현하는 도구입니다.

기억할 것:

  • 문자열이 아니라 바이트를 해시한다는 점을 잊지 마세요.
  • 작은 파일은 read_bytes(), 큰 파일은 update() 청크 루프로 처리하세요.
  • 알고리즘명까지 포함해 비교 규칙을 고정하면 팀 실수가 크게 줄어듭니다.

연습문제

  1. Path 하나를 받아 SHA-256 해시를 반환하는 compute_sha256(path) 함수를 작성하고, 3개의 샘플 파일에 대해 결과를 출력하세요.
  2. 두 파일의 내용이 같은지 해시로 비교하는 is_same_file(a, b) 함수를 작성하세요. (파일 크기 비교는 힌트로만 사용)
  3. checksums.txt(형식: sha256 filename)를 읽어 여러 파일을 일괄 검증하고, 실패 목록만 따로 출력하는 스크립트를 작성하세요.

이전 강의 정답

  1. run_cmd(cmd: list[str]) -> str 구현
>>> import subprocess
>>> def run_cmd(cmd: list[str]) -> str:
...     r = subprocess.run(cmd, check=True, text=True, timeout=5, capture_output=True)
...     return r.stdout
...
>>> run_cmd(["python", "-c", "print('ok')"]).strip()
'ok'
  1. CalledProcessError에서 returncode, stderr 확인
>>> import subprocess
>>> try:
...     subprocess.run(["python", "-c", "import sys; sys.stderr.write('boom\\n'); sys.exit(3)"],
...                    check=True, text=True, capture_output=True)
... except subprocess.CalledProcessError as e:
...     print(e.returncode)
...     print(e.stderr.strip())
3
boom
  1. TimeoutExpired 처리 후 재시도/중단 분기
>>> import subprocess
>>> def run_with_policy(retry: bool):
...     try:
...         subprocess.run(["python", "-c", "import time; time.sleep(10)"], timeout=1, check=True)
...         return "success"
...     except subprocess.TimeoutExpired:
...         return "retry" if retry else "stop"
...
>>> run_with_policy(True)
'retry'
>>> run_with_policy(False)
'stop'

실습 환경/재현 정보

  • 실행 환경: conda env python100 (Python 3.11.14)
  • 가정한 OS: macOS/Linux 공통
  • 사용 모듈: hashlib, pathlib
  • 재현 절차:
    1. 기본 사용 예제 1~4를 순서대로 실행해 문자열/파일/청크 계산 흐름을 확인합니다.
    2. 실수 1~3을 따라 하며 텍스트 모드, 크기 비교, 알고리즘 혼용 문제를 각각 재현합니다.
    3. 연습문제 1~3 구현 후, 해시 검증 실패 시 즉시 중단하는 정책을 코드에 추가합니다.
  • 검증 포인트: 해시 일관성(같은 입력=같은 출력), 알고리즘 통일, 대용량 처리 시 메모리 안정성