왜 AI API 캐싱이 중요한가?
저는 최근 한 전자상거래 플랫폼의 AI 검색 시스템 구축 프로젝트를 진행했습니다. 매일 수십만 건의 사용자 질문이 AI API를 호출하는데, 상당수가 반복되거나 유사한 패턴을 보였습니다. 실제로 로그를 분석해보니 전체 요청의 약 35%가 완전히 동일하거나 의미상 유사한 질문이었습니다.
AI API 비용은 아직 저렴하지만, 트래픽이 증가하면 이야기가 달라집니다. GPT-4.1은 출력 토큰당 $8, Claude Sonnet 4.5는 $15가 청구됩니다. 월 1,000만 토큰 규모에서 이 비용은 빠르게 누적됩니다.
이 글에서는 Redis 기반 정확한 캐싱과 의미론적 유사도 매칭을 결합한 하이브리드 캐싱 전략을 구현해보겠습니다. 이를 통해 불필요한 API 호출을 제거하고 비용을 최적화할 수 있습니다.
월 1,000만 토큰 기준 비용 비교표
| 모델 | 가격 ($/MTok 출력) | 월 1,000만 토큰 비용 | 캐싱 적용 시 예상 비용 | 절감율 |
|---|---|---|---|---|
| GPT-4.1 | $8.00 | $80.00 | $24.00 | 70% |
| Claude Sonnet 4.5 | $15.00 | $150.00 | $45.00 | 70% |
| Gemini 2.5 Flash | $2.50 | $25.00 | $7.50 | 70% |
| DeepSeek V3.2 | $0.42 | $4.20 | $1.26 | 70% |
* 캐싱 적용 시: 반복 요청 30%, 유사 요청 40% 캐시 히트 가정
HolySheep AI: 단일 API 키로 모든 모델 관리
지금 가입하여 HolySheep AI의 글로벌 AI API 게이트웨이 서비스를 경험하세요:
- 로컬 결제 지원 — 해외 신용카드 불필요, 개발자 친화적 결제 옵션
- 단일 API 키 — GPT-4.1, Claude, Gemini, DeepSeek 등 모든 주요 모델 통합
- 비용 최적화 — GPT-4.1 $8/MTok · Claude Sonnet 4.5 $15/MTok · Gemini 2.5 Flash $2.50/MTok · DeepSeek V3.2 $0.42/MTok
- 무료 크레딧 — 가입 시 즉시 사용 가능한 무료 크레딧 제공
아키텍처 개요
우리의 하이브리드 캐싱 전략은 두 단계로 구성됩니다:
- 1단계: 정확한 캐시 조회 — 요청 해시를 키로 사용하여 정확한 일치 탐색
- 2단계: 의미론적 유사도 매칭 — 임베딩 벡터 기반 코사인 유사도로 유사 요청 탐색
요청 → [해시 키 캐시 조회] → 히트 → 응답 반환
↓ 미스
[임베딩 생성] → [벡터 유사도 검색] → 임계값 초과 → 응답 반환
↓ 미스
[AI API 호출] → [캐시 저장] → 응답 반환
Redis 설치 및 환경 설정
# Docker를 사용한 Redis 설치
docker run -d \
--name redis-cache \
-p 6379:6379 \
-v redis_data:/data \
redis:7-alpine \
redis-server --appendonly yes
Redis CLI 연결 확인
docker exec -it redis-cache redis-cli ping
PONG 응답 확인
의존성 패키지 설치
# Python dependencies
pip install redis sentence-transformers numpy openai scipy
프로젝트 구조
ai_cache/
├── cache_manager.py # 메인 캐시 관리자
├── embedding_service.py # 임베딩 생성 서비스
├── config.py # 설정 파일
└── requirements.txt # 의존성
설정 파일 구성
# config.py
import os
class Config:
# HolySheep AI 설정
HOLYSHEEP_API_KEY = os.getenv("HOLYSHEEP_API_KEY", "YOUR_HOLYSHEEP_API_KEY")
HOLYSHEEP_BASE_URL = "https://api.holysheep.ai/v1"
# Redis 설정
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
REDIS_DB = int(os.getenv("REDIS_DB", 0))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None)
# 캐시 TTL 설정 (초)
CACHE_TTL = 60 * 60 * 24 * 7 # 7일
# 의미론적 유사도 임계값 (0.0 ~ 1.0)
SEMANTIC_SIMILARITY_THRESHOLD = 0.92
# 사용할 임베딩 모델
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
# 벡터 차원 (all-MiniLM-L6-v2 기준)
EMBEDDING_DIMENSION = 384
임베딩 서비스 구현
# embedding_service.py
from sentence_transformers import SentenceTransformer
import numpy as np
from typing import List
import hashlib
import redis
from config import Config
class EmbeddingService:
"""의미론적 임베딩 생성 및 벡터 유사도 검색 서비스"""
def __init__(self, config: Config):
self.model = SentenceTransformer(config.EMBEDDING_MODEL)
self.dimension = config.EMBEDDING_DIMENSION
self.redis_client = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
db=config.REDIS_DB,
password=config.REDIS_PASSWORD,
decode_responses=False
)
self.config = config
def generate_embedding(self, text: str) -> List[float]:
"""텍스트에서 임베딩 벡터 생성"""
embedding = self.model.encode(text, normalize_embeddings=True)
return embedding.tolist()
def store_vector(self, request_hash: str, embedding: List[float],
response_data: dict) -> None:
"""임베딩 벡터와 응답 데이터를 Redis에 저장"""
key = f"vec:{request_hash}"
# 벡터를 바이트로 변환하여 저장
vector_bytes = np.array(embedding, dtype=np.float32).tobytes()
self.redis_client.set(key, vector_bytes, ex=self.config.CACHE_TTL)
# 응답 데이터는 별도 키로 저장
response_key = f"resp:{request_hash}"
import json
self.redis_client.set(response_key, json.dumps(response_data),
ex=self.config.CACHE_TTL)
def find_similar(self, query_embedding: List[float],
exclude_hash: str = None, top_k: int = 5) -> List[tuple]:
"""코사인 유사도를 기반으로 유사 요청 탐색"""
query_vector = np.array(query_embedding, dtype=np.float32)
# 모든 벡터 키 스캔
cursor = 0
results = []
while True:
cursor, keys = self.redis_client.scan(
cursor=cursor,
match="vec:*",
count=100
)
for key in keys:
request_hash = key.decode('utf-8').replace('vec:', '')
# 자기 자신 제외
if exclude_hash and request_hash == exclude_hash:
continue
# 벡터 조회 및 유사도 계산
vector_bytes = self.redis_client.get(key)
if vector_bytes:
stored_vector = np.frombuffer(vector_bytes, dtype=np.float32)
similarity = self._cosine_similarity(query_vector, stored_vector)
results.append((request_hash, similarity))
if cursor == 0:
break
# 유사도 순으로 정렬
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_k]
@staticmethod
def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
"""코사인 유사도 계산"""
return float(np.dot(a, b))
def get_cached_response(self, request_hash: str) -> dict:
"""캐시된 응답 데이터 조회"""
import json
response_key = f"resp:{request_hash}"
data = self.redis_client.get(response_key)
return json.loads(data) if data else None
메인 캐시 관리자 구현
# cache_manager.py
import hashlib
import json
import redis
from typing import Optional, Dict, Any
from openai import OpenAI
from embedding_service import EmbeddingService
from config import Config
class AICacheManager:
"""AI API 응답 캐시 관리자 - 정확한 캐싱 + 의미론적 유사도 매칭"""
def __init__(self, config: Config):
self.config = config
self.redis_client = redis.Redis(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
db=config.REDIS_DB,
password=config.REDIS_PASSWORD
)
self.embedding_service = EmbeddingService(config)
# HolySheep AI 클라이언트 초기화
self.client = OpenAI(
api_key=config.HOLYSHEEP_API_KEY,
base_url=config.HOLYSHEEP_BASE_URL
)
# 캐시 통계
self.stats = {
"exact_hits": 0,
"semantic_hits": 0,
"cache_misses": 0,
"total_tokens_saved": 0
}
def _hash_request(self, request_data: Dict[str, Any]) -> str:
"""요청 데이터를 해시로 변환"""
normalized = json.dumps(request_data, sort_keys=True)
return hashlib.sha256(normalized.encode()).hexdigest()[:16]
def _exact_cache_lookup(self, request_hash: str) -> Optional[Dict]:
"""정확한 해시 기반 캐시 조회"""
cache_key = f"exact:{request_hash}"
cached = self.redis_client.get(cache_key)
return json.loads(cached) if cached else None
def _store_exact_cache(self, request_hash: str, response: Dict,
ttl: int = None) -> None:
"""정확한 캐시 저장"""
cache_key = f"exact:{request_hash}"
self.redis_client.set(
cache_key,
json.dumps(response),
ex=ttl or self.config.CACHE_TTL
)
# 의미론적 캐시에도 저장 (벡터 기반 검색용)
request_text = response.get("request_text", "")
if request_text:
embedding = self.embedding_service.generate_embedding(request_text)
self.embedding_service.store_vector(request_hash, embedding, response)
def get_response(self, user_message: str,
system_prompt: str = None,
model: str = "gpt-4.1") -> Dict[str, Any]:
"""
AI 응답 가져오기 - 캐시 우선 전략
1. 정확한 해시 매칭 시도
2. 실패 시 의미론적 유사도 매칭
3. 모두 실패 시 API 호출
"""
# 요청 구성
request_data = {
"messages": [
{"role": "system", "content": system_prompt or ""},
{"role": "user", "content": user_message}
],
"model": model
}
request_hash = self._hash_request(request_data)
# 1단계: 정확한 캐시 조회
cached_response = self._exact_cache_lookup(request_hash)
if cached_response:
self.stats["exact_hits"] += 1
cached_response["cache_hit"] = "exact"
print(f"✓ 정확한 캐시 히트: {request_hash}")
return cached_response
# 2단계: 의미론적 유사도 매칭
query_embedding = self.embedding_service.generate_embedding(user_message)
similar_requests = self.embedding_service.find_similar(query_embedding)
for similar_hash, similarity in similar_requests:
if similarity >= self.config.SEMANTIC_SIMILARITY_THRESHOLD:
similar_response = self.embedding_service.get_cached_response(similar_hash)
if similar_response:
self.stats["semantic_hits"] += 1
self.stats["total_tokens_saved"] += similar_response.get(
"usage", {}
).get("total_tokens", 0)
similar_response["cache_hit"] = "semantic"
similar_response["similarity_score"] = similarity
similar_response["original_hash"] = similar_hash
print(f"✓ 의미론적 캐시 히트: {similar_hash} (유사도: {similarity:.2%})")
return similar_response
# 3단계: API 호출
print(f"✗ 캐시 미스: API 호출 진행")
self.stats["cache_misses"] += 1
response = self._call_api(request_data, model)
response["request_text"] = user_message
# 캐시 저장
self._store_exact_cache(request_hash, response)
return response
def _call_api(self, request_data: Dict, model: str) -> Dict:
"""HolySheep AI API 호출"""
try:
response = self.client.chat.completions.create(
model=model,
messages=request_data["messages"],
temperature=0.7,
max_tokens=1000
)
return {
"content": response.choices[0].message.content,
"model": response.model,
"usage": {
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens
},
"cache_hit": "none"
}
except Exception as e:
print(f"API 호출 오류: {e}")
raise
def get_stats(self) -> Dict:
"""캐시 통계 반환"""
total_requests = sum(self.stats.values())
hit_rate = (
(self.stats["exact_hits"] + self.stats["semantic_hits"]) / total_requests * 100
if total_requests > 0 else 0
)
return {
**self.stats,
"total_requests": total_requests,
"hit_rate_percent": round(hit_rate, 2)
}
사용 예제
if __name__ == "__main__":
config = Config()
cache_manager = AICacheManager(config)
# 테스트 요청들
test_messages = [
"파이썬에서 리스트를 정렬하는 방법을 알려주세요",
"파이썬 리스트 정렬 방법",
"JavaScript에서 배열을 정렬하는 방법은?",
"파이썬에서 리스트를 정렬하는 방법을 알려주세요", # 중복
]
for msg in test_messages:
result = cache_manager.get_response(
user_message=msg,
system_prompt="당신은 친절한 프로그래밍 도우미입니다.",
model="gpt-4.1"
)
print(f"응답: {result['content'][:50]}...")
print(f"캐시 상태: {result['cache_hit']}\n")
# 통계 출력
print("=" * 50)
print("캐시 통계:", cache_manager.get_stats())
고급 기능: TTL 및 조건부 캐시 무효화
# advanced_cache_manager.py
from datetime import datetime, timedelta
import re
class AdvancedCacheManager(AICacheManager):
"""고급 캐시 관리 기능 추가"""
def _store_with_custom_ttl(self, request_hash: str, response: Dict,
custom_ttl: int) -> None:
"""사용자 정의 TTL로 캐시 저장"""
cache_key = f"exact:{request_hash}"
self.redis_client.set(
cache_key,
json.dumps(response),
ex=custom_ttl
)
def invalidate_pattern(self, pattern: str) -> int:
"""특정 패턴을 가진 캐시 무효화"""
count = 0
cursor = 0
while True:
cursor, keys = self.redis_client.scan(
cursor=cursor,
match=f"resp:*{pattern}*",
count=100
)
if keys:
self.redis_client.delete(*keys)
count += len(keys)
if cursor == 0:
break
return count
def update_ttl(self, request_hash: str, new_ttl: int) -> bool:
"""특정 캐시의 TTL 갱신"""
exact_key = f"exact:{request_hash}"
vector_key = f"vec:{request_hash}"
response_key = f"resp:{request_hash}"
for key in [exact_key, vector_key, response_key]:
if self.redis_client.exists(key):
self.redis_client.expire(key, new_ttl)
return True
return False
def get_cache_info(self, request_hash: str) -> Dict:
"""캐시 상세 정보 조회"""
exact_key = f"exact:{request_hash}"
ttl = self.redis_client.ttl(exact_key)
return {
"request_hash": request_hash,
"ttl_seconds": ttl if ttl > 0 else None,
"ttl_formatted": str(timedelta(seconds=ttl)) if ttl > 0 else None,
"exists": self.redis_client.exists(exact_key) > 0
}
성능 벤치마크: 캐시 히트율 측정
# benchmark.py
import time
from cache_manager import AICacheManager
from config import Config
def run_benchmark():
"""캐시 성능 벤치마크"""
config = Config()
cache_manager = AICacheManager(config)
# 테스트 시나리오
test_scenarios = [
# 50% 반복, 30% 유사, 20% 신규
["안녕하세요", "안녕하세요", "날씨 어때요?", "날씨가 어떤가요?",
"새로운 질문입니다", "안녕하세요"] * 10,
]
messages = [msg for scenario in test_scenarios for msg in scenario]
start_time = time.time()
results = []
for msg in messages:
result = cache_manager.get_response(msg, model="gpt-4.1")
results.append(result)
total_time = time.time() - start_time
stats = cache_manager.get_stats()
# 비용 절감 계산
if stats["total_tokens_saved"] > 0:
# GPT-4.1 기준 ($8/MTok)
cost_saved = (stats["total_tokens_saved"] / 1_000_000) * 8
print(f"💰 예상 비용 절감: ${cost_saved:.2f}")
print(f"\n📊 벤치마크 결과")
print(f"총 요청 수: {stats['total_requests']}")
print(f"정확한 캐시 히트: {stats['exact_hits']}")
print(f"의미론적 캐시 히트: {stats['semantic_hits']}")
print(f"캐시 미스: {stats['cache_misses']}")
print(f"전체 캐시 히트율: {stats['hit_rate_percent']}%")
print(f"총 실행 시간: {total_time:.2f}초")
if __name__ == "__main__":