
LLM 임베딩을 캐시할 때 키를 뭘로 잡아야 캐시 적중률이 떨어지지 않는지는, 실서비스에서 API 요금과 응답 속도를 동시에 좌우하는 문제입니다. 이 글에서는 텍스트 해시 하나만 믿고 키를 설계했을 때 실제로 생기는 문제와, 파이썬 코드로 안전하게 캐시 키를 만드는 방법을 다룹니다. 미리 정리하면, 정규화한 텍스트의 SHA-256 해시만으로는 부족하고 여기에 모델명·임베딩 차원 같은 파라미터를 함께 묶어야 캐시가 새지 않습니다.
텍스트 해시만 쓰면 캐시가 새는 이유
같은 문장이라도 어떤 모델에 넣느냐에 따라 임베딩 벡터는 완전히 달라집니다. 그런데 캐시 키를 sha256(원문 텍스트)처럼 텍스트만으로 잡아버리면, 모델을 바꾼 뒤에도 예전 벡터가 그대로 조회되는 상황이 벌어집니다.
문제는 이게 에러 없이 조용히 일어난다는 점입니다. 예를 들어 RAG 파이프라인에서 text-embedding-3-small을 쓰다가 text-embedding-3-large로 바꿨는데, 캐시 키가 텍스트 해시뿐이라면 새 모델용으로 다시 계산해야 할 벡터가 옛날 모델의 벡터로 그대로 나옵니다. 검색 품질이 서서히 떨어지는데 로그에는 아무 이상 신호가 없어서, 원인을 찾는 데 오래 걸리는 경우가 많습니다.
텍스트를 그대로 저장하지 않고 고정 길이 문자열로 바꿔주는 대표적인 해시 함수가 SHA-256입니다. 이걸 쓰는 이유는 단순합니다. 원문 길이가 몇 글자든 항상 64자리 16진수로 고정되기 때문에 키-밸류 스토어에 저장하기 편하고, 특수문자나 개행이 섞여 있어도 안전하게 다룰 수 있습니다.
모델명과 파라미터까지 묶어야 캐시가 안전합니다
캐시 키를 만들 때 텍스트와 함께 묶어야 하는 정보는 생각보다 많습니다. 최소한 아래 항목은 키 생성 문자열에 포함하는 것이 안전합니다.
- 모델 식별자 (예:
text-embedding-3-small, 자체 호스팅이면 모델 체크포인트 이름) - 임베딩 차원 수를 요청 시점에 선택할 수 있는 API라면, 그 차원 값
- 텍스트 정규화 방식 버전 (아래 3번 항목에서 다룰 정규화 규칙이 바뀌면 이 값도 올려야 합니다)
- 자체 호스팅 모델이라면 가중치 리비전(커밋 해시 등) — 같은 이름이라도 가중치가 바뀌면 벡터가 달라지기 때문입니다
세 가지 방식을 비교하면 아래와 같습니다.
| 방식 | 키 길이 | 모델 변경 감지 | 디버깅 편의성 | 비고 |
|---|---|---|---|---|
| 원문 텍스트를 그대로 키로 사용 | 가변(텍스트 길이만큼) | 불가능 | 높음 | 특수문자·개행 처리 번거로움, 키 길이 제한 걸리는 저장소 있음 |
| 텍스트 해시(SHA-256)만 사용 | 고정 64자 | 불가능 | 낮음 | 모델이 바뀌어도 같은 키라 오답 캐시가 조용히 반환됨 |
| 해시 + 모델명 + 파라미터 조합 | 고정 64자 | 가능 | 낮음(별도 매핑 필요) | 모델 교체 시 키가 자동으로 달라져 캐시 무효화가 자연스럽게 됨 |
가운데 열의 “모델 변경 감지”가 핵심입니다. 모델명과 파라미터를 해시 입력값에 섞어 넣으면, 모델을 바꾸는 순간 키 자체가 달라지므로 별도 무효화 로직 없이도 새 벡터가 계산됩니다.
정규화는 어디까지 해야 할까요?
정규화를 얼마나 세게 할지는 은근히 골치 아픈 문제입니다. 앞뒤 공백을 지우고, 중간 공백을 하나로 합치고, 대소문자를 통일하는 식으로 세게 정규화하면 캐시 적중률은 올라갑니다. 하지만 여기서 함정이 있습니다.
캐시 키를 만들 때만 정규화하고, 실제로 임베딩 API에 보내는 텍스트는 원문 그대로 보낸다면 문제가 생깁니다. 공백이 하나 다른 두 문장이 캐시 키는 같아지지만, 실제 모델에 들어가는 입력은 다르기 때문에 진짜 벡터도 다를 수 있습니다. 즉 캐시 키 계산에 쓰는 정규화 규칙과 API에 실제로 보내는 텍스트는 항상 같은 함수를 거쳐야 합니다.
그래서 가장 안전한 접근은, 유니코드 정규화(NFC)와 앞뒤 공백 제거 정도로만 최소한으로 정규화하고, 그 정규화된 문자열을 캐시 키 계산에도 쓰고 API 호출에도 그대로 쓰는 것입니다. 정규화 규칙 자체를 나중에 바꾸게 되면, 앞서 언급한 “정규화 버전” 값을 키에 포함해 기존 캐시와 자연스럽게 분리되게 합니다.
파이썬으로 캐시 키 만들어보기
파이썬 표준 라이브러리의 hashlib 모듈만으로 별도 설치 없이 캐시 키를 만들 수 있습니다. 아래 코드는 정규화한 텍스트에 모델명과 차원 값을 붙여 SHA-256 해시를 계산하는 함수입니다.
import hashlib
import unicodedata
def make_embedding_cache_key(text: str, model: str, dimensions: int | None = None, norm_version: str = "v1") -> str:
normalized = unicodedata.normalize("NFC", text.strip())
key_material = f"{norm_version}:{model}:{dimensions}:{normalized}"
return hashlib.sha256(key_material.encode("utf-8")).hexdigest()
key1 = make_embedding_cache_key("오늘 날씨 어때?", "text-embedding-3-small", 1536)
key2 = make_embedding_cache_key("오늘 날씨 어때?", "text-embedding-3-large", 1536)
print(key1 == key2)
print(len(key1))
실행 결과는 아래와 같습니다.
False
64

모델명만 다르고 텍스트는 완전히 같은데도 키가 서로 달라진 것을 확인할 수 있습니다. 64라는 길이는 SHA-256 다이제스트를 16진수 문자열로 표현했을 때 항상 고정되는 값입니다.
실제 저장소에 넣을 때는 이 해시값을 그대로 키로 써도 되지만, emb:v1:text-embedding-3-small:<해시> 처럼 모델명을 눈에 보이게 앞에 붙여두면, 나중에 특정 모델의 캐시만 골라서 지우거나 검사할 때 훨씬 편합니다. 해시 입력값에는 어차피 모델명이 포함돼 있으니 보안상 문제는 없고, 운영 편의성만 올라갑니다.
이 설계가 흔들리는 경우 — 트레이드오프
모델명·파라미터를 묶은 해시 키도 만능은 아닙니다. 몇 가지 상황에서는 이 설계만으로 부족합니다.
먼저, 텍스트를 토큰 길이 제한에 맞춰 잘라내는 로직 자체가 나중에 바뀌는 경우입니다. 원문 텍스트 해시는 그대로인데, 실제로 API에 전달되는 잘린 시퀀스가 달라지면 캐시된 벡터는 더 이상 지금 로직의 결과와 일치하지 않습니다. 이런 상황을 막으려면 잘라내는 최대 길이나 토크나이저 버전도 키 생성 문자열에 넣어야 합니다.
다음으로, API 제공사가 모델 이름은 그대로 두고 내부 가중치만 조용히 업데이트하는 경우입니다. 이 경우는 캐시 키에 어떤 필드를 추가해도 애초에 감지할 방법이 없습니다. 이런 리스크가 걱정된다면 캐시에 TTL을 걸어 일정 기간 지나면 강제로 재계산하게 하는 방식이 현실적인 대응입니다.
마지막으로, 해시 키만으로는 원문을 역추적할 수 없어서 디버깅이 어려워집니다. “이 키가 어떤 문장이었지?”를 확인하려면 해시값과 원문을 매핑한 별도 테이블을 유지해야 하고, 이건 저장 공간을 추가로 씁니다. 캐시 적중률을 조금 포기하더라도 원문을 함께 로깅해두는 편이 장애 대응에는 유리한 경우가 많습니다.
캐시 키 설계, 배포 전에 점검해보세요
LLM 임베딩을 캐시할 때 키를 뭘로 잡아야 하는지 정리하면, 정규화한 텍스트만 해시하는 방식은 모델이나 파라미터가 바뀌는 순간 조용히 틀린 값을 돌려주는 위험이 있습니다. 정규화 버전·모델명·차원 값을 함께 묶어 해시하면 이 문제를 대부분 피할 수 있고, 앞서 소개한 make_embedding_cache_key 형태의 함수 하나로 바로 적용할 수 있습니다.
지금 서비스에서 임베딩 캐시를 쓰고 있다면, 캐시 키 생성 코드를 열어서 모델명이 문자열에 실제로 포함돼 있는지부터 확인해보시길 권합니다. 텍스트 해시만 쓰고 있었다면, 다음 모델 교체 시점에 캐시 전체를 수동으로 비워야 하는 상황을 피하기 위해서라도 키 구조를 지금 손봐두는 편이 낫습니다.
