
정확 일치 캐시는 같은 질문을 토씨 하나 안 틀리고 다시 물어야만 작동하고, 시맨틱 캐시는 의미가 비슷한 질문까지 잡아내 LLM 응답 캐시의 적중률을 크게 끌어올리지만 그 대가로 서로 다른 질문에 같은 답을 돌려주는 오답 위험을 함께 짊어집니다. 둘 중 한 방식만 쓰면 어느 한쪽 문제를 반드시 만나게 되므로, 실제 서비스에서는 질문의 성격에 따라 캐시 전략을 나눠 쓰는 편이 안전합니다. 이 글에서는 정확 일치와 시맨틱 캐시가 각각 어디서 무너지는지 코드로 직접 확인해 보겠습니다.
정확 일치 캐시가 놓치는 지점
정확 일치 방식은 프롬프트 문자열(모델명, temperature 같은 파라미터까지 포함)을 그대로 해시로 바꿔서 키로 쓰는 구조입니다. 완전히 같은 입력이 들어와야만 캐시된 값을 돌려주기 때문에, 잘못된 답이 섞여 들어올 여지가 거의 없습니다. 문제는 이 ‘완전히 같은 입력’이라는 조건이 실사용 트래픽에서는 생각보다 잘 성립하지 않는다는 점입니다.
아래 예시처럼 띄어쓰기 하나만 달라도 캐시는 미스로 처리됩니다.
import hashlib
cache = {}
def cache_key(prompt: str, model: str = "gpt-4o-mini", temperature: float = 0.0) -> str:
raw = f"{model}|{temperature}|{prompt}"
return hashlib.sha256(raw.encode()).hexdigest()
def get_or_call(prompt, call_fn):
key = cache_key(prompt)
if key in cache:
print("CACHE HIT (exact) ->", cache[key])
return cache[key]
print("CACHE MISS -> LLM 호출")
result = call_fn(prompt)
cache[key] = result
return result
get_or_call("서울 날씨 알려줘", lambda p: "맑음, 27도")
get_or_call("서울 날씨 알려 줘", lambda p: "맑음, 27도") # 띄어쓰기 하나 차이
CACHE MISS -> LLM 호출
CACHE MISS -> LLM 호출
두 문장은 사람이 보기엔 같은 질문이지만, 해시 값이 완전히 달라 캐시가 두 번 다 미스로 처리됩니다. 사용자가 매번 똑같은 문장을 입력하는 사내 배치 작업 같은 환경에서는 정확 일치 캐시만으로도 충분하지만, 채팅형 서비스처럼 사용자가 문장을 자유롭게 쓰는 환경에서는 적중률이 기대만큼 오르지 않습니다.
시맨틱 캐시는 적중률을 얼마나 끌어올릴까요?
시맨틱 캐시는 프롬프트를 임베딩 모델로 벡터화한 뒤, 기존에 저장해 둔 질문 벡터들과 코사인 유사도를 계산해서 일정 임계값 이상이면 저장된 답을 그대로 돌려주는 구조입니다. 표현이 달라도 의미가 비슷하면 캐시가 걸리기 때문에, 정확 일치 방식보다 적중률이 높아지는 것은 구조적으로 자연스러운 결과입니다.
다만 정확히 몇 퍼센트포인트가 오르는지는 서비스마다 다릅니다. 질문 분포가 좁고 반복적인 FAQ형 챗봇이라면 상승 폭이 크지만, 질문이 매번 새로운 컨텍스트를 담는 상담·코딩 어시스턴트라면 상승 폭이 작습니다. 그래서 도입 전에 “이 정도 오를 것”이라고 미리 단정하기보다, 실제 로그로 유사도 분포를 뽑아보고 판단하는 편이 현실적입니다. 이런 구조를 구현한 대표적인 오픈소스가 GPTCache이고, Redis 쪽에서는 RedisVL의 시맨틱 캐시 모듈이, LangChain 쪽에서는 캐시 통합 클래스가 같은 개념을 제공합니다.
유사도 임계값이 오답 위험의 진짜 원인입니다
시맨틱 캐시의 오답은 임베딩 모델이 부정확해서가 아니라, 대부분 임계값 설정 문제에서 나옵니다. ‘주제’가 같은 두 질문은 벡터 공간에서도 가깝게 나오는데, 의도까지 같다고 보장되지는 않습니다.
import numpy as np
def cos_sim(a, b):
a, b = np.array(a), np.array(b)
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
# 실제로는 임베딩 모델이 만들어주는 벡터입니다. 여기서는 계산 원리를 보여주기 위한 예시 값입니다.
q1 = [0.90, 0.10, 0.20] # "환불 규정이 어떻게 되나요?" (정책 질문)
q2 = [0.88, 0.15, 0.18] # "환불 아직 안 됐는데 언제 되나요?" (개인 처리 상태 질문)
print(round(cos_sim(q1, q2), 4))
0.998
두 질문 모두 ‘환불’이라는 주제를 공유하지만 하나는 일반 정책을, 다른 하나는 특정 사용자의 처리 현황을 묻고 있습니다. 임계값을 0.95 정도로 잡아 두면 이 둘은 같은 질문으로 취급돼 정책 답변이 개인 문의에 그대로 나가버립니다. 반대로 임계값을 0.99 이상으로 올리면 오답은 줄지만 캐시 적중률도 정확 일치에 가깝게 떨어져서, 시맨틱 캐시를 쓰는 의미가 옅어집니다.
| 항목 | 정확 일치 캐시 | 시맨틱 캐시 |
|---|---|---|
| 매칭 기준 | 문자열(해시) 완전 일치 | 임베딩 코사인 유사도 |
| 적중률 | 낮음 (표현 차이에 취약) | 상대적으로 높음 |
| 오답 위험 | 거의 없음 | 임계값 설정에 좌우됨 |
| 추가 비용 | 없음 | 임베딩 호출 + 벡터 검색 비용 |
| 적합한 트래픽 | 반복 입력이 정형화된 배치·API | 표현이 다양한 채팅형 서비스 |
캐시를 켰는데 왜 가끔 엉뚱한 답이 돌아올까요?
시맨틱 캐시를 도입한 뒤 이 질문을 받는 경우 원인은 대개 세 가지 중 하나로 좁혀집니다. 첫 번째는 앞서 본 것처럼 임계값이 서비스의 질문 분포에 비해 느슨하게 잡혀 있는 경우입니다. 두 번째는 사용자·세션 구분 없이 캐시를 전역으로 공유해서, A 사용자의 질문 벡터가 B 사용자의 개인화된 질문과 매칭되는 경우입니다.
세 번째는 시간에 따라 답이 바뀌는 질문(재고 수량, 실시간 환율, 오늘 날짜 관련 질문)까지 캐시 대상에 포함시킨 경우입니다. 이런 질문은 벡터가 아무리 정확히 매칭돼도 캐시된 답 자체가 이미 낡은 정보라서 오답이 됩니다. 이 세 원인은 임베딩 모델을 바꾼다고 해결되지 않고, 캐시 정책(임계값, 메타데이터 필터링, TTL)을 조정해야 해결됩니다.
실제 도입 전에 이 체크리스트부터 확인해 보세요
시맨틱 캐시를 붙이기 전에 아래 항목을 먼저 점검해 보시길 권합니다.
- 캐시 대상 질문군이 정책·FAQ성인지, 개인화·시간 민감성인지부터 나눠 보세요. 후자는 아예 캐시 대상에서 빼는 편이 안전합니다.
- 임계값 초기값은 0.95 이상으로 보수적으로 잡고, 실제 오답 신고가 없는 범위에서만 조금씩 낮춰 보세요.
- 캐시 히트마다 유사도 점수와 매칭된 원본 질문을 로그로 남겨서, 나중에 어떤 질문 쌍이 잘못 묶였는지 추적할 수 있게 해두세요.
- TTL(캐시 유지 시간)을 걸어서, 원본 데이터가 갱신됐는데도 오래된 캐시가 계속 나가는 상황을 막으세요.
- 사용자·세션 ID를 메타데이터로 같이 저장해서, 서로 다른 사용자의 개인화 질문이 섞여 매칭되지 않게 필터링하세요.
이 체크리스트는 GPTCache나 RedisVL의 시맨틱 캐시 모듈을 그대로 켠다고 자동으로 해결되지 않습니다. 임계값과 필터링 로직은 서비스 쪽에서 직접 설계해야 하는 부분입니다.
정확 일치와 시맨틱을 함께 쓰는 게 현실적인 답입니다
정확 일치 캐시는 오답 위험이 거의 없지만 적중률이 낮고, 시맨틱 캐시는 적중률이 높은 대신 임계값을 잘못 잡으면 다른 질문에 같은 답을 내보내는 위험을 안습니다. 두 방식은 서로 다른 문제를 풀기 때문에, 하나만 골라 쓰기보다 먼저 정확 일치로 걸러보고 미스가 났을 때만 시맨틱 캐시로 넘어가는 이중 구조가 안전합니다.
다음 단계로는 지금 서비스의 최근 로그에서 사용자 질문 100건 정도를 뽑아 임베딩 유사도 분포를 직접 계산해 보시길 권합니다. 어느 임계값에서 오답 후보(주제는 같지만 의도가 다른 질문 쌍)가 나타나기 시작하는지 확인한 뒤, 그 값보다 여유를 둔 임계값으로 GPTCache나 RedisVL 시맨틱 캐시를 도입하면 오답 위험을 줄이면서 캐시 적중률의 이득을 가져갈 수 있습니다.
