# app/utils/redis_client.py """ HealthSync AI Redis 캐시 클라이언트 (Azure Cache for Redis 지원) """ import redis.asyncio as redis import json import logging from typing import Any, Optional, List from app.config.settings import settings logger = logging.getLogger(__name__) class RedisClient: """Redis 캐시 연동 클라이언트 (Azure Cache for Redis 지원)""" def __init__(self): self.redis_host = settings.redis_host self.redis_port = settings.redis_port self.redis_password = settings.redis_password self.redis_db = settings.redis_db self.default_ttl = settings.redis_cache_ttl self.client = None self._connected = False async def connect(self): """Azure Cache for Redis 연결""" if self._connected: return try: # Azure Cache for Redis 연결 설정 if self.redis_password and self.redis_password != "": # Azure Cache for Redis (SSL + 인증) self.client = redis.Redis( host=self.redis_host, port=self.redis_port, password=self.redis_password, db=self.redis_db, ssl=True, # Azure Cache는 SSL 필수 ssl_cert_reqs=None, # SSL 인증서 검증 비활성화 decode_responses=True, socket_timeout=10, socket_connect_timeout=10, retry_on_timeout=True, health_check_interval=30 ) logger.info(f"🔐 Azure Cache for Redis 연결 시도 - {self.redis_host}:{self.redis_port}") else: # 로컬 Redis (비SSL) self.client = redis.Redis( host=self.redis_host, port=self.redis_port, db=self.redis_db, decode_responses=True, socket_timeout=5, socket_connect_timeout=5 ) logger.info(f"🔓 로컬 Redis 연결 시도 - {self.redis_host}:{self.redis_port}") # 연결 테스트 await self.client.ping() self._connected = True logger.info(f"✅ Redis 클라이언트 연결 완료 - {self.redis_host}:{self.redis_port}") except redis.AuthenticationError as e: logger.error(f"❌ Redis 인증 실패 - 패스워드 확인 필요: {str(e)}") raise Exception(f"Redis 인증 실패: {str(e)}") except redis.ConnectionError as e: logger.error(f"❌ Redis 연결 실패 - 호스트/포트 확인 필요: {str(e)}") raise Exception(f"Redis 연결 실패: {str(e)}") except redis.TimeoutError as e: logger.error(f"❌ Redis 연결 타임아웃: {str(e)}") raise Exception(f"Redis 연결 타임아웃: {str(e)}") except Exception as e: logger.error(f"❌ Redis 연결 실패: {str(e)}") raise Exception(f"Redis 연결 실패: {str(e)}") async def disconnect(self): """Redis 연결 해제""" if self.client and self._connected: try: await self.client.aclose() # redis.asyncio의 올바른 종료 메소드 self._connected = False logger.info("✅ Redis 연결 해제 완료") except Exception as e: logger.error(f"❌ Redis 연결 해제 실패: {str(e)}") async def get(self, key: str) -> Optional[Any]: """캐시에서 값 조회""" try: if not self._connected: await self.connect() value = await self.client.get(key) if value: result = json.loads(value) logger.info(f"✅ 캐시 조회 성공 - key: {key}") return result else: logger.info(f"❌ 캐시 미스 - key: {key}") return None except json.JSONDecodeError as e: logger.error(f"❌ JSON 파싱 실패 - key: {key}, error: {str(e)}") return None except redis.ConnectionError as e: logger.error(f"❌ Redis 연결 오류 - key: {key}, error: {str(e)}") return None except Exception as e: logger.error(f"❌ 캐시 조회 실패 - key: {key}, error: {str(e)}") return None async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool: """캐시에 값 저장""" try: if not self._connected: await self.connect() ttl = ttl or self.default_ttl json_value = json.dumps(value, ensure_ascii=False, default=str) await self.client.setex(key, ttl, json_value) logger.info(f"✅ 캐시 저장 성공 - key: {key}, ttl: {ttl}s") return True except redis.ConnectionError as e: logger.error(f"❌ Redis 연결 오류 - key: {key}, error: {str(e)}") return False except Exception as e: logger.error(f"❌ 캐시 저장 실패 - key: {key}, error: {str(e)}") return False async def delete(self, key: str) -> bool: """캐시에서 값 삭제""" try: if not self._connected: await self.connect() result = await self.client.delete(key) if result: logger.info(f"✅ 캐시 삭제 성공 - key: {key}") return True else: logger.info(f"❌ 캐시 삭제 실패 (키 없음) - key: {key}") return False except redis.ConnectionError as e: logger.error(f"❌ Redis 연결 오류 - key: {key}, error: {str(e)}") return False except Exception as e: logger.error(f"❌ 캐시 삭제 실패 - key: {key}, error: {str(e)}") return False async def get_or_set(self, key: str, fetch_func, ttl: Optional[int] = None) -> Any: """캐시 조회 후 없으면 fetch_func 실행하여 저장 (Cache Aside 패턴)""" try: # 1. 캐시에서 조회 cached_value = await self.get(key) if cached_value is not None: return cached_value # 2. 캐시 미스 시 데이터 fetch fresh_value = await fetch_func() if fresh_value is not None: # 3. 캐시에 저장 await self.set(key, fresh_value, ttl) return fresh_value return None except Exception as e: logger.error(f"❌ Cache Aside 패턴 실행 실패 - key: {key}, error: {str(e)}") # 캐시 실패 시에도 fresh_value 반환 시도 try: return await fetch_func() except: return None def generate_similar_users_key(self, user_id: int) -> str: """유사 사용자 캐시 키 생성""" return f"similar_users:{user_id}" def generate_mission_news_key(self, user_id: int) -> str: """미션 소식 캐시 키 생성 (짧은 TTL용)""" return f"mission_news:{user_id}" async def test_connection(self) -> dict: """Redis 연결 테스트""" try: if not self._connected: await self.connect() # 기본 명령 테스트 test_key = "healthsync:connection_test" test_value = "test_connection_success" await self.client.set(test_key, test_value, ex=10) # 10초 TTL retrieved_value = await self.client.get(test_key) await self.client.delete(test_key) info = await self.client.info() return { "status": "connected", "host": self.redis_host, "port": self.redis_port, "ssl_enabled": bool(self.redis_password), "test_result": retrieved_value == test_value, "redis_version": info.get("redis_version", "unknown"), "connected_clients": info.get("connected_clients", 0), "used_memory_human": info.get("used_memory_human", "unknown") } except Exception as e: return { "status": "failed", "error": str(e), "error_type": type(e).__name__, "host": self.redis_host, "port": self.redis_port } # 전역 클라이언트 인스턴스 redis_client = RedisClient()