""" FastAPI 브릿지 캐싱 시스템 Phase 3: Redis 캐싱 (메모리 캐시로 폴백) """ import asyncio import hashlib import json import time from typing import Any, Dict, Optional, Union from functools import wraps import redis.asyncio as redis from config import settings class CacheManager: """캐시 관리자 - Redis 우선, 메모리 캐시 폴백""" def __init__(self): self.redis_client: Optional[redis.Redis] = None self.memory_cache: Dict[str, Dict[str, Any]] = {} self.is_redis_available = False async def connect(self): """Redis 연결 시도, 실패시 메모리 캐시 사용""" try: self.redis_client = redis.from_url( settings.REDIS_URL, decode_responses=True, socket_connect_timeout=5, socket_timeout=5 ) # Redis 연결 테스트 await self.redis_client.ping() self.is_redis_available = True print("✅ Redis 연결 성공") except Exception as e: print(f"⚠️ Redis 연결 실패, 메모리 캐시 사용: {e}") self.is_redis_available = False self.redis_client = None async def disconnect(self): """연결 종료""" if self.redis_client: await self.redis_client.close() def _generate_key(self, prefix: str, *args, **kwargs) -> str: """캐시 키 생성""" # 파라미터들을 문자열로 변환 key_data = f"{prefix}:{':'.join(map(str, args))}" if kwargs: key_data += f":{json.dumps(kwargs, sort_keys=True)}" # 긴 키는 해시로 단축 if len(key_data) > 100: key_hash = hashlib.md5(key_data.encode()).hexdigest() return f"{prefix}:{key_hash}" return key_data async def get(self, key: str) -> Optional[Any]: """캐시에서 값 조회""" if self.is_redis_available and self.redis_client: try: value = await self.redis_client.get(key) if value: return json.loads(value) except Exception as e: print(f"Redis GET 오류: {e}") # 메모리 캐시 폴백 if key in self.memory_cache: cache_entry = self.memory_cache[key] if cache_entry['expires_at'] > time.time(): return cache_entry['data'] else: # 만료된 캐시 삭제 del self.memory_cache[key] return None async def set(self, key: str, value: Any, ttl: int = 300) -> bool: """캐시에 값 저장""" json_value = json.dumps(value, ensure_ascii=False) if self.is_redis_available and self.redis_client: try: await self.redis_client.setex(key, ttl, json_value) return True except Exception as e: print(f"Redis SET 오류: {e}") # 메모리 캐시 폴백 self.memory_cache[key] = { 'data': value, 'expires_at': time.time() + ttl } # 메모리 캐시 크기 제한 (최대 1000개) if len(self.memory_cache) > 1000: # 가장 오래된 10개 항목 제거 sorted_keys = sorted( self.memory_cache.keys(), key=lambda k: self.memory_cache[k]['expires_at'] ) for key_to_remove in sorted_keys[:10]: del self.memory_cache[key_to_remove] return True async def delete(self, key: str) -> bool: """캐시에서 값 삭제""" deleted = False if self.is_redis_available and self.redis_client: try: result = await self.redis_client.delete(key) deleted = result > 0 except Exception as e: print(f"Redis DELETE 오류: {e}") # 메모리 캐시에서도 삭제 if key in self.memory_cache: del self.memory_cache[key] deleted = True return deleted async def clear_pattern(self, pattern: str) -> int: """패턴 매칭으로 키 삭제""" deleted_count = 0 if self.is_redis_available and self.redis_client: try: keys = await self.redis_client.keys(pattern) if keys: deleted_count = await self.redis_client.delete(*keys) except Exception as e: print(f"Redis CLEAR_PATTERN 오류: {e}") # 메모리 캐시에서 패턴 매칭 삭제 import fnmatch keys_to_delete = [ key for key in self.memory_cache.keys() if fnmatch.fnmatch(key, pattern) ] for key in keys_to_delete: del self.memory_cache[key] deleted_count += 1 return deleted_count def get_stats(self) -> Dict[str, Any]: """캐시 통계""" stats = { "redis_available": self.is_redis_available, "memory_cache_size": len(self.memory_cache), "cache_type": "Redis" if self.is_redis_available else "Memory" } # 만료된 메모리 캐시 정리 current_time = time.time() expired_keys = [ key for key, entry in self.memory_cache.items() if entry['expires_at'] <= current_time ] for key in expired_keys: del self.memory_cache[key] stats["expired_keys_cleaned"] = len(expired_keys) return stats # 전역 캐시 매니저 cache_manager = CacheManager() def cached(prefix: str = "default", ttl: int = 300): """캐싱 데코레이터""" def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): # 캐시 키 생성 cache_key = cache_manager._generate_key(prefix, *args, **kwargs) # 캐시에서 조회 cached_result = await cache_manager.get(cache_key) if cached_result is not None: print(f"🟢 캐시 히트: {cache_key}") return cached_result # 캐시 미스 - 함수 실행 print(f"🟡 캐시 미스: {cache_key}") result = await func(*args, **kwargs) # 결과 캐싱 await cache_manager.set(cache_key, result, ttl) return result return wrapper return decorator