Files
TK-BOM-Project/backend/app/utils/cache_manager.py
Hyungi Ahn 4f8e395f87
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: SWG 가스켓 전체 구성 정보 표시 개선
- H/F/I/O SS304/GRAPHITE/CS/CS 패턴에서 4개 구성요소 모두 표시
- 기존 SS304 + GRAPHITE → SS304/GRAPHITE/CS/CS로 완전한 구성 표시
- 외부링/필러/내부링/추가구성 모든 정보 포함
- 구매수량 계산 모달에서 정확한 재질 정보 확인 가능
2025-08-30 14:23:01 +09:00

267 lines
10 KiB
Python

"""
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()