파일은 그대로인데 결과는 달라진다: 캐시에 처리 버전이 필요한 이유

이미지 파일의 SHA-256이 어제와 같으면 오늘도 같은 분석 결과를 써도 될까요? 파일이 바뀌었는지 확인하는 데는 도움이 됩니다. 하지만 모델을 바꾸거나 판정 기준을 조정했다면 같은 입력에서도 결과가 달라질 수 있습니다.

캐시를 재사용하려면 “같은 파일인가?”와 함께 **“같은 방식으로 처리했는가?”**를 물어야 합니다. 이 두 조건을 하나의 해시가 모두 설명한다고 생각하면 설정을 고쳤는데도 이전 결과가 남는 이유를 찾기 어려워집니다.

입력의 동일성과 처리의 동일성

예를 들어 점수 0.7을 얻은 항목을 임계값 0.6에서는 통과시키고 0.8에서는 제외하는 처리기를 생각해 봅시다. 입력과 점수가 같아도 최종 판정은 달라집니다. 이 숫자는 원리를 설명하기 위한 예시이며 실제 모델의 측정값은 아닙니다.

이때 파일 해시만 키로 삼아 통과라는 결과를 보관했다면 임계값 변경을 캐시에서 구별하지 못합니다. 재사용할 결과에 영향을 주는 모델과 설정도 캐시의 유효성을 결정해야 합니다. 처리 버전 토큰을 두는 방법은 그 조건을 명시하는 한 가지 선택입니다.

개인 사진 분류 프로젝트의 계획에는 세 항목이 나란히 적혀 있습니다.

  • content_hash: 원본 파일 바이트를 기준으로 계산한 해시입니다.
  • normalized_hash: 정규화한 데이터를 기준으로 계산하는 해시입니다.
  • pipeline_version: 처리 방식의 변경을 구분하기 위한 버전입니다.

첫 두 항목은 입력을 어떤 형태로 비교할지에 관한 선택입니다. 마지막 항목은 그 입력을 처리하는 규칙이 계속 유효한지를 판단하는 데 쓰입니다. 이름이 나란히 있다고 세 값이 하나의 동일성 검사를 수행하는 것은 아닙니다.

버전이 다르면 기존 항목을 읽지 않는 코드

2026-10-27에 확인한 구현은 캐시 JSON의 버전과 요청된 버전을 먼저 비교합니다. 다음은 _load_cache_entries()에서 버전을 검사하고 항목을 읽는 부분입니다. 함수 앞부분에는 파일 존재 여부와 JSON 읽기가 있습니다.

if payload.get("pipeline_version") != pipeline_version:
    return {}

entries: dict[str, dict[str, Any]] = {}
for photo in payload.get("photos", []):
    rel = str(photo["relative_path"])
    entries[rel] = photo
return entries

실제 함수를 나머지 파이프라인과 분리해 임시 JSON으로 확인했습니다. 저장된 버전이 v1일 때 v1로 읽으면 샘플 항목이 반환됐고 v2로 읽으면 빈 딕셔너리가 반환됐습니다. 버전이 다른데도 같은 항목이 있어야 한다는 잘못된 대조 조건은 실패했습니다. 이 확인은 버전 불일치가 기존 캐시를 배제한다는 동작을 검증합니다. 모델 실행 결과나 전체 사진 처리 성능을 검증한 것은 아닙니다.

버전 문자열만 바꾸면 기존 파일을 직접 삭제하지 않아도 다음 읽기에서 이전 항목을 제외할 수 있습니다. 다만 버전을 언제 바꿀지는 이 함수가 결정하지 않습니다. 모델이나 설정이 바뀌어도 같은 버전을 전달하면 이 비교는 통과합니다.

무엇을 캐시하는지 먼저 읽어야 합니다

여기서 구현의 범위를 하나 짚어야 합니다. 이 프로젝트의 해당 캐시는 모델의 얼굴 분석 결과를 저장하지 않습니다. 사진을 스캔하면서 계산한 해시와 파일 정보를 보관합니다. 항목에는 상대 경로, 파일 크기, 수정 시각, 두 해시가 들어갑니다.

같은 버전의 캐시를 읽은 다음에는 파일 크기와 수정 시각을 비교하고 필요한 해시 필드가 있는지 확인해 이전 해시를 재사용합니다. 기존 테스트도 변경되지 않은 파일에서 해시 계산 함수를 다시 호출하지 않는지 검사합니다. 이번 글을 위해 그 전체 테스트를 다시 실행하지는 않았습니다.

따라서 pipeline_version을 바꾸면 모델의 모든 분석 결과가 자동으로 새로 계산된다고 확대해서 설명할 수 없습니다. 실제로 그 버전을 읽는 캐시가 어디인지, 해당 캐시에 무엇이 저장되는지까지 따라가야 합니다. 스캔 캐시를 무효화하는 것과 별도의 추론 결과 저장소를 무효화하는 것은 다른 작업일 수 있습니다.

이 구분은 설계할 때도 유용합니다. 파일 해시를 계산하는 단계, 모델 추론 단계, 임계값으로 판정하는 단계의 결과를 각각 저장한다면 임계값 변경이 어느 결과를 다시 계산하게 해야 하는지 결정할 수 있습니다. 모든 단계에 버전 하나를 쓰면 단순하지만 필요 이상으로 재계산할 수 있습니다. 단계마다 버전을 나누면 재사용 범위는 늘어나지만 단계 사이의 의존성을 빠뜨리지 않아야 합니다. 먼저 한 버전으로 시작하고 실제 재계산 비용이 문제가 될 때 나누는 편이 관리하기 쉽습니다.

정규화 해시도 “같은 사진”을 전부 알아보지는 않습니다

원본 해시와 정규화 해시는 비교 대상이 다릅니다. 원본 바이트 해시는 메타데이터를 포함한 파일 바이트에 기반합니다. 정규화 해시는 어떤 차이를 제거하고 계산하느냐에 따라 의미가 달라집니다.

확인한 구현의 JPEG 경로는 EXIF 방향 정보와 ICC 프로필 메타데이터를 다루는 바이트 정규화를 거칩니다. JPEG가 아니면 원본 해시를 사용합니다. 눈으로 비슷해 보이는 모든 사진을 찾아내는 지각 해시나 모든 이미지 형식의 픽셀을 동일한 방식으로 비교하는 기능은 아닙니다.

그래서 normalized_hash라는 이름만 보고 같아 보이는 이미지는 전부 같은 값이 된다고 가정하면 안 됩니다. 정규화 규칙을 바꾸면 이전 해시와 새 해시를 그대로 섞어 비교해도 되는지 다시 판단해야 합니다. 그 변경 역시 캐시 버전을 올릴 이유가 될 수 있습니다.

버전을 올리는 기준을 결과와 연결하기

캐시에 버전 필드를 추가했다면 리뷰할 질문도 함께 남기는 것이 좋습니다.

  1. 저장된 값은 원본 해시인가, 중간 결과인가, 최종 판정인가?
  2. 이번 변경이 같은 입력에 대해 그 값을 바꿀 수 있는가?
  3. 바꿀 수 있다면 어떤 버전이 달라져야 하며 실제 읽기 경로가 그 값을 비교하는가?

화면의 문구를 고친 일과 정규화 알고리즘을 바꾼 일은 같은 무효화 정책을 요구하지 않습니다. 반대로 모델 이름이 같다는 이유만으로 결과도 같다고 간주해서는 안 됩니다. 사용한 모델 파일과 결과에 영향을 주는 설정을 어디서 식별할지 정해야 합니다.

파일 해시는 입력이 같은지를 답합니다. 캐시를 다시 써도 되는지는 저장한 결과의 의미와 처리 조건까지 알아야 답할 수 있습니다. pipeline_version은 그 판단을 코드에 옮기는 수단이며 실제 캐시의 범위를 읽는 일을 대신해 주지는 않습니다.