| name | content-hash-cache-pattern |
| description | SHA-256 콘텐츠 해시를 사용해 비용이 큰 파일 처리 결과를 캐시합니다. 경로와 무관하고, 콘텐츠 변경 시 자동 무효화되며, 서비스 레이어 분리를 유지합니다. |
| origin | ECC |
Content-Hash File Cache Pattern
PDF 파싱, 텍스트 추출, 이미지 분석처럼 비용이 큰 파일 처리 결과를 SHA-256 콘텐츠 해시를 캐시 키로 사용해 저장합니다. 경로 기반 캐시와 달리 파일 이동/이름 변경에도 캐시를 재사용할 수 있고, 내용이 바뀌면 자동으로 무효화됩니다.
활성화 시점
- 파일 처리 파이프라인(PDF, 이미지, 텍스트 추출)을 만들 때
- 처리 비용이 높고 같은 파일을 반복 처리할 때
--cache/--no-cache CLI 옵션이 필요할 때
- 기존 순수 함수에 손대지 않고 캐시를 추가하고 싶을 때
핵심 패턴
1. 콘텐츠 해시 기반 캐시 키
경로가 아니라 파일 내용을 키로 사용합니다.
import hashlib
from pathlib import Path
_HASH_CHUNK_SIZE = 65536
핵심 이유:
- 파일 rename/move에도 cache hit
- 내용 변경 시 자동 invalidation
- 별도 인덱스 파일 불필요
2. Frozen dataclass 기반 캐시 엔트리
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class CacheEntry:
file_hash: str
source_path: str
document: ExtractedDocument
3. 파일 기반 캐시 저장
각 캐시는 {hash}.json으로 저장합니다. 해시 기준 O(1) 조회가 가능하고 인덱스 파일이 필요 없습니다.
def write_cache(cache_dir: Path, entry: CacheEntry) -> None:
...
손상된 엔트리는 None을 반환해 cache miss로 처리합니다.
4. 서비스 레이어 래퍼
처리 함수는 순수하게 유지하고, 캐시는 별도 서비스 레이어로 감쌉니다.
def extract_with_cache(
file_path: Path,
*,
cache_enabled: bool = True,
cache_dir: Path = Path(".cache"),
) -> ExtractedDocument:
...
핵심 순서:
- cache 비활성화면 순수 함수 직접 실행
- 파일 해시 계산
- 캐시 조회
- miss면 실제 처리
- 캐시에 기록
핵심 설계 결정
| Decision | Rationale |
|---|
| SHA-256 content hash | 경로와 무관, 내용 변경 시 자동 무효화 |
{hash}.json naming | O(1) 조회, 인덱스 불필요 |
| Service layer wrapper | 추출 함수는 순수, 캐시는 별도 관심사 |
| Manual JSON serialization | frozen dataclass 제어 |
Corruption returns None | 다음 실행에서 재처리 |
| Lazy cache dir creation | 첫 쓰기 시 생성 |
모범 사례
- 경로가 아니라 콘텐츠를 해시합니다
- 큰 파일은 chunk 단위로 해싱합니다
- 처리 함수는 캐시를 몰라야 합니다
- 디버깅용 hit/miss 로그를 남깁니다
- 손상된 캐시는 miss로 처리하고 크래시하지 않습니다
피해야 할 안티패턴
- 경로 기반 캐시
- 순수 함수 내부에 캐시 로직 삽입
- 복잡한 nested frozen dataclass에 무비판적으로
asdict() 사용
적합한 사용 사례
- PDF 파싱, OCR, 텍스트 추출, 이미지 분석
--cache/--no-cache 옵션이 있는 CLI
- 여러 실행에서 같은 파일이 반복 등장하는 배치 처리
- 기존 순수 함수에 캐시를 레이어로 추가할 때
적합하지 않은 경우
- 항상 최신이어야 하는 실시간 데이터
- 캐시 엔트리가 지나치게 커서 스트리밍이 필요한 경우
- 결과가 파일 내용 외 다른 파라미터에도 크게 의존하는 경우