왜 캐시 키가 문제인가

모노레포에서 Bazel, Nx, Turborepo 같은 빌드 시스템은 이미 만든 결과물을 다시 만들지 않으려고 원격 캐시를 쓴다. 이 캐시의 정확성은 전적으로 "키"에 달려 있다. 키가 실제 입력을 온전히 반영하지 못하면 두 가지 사고가 난다. 첫째는 false hit로, 소스가 바뀌었는데 옛 산출물을 재사용해 잘못된 바이너리가 배포된다. 둘째는 false miss로, 아무것도 안 바뀌었는데 키가 흔들려 매번 풀 빌드가 돌고 캐시 히트율이 바닥난다. 전자는 신뢰성 사고, 후자는 비용 사고다.

키는 무엇을 해싱해야 하는가

안전한 캐시 키는 "이 산출물의 값에 영향을 주는 모든 입력"의 함수여야 한다. 실무에서 빠뜨리기 쉬운 입력은 다음과 같다.

  • 소스 파일 내용(경로가 아니라 내용 해시)
  • 의존 태스크의 산출물 해시(전이적으로 포함)
  • 툴체인 버전: 컴파일러, Node/Python 버전, SDK
  • 빌드 명령과 환경변수 중 결과에 영향을 주는 것
  • 플랫폼: OS, CPU 아키텍처(arm64 vs x86_64)

핵심은 파일의 mtime이나 절대경로를 해싱에 넣지 않는 것이다. 이런 값은 머신마다 달라 false miss를 유발하면서도, 정작 내용 변화는 잡지 못한다.

입력 해싱 구현

파일 목록을 정렬하고, 각 파일의 내용을 해싱한 뒤 상대경로와 함께 다시 해싱하는 것이 기본형이다. 정렬을 빠뜨리면 파일시스템 순서에 따라 키가 달라진다.

import hashlib, os

def file_digest(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 16), b""):
            h.update(chunk)
    return h.hexdigest()

def cache_key(root, files, toolchain, env_keys, extra):
    h = hashlib.sha256()
    # 상대경로 기준 정렬 → 순서 결정성 확보
    for rel in sorted(files):
        h.update(rel.encode())
        h.update(b"\x00")
        h.update(file_digest(os.path.join(root, rel)).encode())
        h.update(b"\x00")
    # 결과에 영향을 주는 환경변수만 선별 포함
    for k in sorted(env_keys):
        h.update(f"{k}={os.environ.get(k,'')}".encode())
        h.update(b"\x00")
    h.update(toolchain.encode())   # 예: "clang-17.0.6"
    h.update(str(extra).encode())  # 플랫폼, 빌드 옵션 등
    return h.hexdigest()

전이 의존은 위 함수를 위상정렬 순서로 태스크마다 호출하되, 하위 태스크의 cache_key를 상위 태스크의 extra에 넣어 접는 방식으로 처리한다. 그러면 하위가 바뀔 때 상위 키도 자동으로 흔들린다.

CI에서의 키 네임스페이스

원격 캐시는 여러 브랜치와 파이프라인이 공유하므로, 순수 입력 해시 앞에 네임스페이스를 붙여 오염을 막는다. 버전을 명시적으로 두면 해싱 로직 자체를 바꿀 때 과거 캐시를 안전하게 무효화할 수 있다.

cache:
  key: "v3-${CI_ARCH}-${INPUT_HASH}"   # v3: 해싱 스킴 버전
  paths:
    - dist/
env:
  # 결과에 영향 없는 값은 해싱에서 제외 → false miss 방지
  IGNORED_FOR_HASH: "CI_JOB_ID,CI_PIPELINE_ID,BUILD_TIMESTAMP"

흔한 함정 비교

입력 요소포함 여부이유
파일 내용 해시포함결과의 직접 원인
파일 mtime / 절대경로제외머신마다 달라 false miss
컴파일러 버전포함같은 소스라도 산출물이 달라짐
CI 잡 ID / 타임스탬프제외결과 무관, 히트율 파괴
CPU 아키텍처포함arm64/x86 바이너리 혼동 방지

검증과 운영

키 설계는 배포 후에도 계속 검증해야 한다. 실무에서 유효한 두 가지 장치가 있다. 첫째는 재현 빌드 대조로, 같은 커밋을 두 머신에서 빌드해 키가 동일한지 CI에서 주기적으로 확인한다. 키가 다르면 비결정 입력이 새어 들어온 것이다. 둘째는 캐시 히트율 모니터링으로, 히트율이 특정 커밋 이후 급락하면 해싱 스킴에 불필요한 입력이 들어갔을 가능성이 높다. 반대로 히트율이 비정상적으로 높다면 입력을 덜 반영하는 false hit를 의심해야 한다.

정리

원격 캐시의 안전성은 "모든 입력을 넣되, 오직 입력만 넣는다"는 원칙으로 요약된다. 결과에 영향을 주는 것은 빠짐없이 해싱하고, 영향을 주지 않는 것은 철저히 배제한다. 해싱 스킴에는 버전을 붙여 언제든 안전하게 무효화하고, 재현 대조와 히트율 지표로 지속적으로 감시한다. 이 세 가지가 갖춰지면 캐시는 비용 절감 도구를 넘어 빌드 정확성의 일부가 된다.