""" Redis 캐시 관리 유틸리티 성능 향상을 위한 캐싱 전략 구현 """ import json import redis from typing import Any, Optional, Dict, List from datetime import timedelta import hashlib import pickle from ..config import get_settings from .logger import get_logger settings = get_settings() logger = get_logger(__name__) class CacheManager: """Redis 캐시 관리 클래스""" def __init__(self): try: # Redis 연결 설정 self.redis_client = redis.from_url( settings.redis.url, decode_responses=False, # 바이너리 데이터 지원 socket_connect_timeout=5, socket_timeout=5, retry_on_timeout=True ) # 연결 테스트 self.redis_client.ping() logger.info("Redis 연결 성공") except Exception as e: logger.error(f"Redis 연결 실패: {e}") self.redis_client = None def _generate_key(self, prefix: str, *args, **kwargs) -> str: """캐시 키 생성""" # 인자들을 문자열로 변환하여 해시 생성 key_parts = [str(arg) for arg in args] key_parts.extend([f"{k}:{v}" for k, v in sorted(kwargs.items())]) if key_parts: key_hash = hashlib.md5("|".join(key_parts).encode()).hexdigest()[:8] return f"tkmp:{prefix}:{key_hash}" else: return f"tkmp:{prefix}" def get(self, key: str) -> Optional[Any]: """캐시에서 데이터 조회""" if not self.redis_client: return None try: data = self.redis_client.get(key) if data: return pickle.loads(data) return None except Exception as e: logger.warning(f"캐시 조회 실패 - key: {key}, error: {e}") return None def set(self, key: str, value: Any, expire: int = 3600) -> bool: """캐시에 데이터 저장""" if not self.redis_client: return False try: serialized_data = pickle.dumps(value) result = self.redis_client.setex(key, expire, serialized_data) logger.debug(f"캐시 저장 - key: {key}, expire: {expire}s") return result except Exception as e: logger.warning(f"캐시 저장 실패 - key: {key}, error: {e}") return False def delete(self, key: str) -> bool: """캐시에서 데이터 삭제""" if not self.redis_client: return False try: result = self.redis_client.delete(key) logger.debug(f"캐시 삭제 - key: {key}") return bool(result) except Exception as e: logger.warning(f"캐시 삭제 실패 - key: {key}, error: {e}") return False def delete_pattern(self, pattern: str) -> int: """패턴에 맞는 캐시 키들 삭제""" if not self.redis_client: return 0 try: keys = self.redis_client.keys(pattern) if keys: deleted = self.redis_client.delete(*keys) logger.info(f"패턴 캐시 삭제 - pattern: {pattern}, deleted: {deleted}") return deleted return 0 except Exception as e: logger.warning(f"패턴 캐시 삭제 실패 - pattern: {pattern}, error: {e}") return 0 def exists(self, key: str) -> bool: """캐시 키 존재 여부 확인""" if not self.redis_client: return False try: return bool(self.redis_client.exists(key)) except Exception as e: logger.warning(f"캐시 존재 확인 실패 - key: {key}, error: {e}") return False def get_ttl(self, key: str) -> int: """캐시 TTL 조회""" if not self.redis_client: return -1 try: return self.redis_client.ttl(key) except Exception as e: logger.warning(f"캐시 TTL 조회 실패 - key: {key}, error: {e}") return -1 class TKMPCache: """TK-MP 프로젝트 전용 캐시 래퍼""" def __init__(self): self.cache = CacheManager() # 캐시 TTL 설정 (초 단위) self.ttl_config = { "file_list": 300, # 5분 - 파일 목록 "material_list": 600, # 10분 - 자재 목록 "job_list": 1800, # 30분 - 작업 목록 "classification": 3600, # 1시간 - 분류 결과 "statistics": 900, # 15분 - 통계 데이터 "comparison": 1800, # 30분 - 리비전 비교 } def get_file_list(self, job_no: Optional[str] = None, show_history: bool = False) -> Optional[List[Dict]]: """파일 목록 캐시 조회""" key = self.cache._generate_key("files", job_no=job_no, history=show_history) return self.cache.get(key) def set_file_list(self, files: List[Dict], job_no: Optional[str] = None, show_history: bool = False) -> bool: """파일 목록 캐시 저장""" key = self.cache._generate_key("files", job_no=job_no, history=show_history) return self.cache.set(key, files, self.ttl_config["file_list"]) def get_material_list(self, file_id: int) -> Optional[List[Dict]]: """자재 목록 캐시 조회""" key = self.cache._generate_key("materials", file_id=file_id) return self.cache.get(key) def set_material_list(self, materials: List[Dict], file_id: int) -> bool: """자재 목록 캐시 저장""" key = self.cache._generate_key("materials", file_id=file_id) return self.cache.set(key, materials, self.ttl_config["material_list"]) def get_job_list(self) -> Optional[List[Dict]]: """작업 목록 캐시 조회""" key = self.cache._generate_key("jobs") return self.cache.get(key) def set_job_list(self, jobs: List[Dict]) -> bool: """작업 목록 캐시 저장""" key = self.cache._generate_key("jobs") return self.cache.set(key, jobs, self.ttl_config["job_list"]) def get_classification_result(self, description: str, category: str) -> Optional[Dict]: """분류 결과 캐시 조회""" key = self.cache._generate_key("classification", desc=description, cat=category) return self.cache.get(key) def set_classification_result(self, result: Dict, description: str, category: str) -> bool: """분류 결과 캐시 저장""" key = self.cache._generate_key("classification", desc=description, cat=category) return self.cache.set(key, result, self.ttl_config["classification"]) def get_statistics(self, job_no: str, stat_type: str) -> Optional[Dict]: """통계 데이터 캐시 조회""" key = self.cache._generate_key("stats", job_no=job_no, type=stat_type) return self.cache.get(key) def set_statistics(self, stats: Dict, job_no: str, stat_type: str) -> bool: """통계 데이터 캐시 저장""" key = self.cache._generate_key("stats", job_no=job_no, type=stat_type) return self.cache.set(key, stats, self.ttl_config["statistics"]) def get_revision_comparison(self, job_no: str, rev1: str, rev2: str) -> Optional[Dict]: """리비전 비교 결과 캐시 조회""" key = self.cache._generate_key("comparison", job_no=job_no, rev1=rev1, rev2=rev2) return self.cache.get(key) def set_revision_comparison(self, comparison: Dict, job_no: str, rev1: str, rev2: str) -> bool: """리비전 비교 결과 캐시 저장""" key = self.cache._generate_key("comparison", job_no=job_no, rev1=rev1, rev2=rev2) return self.cache.set(key, comparison, self.ttl_config["comparison"]) def invalidate_job_cache(self, job_no: str): """특정 작업의 모든 캐시 무효화""" patterns = [ f"tkmp:files:*job_no:{job_no}*", f"tkmp:materials:*job_no:{job_no}*", f"tkmp:stats:*job_no:{job_no}*", f"tkmp:comparison:*job_no:{job_no}*" ] total_deleted = 0 for pattern in patterns: deleted = self.cache.delete_pattern(pattern) total_deleted += deleted logger.info(f"작업 캐시 무효화 완료 - job_no: {job_no}, deleted: {total_deleted}") return total_deleted def invalidate_file_cache(self, file_id: int): """특정 파일의 모든 캐시 무효화""" patterns = [ f"tkmp:materials:*file_id:{file_id}*", f"tkmp:files:*" # 파일 목록도 갱신 필요 ] total_deleted = 0 for pattern in patterns: deleted = self.cache.delete_pattern(pattern) total_deleted += deleted logger.info(f"파일 캐시 무효화 완료 - file_id: {file_id}, deleted: {total_deleted}") return total_deleted def get_cache_info(self) -> Dict[str, Any]: """캐시 상태 정보 조회""" if not self.cache.redis_client: return {"status": "disconnected"} try: info = self.cache.redis_client.info() return { "status": "connected", "used_memory": info.get("used_memory_human", "N/A"), "connected_clients": info.get("connected_clients", 0), "total_commands_processed": info.get("total_commands_processed", 0), "keyspace_hits": info.get("keyspace_hits", 0), "keyspace_misses": info.get("keyspace_misses", 0), "hit_rate": round( info.get("keyspace_hits", 0) / max(info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0), 1) * 100, 2 ) } except Exception as e: logger.error(f"캐시 정보 조회 실패: {e}") return {"status": "error", "error": str(e)} # 전역 캐시 인스턴스 tkmp_cache = TKMPCache()