KR_Redis - somaz94/python-study GitHub Wiki

Python Redis κ°œλ… 정리


1️⃣ Redis 기초

RedisλŠ” 인메λͺ¨λ¦¬ 데이터 ꡬ쑰 μ €μž₯μ†Œλ‘œ, κ³ μ„±λŠ₯ 캐싱, λ©”μ‹œμ§€ 브둜컀 및 μ‹€μ‹œκ°„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 데이터 μ €μž₯μ†Œλ‘œ 널리 μ‚¬μš©λœλ‹€. Pythonμ—μ„œλŠ” redis-py 라이브러리λ₯Ό 톡해 μ‰½κ²Œ μ ‘κ·Όν•˜κ³  ν™œμš©ν•  수 μžˆλ‹€.

import redis
from typing import Optional, Any, Dict, List, Union
import json
import time
import logging
from contextlib import contextmanager

# Redis μ—°κ²° 클래슀
class RedisClient:
    """Redis μ„œλ²„ μ—°κ²° 및 관리λ₯Ό μœ„ν•œ ν΄λΌμ΄μ–ΈνŠΈ 클래슀"""
    
    def __init__(
        self,
        host: str = 'localhost',
        port: int = 6379,
        db: int = 0,
        password: Optional[str] = None,
        socket_timeout: float = 5.0,
        socket_connect_timeout: float = 5.0,
        socket_keepalive: bool = True,
        retry_on_timeout: bool = True,
        health_check_interval: int = 30,
        decode_responses: bool = True
    ):
        """
        Redis ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™”
        
        Args:
            host: Redis μ„œλ²„ 호슀트
            port: Redis μ„œλ²„ 포트
            db: μ‚¬μš©ν•  λ°μ΄ν„°λ² μ΄μŠ€ 번호
            password: 인증 λΉ„λ°€λ²ˆν˜Έ
            socket_timeout: μ†ŒμΌ“ μž‘μ—… νƒ€μž„μ•„μ›ƒ(초)
            socket_connect_timeout: μ—°κ²° νƒ€μž„μ•„μ›ƒ(초)
            socket_keepalive: μ†ŒμΌ“ keepalive μ„€μ •
            retry_on_timeout: νƒ€μž„μ•„μ›ƒ μ‹œ μž¬μ‹œλ„ μ—¬λΆ€
            health_check_interval: μƒνƒœ 확인 간격(초)
            decode_responses: 응닡 μžλ™ λ””μ½”λ”© μ—¬λΆ€
        """
        self.logger = logging.getLogger(__name__)
        
        # μ—°κ²° μ„€μ •
        self.connection_params = {
            'host': host,
            'port': port,
            'db': db,
            'password': password,
            'socket_timeout': socket_timeout,
            'socket_connect_timeout': socket_connect_timeout,
            'socket_keepalive': socket_keepalive,
            'retry_on_timeout': retry_on_timeout,
            'health_check_interval': health_check_interval,
            'decode_responses': decode_responses
        }
        
        self.redis = self._create_connection()
    
    def _create_connection(self):
        """Redis μ—°κ²° 생성"""
        try:
            self.logger.info(f"Redis μ„œλ²„ {self.connection_params['host']}:{self.connection_params['port']} μ—°κ²° μ‹œλ„...")
            connection = redis.Redis(**self.connection_params)
            return connection
        except redis.ConnectionError as e:
            self.logger.error(f"Redis μ—°κ²° μ‹€νŒ¨: {e}")
            raise
    
    def ping(self) -> bool:
        """
        Redis μ„œλ²„ μ—°κ²° ν…ŒμŠ€νŠΈ
        
        Returns:
            bool: μ—°κ²° 성곡 μ—¬λΆ€
        """
        try:
            return self.redis.ping()
        except redis.ConnectionError as e:
            self.logger.error(f"Redis μ„œλ²„ μ—°κ²° ν…ŒμŠ€νŠΈ μ‹€νŒ¨: {e}")
            return False
    
    def info(self) -> Dict:
        """
        Redis μ„œλ²„ 정보 쑰회
        
        Returns:
            Dict: Redis μ„œλ²„ 정보
        """
        try:
            return self.redis.info()
        except redis.RedisError as e:
            self.logger.error(f"Redis μ„œλ²„ 정보 쑰회 μ‹€νŒ¨: {e}")
            return {}
    
    def close(self):
        """μ—°κ²° μ’…λ£Œ"""
        try:
            self.redis.close()
            self.logger.info("Redis μ—°κ²° μ’…λ£Œ")
        except Exception as e:
            self.logger.error(f"Redis μ—°κ²° μ’…λ£Œ 쀑 였λ₯˜: {e}")
    
    @contextmanager
    def pipeline_context(self, transaction: bool = True):
        """
        νŒŒμ΄ν”„λΌμΈ μ»¨ν…μŠ€νŠΈ λ§€λ‹ˆμ €
        
        μ‚¬μš© 예:
        ```
        with client.pipeline_context() as pipe:
            pipe.set('key1', 'value1')
            pipe.set('key2', 'value2')
            results = pipe.execute()
        ```
        
        Args:
            transaction: νŠΈλžœμž­μ…˜ μ‚¬μš© μ—¬λΆ€
            
        Yields:
            Pipeline: Redis νŒŒμ΄ν”„λΌμΈ 객체
        """
        pipeline = self.redis.pipeline(transaction=transaction)
        try:
            yield pipeline
        finally:
            pipeline.execute()
    
    def flush_db(self, force: bool = False):
        """
        ν˜„μž¬ λ°μ΄ν„°λ² μ΄μŠ€μ˜ λͺ¨λ“  ν‚€ μ‚­μ œ
        
        Args:
            force: κ°•μ œ μ‚­μ œ μ—¬λΆ€ (μ•ˆμ „μž₯치)
            
        Returns:
            bool: 성곡 μ—¬λΆ€
        """
        if not force:
            self.logger.warning("flush_db 호좜 μ‹œ force=True μ˜΅μ…˜μ΄ ν•„μš”ν•˜λ‹€")
            return False
            
        try:
            self.redis.flushdb()
            self.logger.info(f"λ°μ΄ν„°λ² μ΄μŠ€ {self.connection_params['db']} 비움")
            return True
        except redis.RedisError as e:
            self.logger.error(f"λ°μ΄ν„°λ² μ΄μŠ€ λΉ„μš°κΈ° μ‹€νŒ¨: {e}")
            return False

βœ… νŠΉμ§•:

  • μ•ˆμ •μ μΈ μ—°κ²° μ„€μ • 및 관리
  • λ‹€μ–‘ν•œ μ—°κ²° μ˜΅μ…˜ 지원
  • νŒŒμ΄ν”„λΌμΈμ„ ν†΅ν•œ μ„±λŠ₯ μ΅œμ ν™”
  • μžλ™ 응닡 λ””μ½”λ”© μ„€μ •
  • νƒ€μž… νžŒνŒ…μœΌλ‘œ μ½”λ“œ 가독성 ν–₯상
  • μ»¨ν…μŠ€νŠΈ λ§€λ‹ˆμ €λ₯Ό ν†΅ν•œ μžμ› 관리
  • μƒμ„Έν•œ λ‘œκΉ…μœΌλ‘œ μ—°κ²° 문제 좔적
  • μ„œλ²„ μƒνƒœ 확인 (ping, info)
  • μ•ˆμ „ν•œ λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” λ©”μ»€λ‹ˆμ¦˜


2️⃣ κΈ°λ³Έ μž‘μ—…

RedisλŠ” λ¬Έμžμ—΄, ν•΄μ‹œ, 리슀트, μ§‘ν•©, μ •λ ¬ μ§‘ν•© λ“± λ‹€μ–‘ν•œ 데이터 ꡬ쑰λ₯Ό μ œκ³΅ν•œλ‹€. 각 데이터 νƒ€μž…μ— λ§žλŠ” 효율적인 μž‘μ—… 방법을 톡해 λ‹€μ–‘ν•œ μ‚¬μš© 사둀λ₯Ό κ΅¬ν˜„ν•  수 μžˆλ‹€.

class RedisOperations:
    """Redis κΈ°λ³Έ 데이터 νƒ€μž…λ³„ μž‘μ—… 클래슀"""
    
    def __init__(self, client: RedisClient):
        """
        Redis μž‘μ—… 클래슀 μ΄ˆκΈ°ν™”
        
        Args:
            client: Redis ν΄λΌμ΄μ–ΈνŠΈ μΈμŠ€ν„΄μŠ€
        """
        self.redis = client.redis
        self.logger = logging.getLogger(__name__)
    
    # λ¬Έμžμ—΄ μž‘μ—…
    def set_value(
        self, 
        key: str, 
        value: str, 
        expiry: Optional[int] = None,
        nx: bool = False,
        xx: bool = False
    ) -> bool:
        """
        ν‚€-κ°’ μ €μž₯
        
        Args:
            key: ν‚€
            value: κ°’
            expiry: 만료 μ‹œκ°„(초)
            nx: ν‚€κ°€ μ‘΄μž¬ν•˜μ§€ μ•Šμ„ λ•Œλ§Œ μ„€μ •
            xx: ν‚€κ°€ 이미 μ‘΄μž¬ν•  λ•Œλ§Œ μ„€μ •
            
        Returns:
            bool: μ„€μ • 성곡 μ—¬λΆ€
        """
        try:
            return self.redis.set(key, value, ex=expiry, nx=nx, xx=xx)
        except redis.RedisError as e:
            self.logger.error(f"κ°’ μ„€μ • μ‹€νŒ¨ - ν‚€: {key}, 였λ₯˜: {e}")
            return False
    
    def get_value(self, key: str) -> Optional[str]:
        """
        κ°’ 쑰회
        
        Args:
            key: ν‚€
            
        Returns:
            Optional[str]: μ €μž₯된 κ°’ λ˜λŠ” None
        """
        try:
            return self.redis.get(key)
        except redis.RedisError as e:
            self.logger.error(f"κ°’ 쑰회 μ‹€νŒ¨ - ν‚€: {key}, 였λ₯˜: {e}")
            return None
            
    def increment(self, key: str, amount: int = a1) -> Optional[int]:
        """
        숫자 κ°’ 증가
        
        Args:
            key: ν‚€
            amount: μ¦κ°€λŸ‰
            
        Returns:
            Optional[int]: 증가 ν›„ κ°’ λ˜λŠ” None
        """
        try:
            return self.redis.incrby(key, amount)
        except redis.RedisError as e:
            self.logger.error(f"증뢄 μ‹€νŒ¨ - ν‚€: {key}, 였λ₯˜: {e}")
            return None
    
    def set_with_ttl(self, key: str, value: str, ttl: int) -> bool:
        """
        TTL(Time-To-Live)κ³Ό ν•¨κ»˜ κ°’ μ„€μ •
        
        Args:
            key: ν‚€
            value: κ°’
            ttl: 만료 μ‹œκ°„(초)
            
        Returns:
            bool: μ„€μ • 성곡 μ—¬λΆ€
        """
        try:
            return self.redis.setex(key, ttl, value)
        except redis.RedisError as e:
            self.logger.error(f"TTLκ³Ό ν•¨κ»˜ κ°’ μ„€μ • μ‹€νŒ¨ - ν‚€: {key}, 였λ₯˜: {e}")
            return False
    
    def set_json(self, key: str, data: Dict, expiry: Optional[int] = None) -> bool:
        """
        JSON 데이터 μ €μž₯
        
        Args:
            key: ν‚€
            data: JSON으둜 직렬화할 데이터
            expiry: 만료 μ‹œκ°„(초)
            
        Returns:
            bool: μ„€μ • 성곡 μ—¬λΆ€
        """
        try:
            json_data = json.dumps(data)
            return self.set_value(key, json_data, expiry)
        except (redis.RedisError, TypeError) as e:
            self.logger.error(f"JSON 데이터 μ €μž₯ μ‹€νŒ¨ - ν‚€: {key}, 였λ₯˜: {e}")
            return False
    
    def get_json(self, key: str) -> Optional[Dict]:
        """
        JSON 데이터 쑰회
        
        Args:
            key: ν‚€
            
        Returns:
            Optional[Dict]: μ—­μ§λ ¬ν™”λœ 데이터 λ˜λŠ” None
        """
        try:
            json_data = self.get_value(key)
            if json_data:
                return json.loads(json_data)
            return None
        except (redis.RedisError, json.JSONDecodeError) as e:
            self.logger.error(f"JSON 데이터 쑰회 μ‹€νŒ¨ - ν‚€: {key}, 였λ₯˜: {e}")
            return None
    
    # ν•΄μ‹œ μž‘μ—…
    def set_hash(self, name: str, mapping: Dict) -> bool:
        """
        ν•΄μ‹œ μ €μž₯
        
        Args:
            name: ν•΄μ‹œ 이름
            mapping: ν•„λ“œ-κ°’ λ§€ν•‘
            
        Returns:
            bool: μ„€μ • 성곡 μ—¬λΆ€
        """
        try:
            self.redis.hset(name, mapping=mapping)
            return True
        except redis.RedisError as e:
            self.logger.error(f"ν•΄μ‹œ μ €μž₯ μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return False
    
    def get_hash(self, name: str) -> Dict:
        """
        ν•΄μ‹œ 전체 쑰회
        
        Args:
            name: ν•΄μ‹œ 이름
            
        Returns:
            Dict: ν•΄μ‹œμ˜ λͺ¨λ“  ν•„λ“œ-κ°’ 쌍
        """
        try:
            return self.redis.hgetall(name)
        except redis.RedisError as e:
            self.logger.error(f"ν•΄μ‹œ 쑰회 μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return {}
    
    def get_hash_field(self, name: str, field: str) -> Optional[str]:
        """
        ν•΄μ‹œ ν•„λ“œ κ°’ 쑰회
        
        Args:
            name: ν•΄μ‹œ 이름
            field: ν•„λ“œ 이름
            
        Returns:
            Optional[str]: ν•„λ“œ κ°’ λ˜λŠ” None
        """
        try:
            return self.redis.hget(name, field)
        except redis.RedisError as e:
            self.logger.error(f"ν•΄μ‹œ ν•„λ“œ 쑰회 μ‹€νŒ¨ - 이름: {name}, ν•„λ“œ: {field}, 였λ₯˜: {e}")
            return None
    
    def increment_hash_field(self, name: str, field: str, amount: int = 1) -> Optional[int]:
        """
        ν•΄μ‹œ ν•„λ“œ κ°’ 증가
        
        Args:
            name: ν•΄μ‹œ 이름
            field: ν•„λ“œ 이름
            amount: μ¦κ°€λŸ‰
            
        Returns:
            Optional[int]: 증가 ν›„ κ°’ λ˜λŠ” None
        """
        try:
            return self.redis.hincrby(name, field, amount)
        except redis.RedisError as e:
            self.logger.error(f"ν•΄μ‹œ ν•„λ“œ 증뢄 μ‹€νŒ¨ - 이름: {name}, ν•„λ“œ: {field}, 였λ₯˜: {e}")
            return None
    
    # 리슀트 μž‘μ—…
    def push_to_list(self, name: str, *values: str) -> Optional[int]:
        """
        λ¦¬μŠ€νŠΈμ— κ°’ μΆ”κ°€ (였λ₯Έμͺ½)
        
        Args:
            name: 리슀트 이름
            *values: μΆ”κ°€ν•  κ°’λ“€
            
        Returns:
            Optional[int]: μΆ”κ°€ ν›„ 리슀트 길이 λ˜λŠ” None
        """
        try:
            return self.redis.rpush(name, *values)
        except redis.RedisError as e:
            self.logger.error(f"리슀트 μΆ”κ°€ μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return None
    
    def push_to_list_left(self, name: str, *values: str) -> Optional[int]:
        """
        λ¦¬μŠ€νŠΈμ— κ°’ μΆ”κ°€ (μ™Όμͺ½)
        
        Args:
            name: 리슀트 이름
            *values: μΆ”κ°€ν•  κ°’λ“€
            
        Returns:
            Optional[int]: μΆ”κ°€ ν›„ 리슀트 길이 λ˜λŠ” None
        """
        try:
            return self.redis.lpush(name, *values)
        except redis.RedisError as e:
            self.logger.error(f"리슀트 μ™Όμͺ½ μΆ”κ°€ μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return None
    
    def pop_from_list(self, name: str) -> Optional[str]:
        """
        λ¦¬μŠ€νŠΈμ—μ„œ κ°’ κΊΌλ‚΄κΈ° (였λ₯Έμͺ½)
        
        Args:
            name: 리슀트 이름
            
        Returns:
            Optional[str]: κΊΌλ‚Έ κ°’ λ˜λŠ” None
        """
        try:
            return self.redis.rpop(name)
        except redis.RedisError as e:
            self.logger.error(f"리슀트 κΊΌλ‚΄κΈ° μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return None
    
    def pop_from_list_left(self, name: str) -> Optional[str]:
        """
        λ¦¬μŠ€νŠΈμ—μ„œ κ°’ κΊΌλ‚΄κΈ° (μ™Όμͺ½)
        
        Args:
            name: 리슀트 이름
            
        Returns:
            Optional[str]: κΊΌλ‚Έ κ°’ λ˜λŠ” None
        """
        try:
            return self.redis.lpop(name)
        except redis.RedisError as e:
            self.logger.error(f"리슀트 μ™Όμͺ½ κΊΌλ‚΄κΈ° μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return None
    
    def get_list(self, name: str, start: int = 0, end: int = -1) -> List[str]:
        """
        리슀트 λ²”μœ„ 쑰회
        
        Args:
            name: 리슀트 이름
            start: μ‹œμž‘ 인덱슀
            end: μ’…λ£Œ 인덱슀 (-1은 λκΉŒμ§€)
            
        Returns:
            List[str]: 리슀트 μš”μ†Œλ“€
        """
        try:
            return self.redis.lrange(name, start, end)
        except redis.RedisError as e:
            self.logger.error(f"리슀트 쑰회 μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return []
    
    def get_list_length(self, name: str) -> int:
        """
        리슀트 길이 쑰회
        
        Args:
            name: 리슀트 이름
            
        Returns:
            int: 리슀트 길이
        """
        try:
            return self.redis.llen(name)
        except redis.RedisError as e:
            self.logger.error(f"리슀트 길이 쑰회 μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return 0
    
    # μ§‘ν•© μž‘μ—…
    def add_to_set(self, name: str, *values: str) -> Optional[int]:
        """
        집합에 κ°’ μΆ”κ°€
        
        Args:
            name: μ§‘ν•© 이름
            *values: μΆ”κ°€ν•  κ°’λ“€
            
        Returns:
            Optional[int]: μƒˆλ‘œ μΆ”κ°€λœ κ°’μ˜ 수 λ˜λŠ” None
        """
        try:
            return self.redis.sadd(name, *values)
        except redis.RedisError as e:
            self.logger.error(f"μ§‘ν•© μΆ”κ°€ μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return None
    
    def get_set_members(self, name: str) -> List[str]:
        """
        μ§‘ν•© 멀버 쑰회
        
        Args:
            name: μ§‘ν•© 이름
            
        Returns:
            List[str]: μ§‘ν•© 멀버 λͺ©λ‘
        """
        try:
            return list(self.redis.smembers(name))
        except redis.RedisError as e:
            self.logger.error(f"μ§‘ν•© 멀버 쑰회 μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return []
    
    def is_member(self, name: str, value: str) -> bool:
        """
        값이 μ§‘ν•©μ˜ 멀버인지 확인
        
        Args:
            name: μ§‘ν•© 이름
            value: 확인할 κ°’
            
        Returns:
            bool: 멀버 μ—¬λΆ€
        """
        try:
            return self.redis.sismember(name, value)
        except redis.RedisError as e:
            self.logger.error(f"μ§‘ν•© 멀버 확인 μ‹€νŒ¨ - 이름: {name}, κ°’: {value}, 였λ₯˜: {e}")
            return False
    
    def remove_from_set(self, name: str, *values: str) -> Optional[int]:
        """
        μ§‘ν•©μ—μ„œ κ°’ 제거
        
        Args:
            name: μ§‘ν•© 이름
            *values: μ œκ±°ν•  κ°’λ“€
            
        Returns:
            Optional[int]: 제거된 κ°’μ˜ 수 λ˜λŠ” None
        """
        try:
            return self.redis.srem(name, *values)
        except redis.RedisError as e:
            self.logger.error(f"μ§‘ν•© 제거 μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return None
    
    # μ •λ ¬λœ μ§‘ν•© μž‘μ—…
    def add_to_sorted_set(self, name: str, mapping: Dict[str, float]) -> Optional[int]:
        """
        μ •λ ¬λœ 집합에 κ°’ μΆ”κ°€
        
        Args:
            name: μ •λ ¬λœ μ§‘ν•© 이름
            mapping: κ°’-점수 λ§€ν•‘
            
        Returns:
            Optional[int]: μƒˆλ‘œ μΆ”κ°€λœ κ°’μ˜ 수 λ˜λŠ” None
        """
        try:
            return self.redis.zadd(name, mapping)
        except redis.RedisError as e:
            self.logger.error(f"μ •λ ¬λœ μ§‘ν•© μΆ”κ°€ μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return None
    
    def get_sorted_set_range(self, name: str, start: int = 0, end: int = -1, withscores: bool = False) -> Union[List[str], List[tuple]]:
        """
        μ •λ ¬λœ μ§‘ν•© λ²”μœ„ 쑰회 (점수 μ˜€λ¦„μ°¨μˆœ)
        
        Args:
            name: μ •λ ¬λœ μ§‘ν•© 이름
            start: μ‹œμž‘ 인덱슀
            end: μ’…λ£Œ 인덱슀 (-1은 λκΉŒμ§€)
            withscores: 점수 포함 μ—¬λΆ€
            
        Returns:
            Union[List[str], List[tuple]]: κ°’ λͺ©λ‘ λ˜λŠ” (κ°’, 점수) νŠœν”Œ λͺ©λ‘
        """
        try:
            return self.redis.zrange(name, start, end, withscores=withscores)
        except redis.RedisError as e:
            self.logger.error(f"μ •λ ¬λœ μ§‘ν•© λ²”μœ„ 쑰회 μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return []
    
    def get_sorted_set_rev_range(self, name: str, start: int = 0, end: int = -1, withscores: bool = False) -> Union[List[str], List[tuple]]:
        """
        μ •λ ¬λœ μ§‘ν•© λ²”μœ„ 쑰회 (점수 λ‚΄λ¦Όμ°¨μˆœ)
        
        Args:
            name: μ •λ ¬λœ μ§‘ν•© 이름
            start: μ‹œμž‘ 인덱슀
            end: μ’…λ£Œ 인덱슀 (-1은 λκΉŒμ§€)
            withscores: 점수 포함 μ—¬λΆ€
            
        Returns:
            Union[List[str], List[tuple]]: κ°’ λͺ©λ‘ λ˜λŠ” (κ°’, 점수) νŠœν”Œ λͺ©λ‘
        """
        try:
            return self.redis.zrevrange(name, start, end, withscores=withscores)
        except redis.RedisError as e:
            self.logger.error(f"μ •λ ¬λœ μ§‘ν•© μ—­μˆœ λ²”μœ„ 쑰회 μ‹€νŒ¨ - 이름: {name}, 였λ₯˜: {e}")
            return []
    
    def increment_score(self, name: str, value: str, amount: float = 1.0) -> Optional[float]:
        """
        μ •λ ¬λœ μ§‘ν•© λ©€λ²„μ˜ 점수 증가
        
        Args:
            name: μ •λ ¬λœ μ§‘ν•© 이름
            value: κ°’
            amount: μ¦κ°€λŸ‰
            
        Returns:
            Optional[float]: μƒˆ 점수 λ˜λŠ” None
        """
        try:
            return self.redis.zincrby(name, amount, value)
        except redis.RedisError as e:
            self.logger.error(f"μ •λ ¬λœ μ§‘ν•© 점수 증가 μ‹€νŒ¨ - 이름: {name}, κ°’: {value}, 였λ₯˜: {e}")
            return None
    
    # ν‚€ 관리
    def delete_key(self, *names: str) -> int:
        """
        ν‚€ μ‚­μ œ
        
        Args:
            *names: μ‚­μ œν•  ν‚€ 이름듀
            
        Returns:
            int: μ‚­μ œλœ ν‚€μ˜ 수
        """
        try:
            return self.redis.delete(*names)
        except redis.RedisError as e:
            self.logger.error(f"ν‚€ μ‚­μ œ μ‹€νŒ¨ - ν‚€: {names}, 였λ₯˜: {e}")
            return 0
    
    def set_expiry(self, name: str, time_seconds: int) -> bool:
        """
        ν‚€ 만료 μ‹œκ°„ μ„€μ •
        
        Args:
            name: ν‚€ 이름
            time_seconds: 만료 μ‹œκ°„(초)
            
        Returns:
            bool: μ„€μ • 성곡 μ—¬λΆ€
        """
        try:
            return self.redis.expire(name, time_seconds)
        except redis.RedisError as e:
            self.logger.error(f"만료 μ‹œκ°„ μ„€μ • μ‹€νŒ¨ - ν‚€: {name}, 였λ₯˜: {e}")
            return False
    
    def get_ttl(self, name: str) -> int:
        """
        ν‚€ 만료 μ‹œκ°„ 쑰회
        
        Args:
            name: ν‚€ 이름
            
        Returns:
            int: 남은 μ‹œκ°„(초), -1은 영ꡬ적, -2λŠ” ν‚€ μ—†μŒ
        """
        try:
            return self.redis.ttl(name)
        except redis.RedisError as e:
            self.logger.error(f"TTL 쑰회 μ‹€νŒ¨ - ν‚€: {name}, 였λ₯˜: {e}")
            return -2
    
    def exists(self, *names: str) -> int:
        """
        ν‚€ 쑴재 μ—¬λΆ€ 확인
        
        Args:
            *names: 확인할 ν‚€ 이름듀
            
        Returns:
            int: μ‘΄μž¬ν•˜λŠ” ν‚€μ˜ 수
        """
        try:
            return self.redis.exists(*names)
        except redis.RedisError as e:
            self.logger.error(f"ν‚€ 쑴재 확인 μ‹€νŒ¨ - ν‚€: {names}, 였λ₯˜: {e}")
            return 0

βœ… νŠΉμ§•:

  • λͺ¨λ“  Redis 데이터 νƒ€μž… 지원 (λ¬Έμžμ—΄, ν•΄μ‹œ, 리슀트, μ§‘ν•©, μ •λ ¬ μ§‘ν•©)
  • 각 데이터 νƒ€μž…λ³„ μ „μš© λ©”μ„œλ“œ
  • μΌκ΄€λœ 였λ₯˜ μ²˜λ¦¬μ™€ λ‘œκΉ…
  • JSON 데이터 직렬화/역직렬화 지원
  • TTL(Time-To-Live) μ„€μ • 및 관리
  • μ›μžμ  증뢄 μ—°μ‚° 지원
  • λ‹€μ–‘ν•œ 리슀트 μž‘μ—… (μ–‘λ°©ν–₯ μΆ”κ°€/제거)
  • μ§‘ν•© μ—°μ‚° 지원
  • μ •λ ¬λœ μ§‘ν•© 관리 (μˆœμœ„, 점수 기반)
  • ν‚€ 관리 κΈ°λŠ₯ (μ‚­μ œ, 만료 μ„€μ •, 쑴재 확인)


3️⃣ 캐싱

RedisλŠ” λΉ λ₯Έ μ ‘κ·Ό 속도와 TTL κΈ°λŠ₯을 톡해 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ„±λŠ₯을 ν–₯μƒμ‹œν‚€λŠ” 효과적인 캐싱 μ‹œμŠ€ν…œμœΌλ‘œ ν™œμš©λœλ‹€. ν•¨μˆ˜ κ²°κ³Όλ‚˜ λ°μ΄ν„°λ² μ΄μŠ€ 쿼리 κ²°κ³Ό 등을 μΊμ‹±ν•˜μ—¬ 반볡적인 κ³„μ‚°μ΄λ‚˜ IO μž‘μ—…μ„ μ΅œμ†Œν™”ν•  수 μžˆλ‹€.

from functools import wraps
import pickle
import hashlib
from datetime import timedelta
import inspect
import time

class RedisCache:
    """Redisλ₯Ό ν™œμš©ν•œ 캐싱 κ΅¬ν˜„ 클래슀"""
    
    def __init__(self, client: RedisClient, prefix: str = "cache"):
        """
        Redis μΊμ‹œ μ΄ˆκΈ°ν™”
        
        Args:
            client: Redis ν΄λΌμ΄μ–ΈνŠΈ μΈμŠ€ν„΄μŠ€
            prefix: μΊμ‹œ ν‚€ 접두사
        """
        self.redis = client.redis
        self.prefix = prefix
        self.logger = logging.getLogger(__name__)
    
    def _make_cache_key(self, func_name: str, args: tuple, kwargs: dict) -> str:
        """
        ν•¨μˆ˜ ν˜ΈμΆœμ— λŒ€ν•œ μΊμ‹œ ν‚€ 생성
        
        Args:
            func_name: ν•¨μˆ˜ 이름
            args: μœ„μΉ˜ 인자
            kwargs: ν‚€μ›Œλ“œ 인자
            
        Returns:
            str: μƒμ„±λœ μΊμ‹œ ν‚€
        """
        # 인자λ₯Ό μ •λ ¬ν•˜μ—¬ μΌκ΄€λœ ν‚€ 생성
        key_parts = [func_name, str(args)]
        
        if kwargs:
            sorted_items = sorted(kwargs.items())
            key_parts.append(str(sorted_items))
        
        # SHA-256 ν•΄μ‹œ 생성
        key_str = "::".join(key_parts)
        hashed = hashlib.sha256(key_str.encode()).hexdigest()
        
        return f"{self.prefix}:{func_name}:{hashed}"
    
    def cache_decorator(self, expiry: int = 3600):
        """
        ν•¨μˆ˜ κ²°κ³Όλ₯Ό μΊμ‹œν•˜λŠ” λ°μ½”λ ˆμ΄ν„°
        
        Args:
            expiry: μΊμ‹œ 만료 μ‹œκ°„(초)
            
        Returns:
            callable: λ°μ½”λ ˆμ΄ν„° ν•¨μˆ˜
        """
        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                # ν•¨μˆ˜ 이름 κ°€μ Έμ˜€κΈ°
                func_name = func.__name__
                
                # μΊμ‹œ ν‚€ 생성
                cache_key = self._make_cache_key(func_name, args, kwargs)
                
                # μΊμ‹œλœ κ²°κ³Ό 확인
                cached_result = self.redis.get(cache_key)
                if cached_result:
                    self.logger.debug(f"μΊμ‹œ 히트: {func_name}")
                    try:
                        return pickle.loads(cached_result)
                    except pickle.PickleError as e:
                        self.logger.error(f"μΊμ‹œ 역직렬화 μ‹€νŒ¨: {e}")
                
                # μΊμ‹œ 미슀 - ν•¨μˆ˜ μ‹€ν–‰
                self.logger.debug(f"μΊμ‹œ 미슀: {func_name}")
                start_time = time.time()
                result = func(*args, **kwargs)
                execution_time = time.time() - start_time
                
                # κ²°κ³Ό 캐싱
                try:
                    serialized = pickle.dumps(result)
                    self.redis.setex(cache_key, expiry, serialized)
                    self.logger.debug(
                        f"μΊμ‹œ μ €μž₯: {func_name}, μ‹€ν–‰ μ‹œκ°„: {execution_time:.4f}s, 크기: {len(serialized)} λ°”μ΄νŠΈ"
                    )
                except (pickle.PickleError, redis.RedisError) as e:
                    self.logger.error(f"μΊμ‹œ μ €μž₯ μ‹€νŒ¨: {e}")
                
                return result
            return wrapper
        return decorator
    
    def invalidate_cache(self, pattern: str = "*") -> int:
        """
        νŒ¨ν„΄μ— λ§žλŠ” μΊμ‹œ λ¬΄νš¨ν™”
        
        Args:
            pattern: μΊμ‹œ ν‚€ νŒ¨ν„΄
            
        Returns:
            int: μ‚­μ œλœ ν‚€μ˜ 수
        """
        try:
            # νŒ¨ν„΄μ— λ§žλŠ” ν‚€ μ°ΎκΈ°
            cache_pattern = f"{self.prefix}:{pattern}"
            keys = self.redis.keys(cache_pattern)
            
            if not keys:
                return 0
                
            # ν‚€ μ‚­μ œ
            deleted = self.redis.delete(*keys)
            self.logger.info(f"{deleted}개의 μΊμ‹œ ν•­λͺ© μ‚­μ œλ¨: {cache_pattern}")
            return deleted
        except redis.RedisError as e:
            self.logger.error(f"μΊμ‹œ λ¬΄νš¨ν™” μ‹€νŒ¨: {e}")
            return 0
    
    def memoize(self, expiry: int = 3600):
        """
        ν•¨μˆ˜ κ²°κ³Όλ₯Ό λ©”λͺ¨μ΄μ œμ΄μ…˜ν•˜λŠ” λ°μ½”λ ˆμ΄ν„°
        (동일 μΈμžμ— λŒ€ν•΄ ν•¨μˆ˜ λ‚΄λΆ€μ—μ„œ ν•œ 번만 μ‹€ν–‰)
        
        Args:
            expiry: μΊμ‹œ 만료 μ‹œκ°„(초)
            
        Returns:
            callable: λ°μ½”λ ˆμ΄ν„° ν•¨μˆ˜
        """
        return self.cache_decorator(expiry)
    
    def cached_property(self, expiry: int = 3600):
        """
        클래슀 ν”„λ‘œνΌν‹° κ²°κ³Όλ₯Ό μΊμ‹œν•˜λŠ” λ°μ½”λ ˆμ΄ν„°
        
        Args:
            expiry: μΊμ‹œ 만료 μ‹œκ°„(초)
            
        Returns:
            callable: λ°μ½”λ ˆμ΄ν„° ν•¨μˆ˜
        """
        def decorator(method):
            @property
            @wraps(method)
            def wrapper(self):
                # μΈμŠ€ν„΄μŠ€ 정보λ₯Ό ν¬ν•¨ν•œ μΊμ‹œ ν‚€ 생성
                instance_id = id(self)
                method_name = method.__name__
                class_name = self.__class__.__name__
                
                cache_key = f"{self.prefix}:{class_name}:{instance_id}:{method_name}"
                
                # μΊμ‹œ 확인
                cached_result = self.redis.get(cache_key)
                if cached_result:
                    try:
                        return pickle.loads(cached_result)
                    except pickle.PickleError:
                        pass
                
                # λ©”μ„œλ“œ μ‹€ν–‰
                result = method(self)
                
                # κ²°κ³Ό 캐싱
                try:
                    self.redis.setex(cache_key, expiry, pickle.dumps(result))
                except (pickle.PickleError, redis.RedisError):
                    pass
                
                return result
            return wrapper
        return decorator

    def cache_context(self, key: str, expiry: int = 3600):
        """
        μ»¨ν…μŠ€νŠΈ λ§€λ‹ˆμ €λ₯Ό ν†΅ν•œ 캐싱
        
        Args:
            key: μΊμ‹œ ν‚€
            expiry: μΊμ‹œ 만료 μ‹œκ°„(초)
            
        Returns:
            contextmanager: μ»¨ν…μŠ€νŠΈ λ§€λ‹ˆμ €
        """
        @contextmanager
        def cache_cm():
            cache_key = f"{self.prefix}:{key}"
            
            # μΊμ‹œ 확인
            cached_data = self.redis.get(cache_key)
            if cached_data:
                try:
                    yield pickle.loads(cached_data), True  # (데이터, μΊμ‹œ 히트)
                    return
                except pickle.PickleError:
                    pass
            
            # μΊμ‹œ 미슀
            result_container = []
            
            try:
                yield result_container, False  # (κ²°κ³Ό μ»¨ν…Œμ΄λ„ˆ, μΊμ‹œ 미슀)
                
                # κ²°κ³Όκ°€ μ»¨ν…Œμ΄λ„ˆμ— μ €μž₯λ˜μ—ˆλŠ”μ§€ 확인
                if result_container:
                    # 첫 번째 ν•­λͺ©μ„ μΊμ‹œ
                    try:
                        self.redis.setex(cache_key, expiry, pickle.dumps(result_container[0]))
                    except (pickle.PickleError, redis.RedisError) as e:
                        self.logger.error(f"μ»¨ν…μŠ€νŠΈ μΊμ‹œ μ €μž₯ μ‹€νŒ¨: {e}")
            except Exception as e:
                self.logger.error(f"μΊμ‹œ μ»¨ν…μŠ€νŠΈ 였λ₯˜: {e}")
                raise
        
        return cache_cm()

βœ… νŠΉμ§•:

  • λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ„ ν†΅ν•œ κ°„νŽΈν•œ 캐싱
  • μ•ˆμ •μ μΈ μΊμ‹œ ν‚€ 생성 (SHA-256 ν•΄μ‹œ μ‚¬μš©)
  • 직렬화λ₯Ό ν†΅ν•œ λ³΅μž‘ν•œ 객체 캐싱
  • νŒ¨ν„΄ 기반 μΊμ‹œ λ¬΄νš¨ν™”
  • λ©”λͺ¨μ΄μ œμ΄μ…˜ 지원
  • ν”„λ‘œνΌν‹° 캐싱 κΈ°λŠ₯
  • μ»¨ν…μŠ€νŠΈ λ§€λ‹ˆμ €λ₯Ό ν†΅ν•œ μœ μ—°ν•œ 캐싱
  • μ‹€ν–‰ μ‹œκ°„ μΈ‘μ • 및 λ‘œκΉ…
  • μ˜ˆμ™Έ 처리λ₯Ό ν†΅ν•œ μ•ˆμ •μ„± 확보
  • μΌκ΄€λœ ν‚€ 접두사 관리


4️⃣ μ„Έμ…˜ 관리

Redis의 λΉ λ₯Έ 처리 속도와 TTL κΈ°λŠ₯은 μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ„Έμ…˜ 관리에 이상적이닀. 인증 정보와 μ‚¬μš©μž μƒνƒœλ₯Ό μ•ˆμ „ν•˜κ²Œ μ €μž₯ν•˜κ³  관리할 수 있으며, λΆ„μ‚° ν™˜κ²½μ—μ„œλ„ μΌκ΄€λœ μ„Έμ…˜ 접근이 κ°€λŠ₯ν•˜λ‹€.

import uuid
from datetime import datetime, timedelta
import json
import secrets
import time

class RedisSession:
    """Redisλ₯Ό ν™œμš©ν•œ μ„Έμ…˜ 관리 클래슀"""
    
    def __init__(
        self, 
        client: RedisClient, 
        prefix: str = "session",
        default_expiry: int = 86400  # 24μ‹œκ°„
    ):
        """
        Redis μ„Έμ…˜ κ΄€λ¦¬μž μ΄ˆκΈ°ν™”
        
        Args:
            client: Redis ν΄λΌμ΄μ–ΈνŠΈ μΈμŠ€ν„΄μŠ€
            prefix: μ„Έμ…˜ ν‚€ 접두사
            default_expiry: κΈ°λ³Έ μ„Έμ…˜ 만료 μ‹œκ°„(초)
        """
        self.redis = client.redis
        self.prefix = prefix
        self.default_expiry = default_expiry
        self.logger = logging.getLogger(__name__)
    
    def _get_session_key(self, session_id: str) -> str:
        """
        μ„Έμ…˜ IDλ‘œλΆ€ν„° μ„Έμ…˜ ν‚€ 생성
        
        Args:
            session_id: μ„Έμ…˜ ID
            
        Returns:
            str: μ„Έμ…˜ ν‚€
        """
        return f"{self.prefix}:{session_id}"
    
    def create_session(
        self, 
        user_id: str, 
        data: dict,
        expiry: Optional[int] = None
    ) -> str:
        """
        μƒˆ μ„Έμ…˜ 생성
        
        Args:
            user_id: μ‚¬μš©μž ID
            data: μ„Έμ…˜μ— μ €μž₯ν•  데이터
            expiry: μ„Έμ…˜ 만료 μ‹œκ°„(초), None이면 κΈ°λ³Έκ°’ μ‚¬μš©
            
        Returns:
            str: μƒμ„±λœ μ„Έμ…˜ ID
        """
        # μ„Έμ…˜ ID 생성 (UUID v4)
        session_id = str(uuid.uuid4())
        session_key = self._get_session_key(session_id)
        
        # μ„Έμ…˜ 데이터 μ€€λΉ„
        current_time = datetime.now().isoformat()
        session_data = {
            "user_id": user_id,
            "created_at": current_time,
            "last_accessed": current_time,
            "ip_address": data.get("ip_address", "unknown"),
            "user_agent": data.get("user_agent", "unknown"),
            **data
        }
        
        # TTL μ„€μ •
        ttl = expiry if expiry is not None else self.default_expiry
        
        try:
            # μ„Έμ…˜ μ €μž₯
            self.redis.hset(session_key, mapping=session_data)
            self.redis.expire(session_key, timedelta(seconds=ttl))
            
            # μ‚¬μš©μž ID둜 μ„Έμ…˜ 인덱싱 (ν•œ μ‚¬μš©μžκ°€ μ—¬λŸ¬ μ„Έμ…˜μ„ κ°€μ§ˆ 수 있음)
            user_sessions_key = f"user_sessions:{user_id}"
            self.redis.sadd(user_sessions_key, session_id)
            
            self.logger.info(f"μ„Έμ…˜ 생성: {session_id} (μ‚¬μš©μž: {user_id})")
            return session_id
            
        except redis.RedisError as e:
            self.logger.error(f"μ„Έμ…˜ 생성 μ‹€νŒ¨: {e}")
            raise
    
    def get_session(self, session_id: str, update_access: bool = True) -> Optional[dict]:
        """
        μ„Έμ…˜ 정보 쑰회
        
        Args:
            session_id: μ„Έμ…˜ ID
            update_access: μ ‘κ·Ό μ‹œκ°„ μ—…λ°μ΄νŠΈ μ—¬λΆ€
            
        Returns:
            Optional[dict]: μ„Έμ…˜ 데이터 λ˜λŠ” None
        """
        if not session_id:
            return None
            
        session_key = self._get_session_key(session_id)
        
        try:
            # μ„Έμ…˜ 데이터 쑰회
            session_data = self.redis.hgetall(session_key)
            
            if not session_data:
                return None
                
            # λ§ˆμ§€λ§‰ μ ‘κ·Ό μ‹œκ°„ μ—…λ°μ΄νŠΈ
            if update_access:
                self.redis.hset(session_key, "last_accessed", datetime.now().isoformat())
                
            return session_data
            
        except redis.RedisError as e:
            self.logger.error(f"μ„Έμ…˜ 쑰회 μ‹€νŒ¨: {e}")
            return None
    
    def update_session(self, session_id: str, data: dict) -> bool:
        """
        μ„Έμ…˜ 데이터 μ—…λ°μ΄νŠΈ
        
        Args:
            session_id: μ„Έμ…˜ ID
            data: μ—…λ°μ΄νŠΈν•  데이터
            
        Returns:
            bool: μ—…λ°μ΄νŠΈ 성곡 μ—¬λΆ€
        """
        session_key = self._get_session_key(session_id)
        
        try:
            # μ„Έμ…˜ 쑴재 확인
            if not self.redis.exists(session_key):
                return False
                
            # μ—…λ°μ΄νŠΈν•  데이터에 λ§ˆμ§€λ§‰ μ ‘κ·Ό μ‹œκ°„ μΆ”κ°€
            update_data = {
                **data,
                "last_accessed": datetime.now().isoformat()
            }
            
            # μ„Έμ…˜ μ—…λ°μ΄νŠΈ
            self.redis.hset(session_key, mapping=update_data)
            return True
            
        except redis.RedisError as e:
            self.logger.error(f"μ„Έμ…˜ μ—…λ°μ΄νŠΈ μ‹€νŒ¨: {e}")
            return False
    
    def delete_session(self, session_id: str) -> bool:
        """
        μ„Έμ…˜ μ‚­μ œ
        
        Args:
            session_id: μ„Έμ…˜ ID
            
        Returns:
            bool: μ‚­μ œ 성곡 μ—¬λΆ€
        """
        session_key = self._get_session_key(session_id)
        
        try:
            # μ„Έμ…˜ λ°μ΄ν„°μ—μ„œ μ‚¬μš©μž ID κ°€μ Έμ˜€κΈ°
            user_id = self.redis.hget(session_key, "user_id")
            
            # μ„Έμ…˜ μ‚­μ œ
            if self.redis.delete(session_key):
                # μ‚¬μš©μž μ„Έμ…˜ μΈλ±μŠ€μ—μ„œλ„ μ‚­μ œ
                if user_id:
                    self.redis.srem(f"user_sessions:{user_id}", session_id)
                
                self.logger.info(f"μ„Έμ…˜ μ‚­μ œ: {session_id}")
                return True
            return False
            
        except redis.RedisError as e:
            self.logger.error(f"μ„Έμ…˜ μ‚­μ œ μ‹€νŒ¨: {e}")
            return False
    
    def extend_session(self, session_id: str, extension: int) -> bool:
        """
        μ„Έμ…˜ 만료 μ‹œκ°„ μ—°μž₯
        
        Args:
            session_id: μ„Έμ…˜ ID
            extension: μ—°μž₯ν•  μ‹œκ°„(초)
            
        Returns:
            bool: μ—°μž₯ 성곡 μ—¬λΆ€
        """
        session_key = self._get_session_key(session_id)
        
        try:
            # ν˜„μž¬ 남은 μ‹œκ°„μ— μ—°μž₯ μ‹œκ°„ μΆ”κ°€
            return self.redis.expire(session_key, timedelta(seconds=extension))
        except redis.RedisError as e:
            self.logger.error(f"μ„Έμ…˜ μ—°μž₯ μ‹€νŒ¨: {e}")
            return False
    
    def get_user_sessions(self, user_id: str) -> List[str]:
        """
        μ‚¬μš©μžμ˜ λͺ¨λ“  μ„Έμ…˜ ID 쑰회
        
        Args:
            user_id: μ‚¬μš©μž ID
            
        Returns:
            List[str]: μ„Έμ…˜ ID λͺ©λ‘
        """
        try:
            user_sessions_key = f"user_sessions:{user_id}"
            return list(self.redis.smembers(user_sessions_key))
        except redis.RedisError as e:
            self.logger.error(f"μ‚¬μš©μž μ„Έμ…˜ 쑰회 μ‹€νŒ¨: {e}")
            return []
    
    def invalidate_user_sessions(self, user_id: str, keep_current: Optional[str] = None) -> int:
        """
        μ‚¬μš©μžμ˜ λͺ¨λ“  μ„Έμ…˜ λ¬΄νš¨ν™”
        
        Args:
            user_id: μ‚¬μš©μž ID
            keep_current: μœ μ§€ν•  ν˜„μž¬ μ„Έμ…˜ ID (선택 사항)
            
        Returns:
            int: λ¬΄νš¨ν™”λœ μ„Έμ…˜ 수
        """
        try:
            # μ‚¬μš©μžμ˜ λͺ¨λ“  μ„Έμ…˜ ID κ°€μ Έμ˜€κΈ°
            sessions = self.get_user_sessions(user_id)
            if not sessions:
                return 0
                
            # ν˜„μž¬ μ„Έμ…˜μ„ μ œμ™Έν•œ λͺ¨λ“  μ„Έμ…˜ μ‚­μ œ
            deleted_count = 0
            for session_id in sessions:
                if keep_current and session_id == keep_current:
                    continue
                    
                if self.delete_session(session_id):
                    deleted_count += 1
            
            self.logger.info(f"μ‚¬μš©μž {user_id}의 μ„Έμ…˜ {deleted_count}개 λ¬΄νš¨ν™”")
            return deleted_count
            
        except redis.RedisError as e:
            self.logger.error(f"μ‚¬μš©μž μ„Έμ…˜ λ¬΄νš¨ν™” μ‹€νŒ¨: {e}")
            return 0
    
    def create_csrf_token(self, session_id: str) -> Optional[str]:
        """
        CSRF 토큰 생성 및 μ„Έμ…˜μ— μ €μž₯
        
        Args:
            session_id: μ„Έμ…˜ ID
            
        Returns:
            Optional[str]: CSRF 토큰 λ˜λŠ” None
        """
        try:
            # 랜덀 토큰 생성
            csrf_token = secrets.token_hex(32)
            
            # μ„Έμ…˜μ— 토큰 μ €μž₯
            if self.update_session(session_id, {"csrf_token": csrf_token}):
                return csrf_token
            return None
            
        except Exception as e:
            self.logger.error(f"CSRF 토큰 생성 μ‹€νŒ¨: {e}")
            return None
    
    def validate_csrf_token(self, session_id: str, token: str) -> bool:
        """
        CSRF 토큰 검증
        
        Args:
            session_id: μ„Έμ…˜ ID
            token: 검증할 CSRF 토큰
            
        Returns:
            bool: 유효 μ—¬λΆ€
        """
        try:
            # μ„Έμ…˜μ—μ„œ 토큰 κ°€μ Έμ˜€κΈ°
            session = self.get_session(session_id, update_access=False)
            if not session:
                return False
                
            # 토큰 비ꡐ (μ‹œκ°„ 독립적 비ꡐ)
            stored_token = session.get("csrf_token")
            if not stored_token:
                return False
                
            return secrets.compare_digest(stored_token, token)
            
        except Exception as e:
            self.logger.error(f"CSRF 토큰 검증 μ‹€νŒ¨: {e}")
            return False

# μ‚¬μš© 예제
def create_user_session(redis_session, user_info):
    """μ‚¬μš©μž 둜그인 ν›„ μ„Έμ…˜ 생성 예제"""
    user_id = user_info["id"]
    ip_address = user_info.get("ip_address", "127.0.0.1")
    user_agent = user_info.get("user_agent", "Unknown")
    
    # μ„Έμ…˜ 데이터 μ€€λΉ„
    session_data = {
        "username": user_info["username"],
        "email": user_info["email"],
        "role": user_info["role"],
        "ip_address": ip_address,
        "user_agent": user_agent,
        "login_time": int(time.time())
    }
    
    # μ„Έμ…˜ 생성
    session_id = redis_session.create_session(user_id, session_data)
    
    # CSRF 토큰 생성
    csrf_token = redis_session.create_csrf_token(session_id)
    
    return {
        "session_id": session_id,
        "csrf_token": csrf_token
    }

βœ… νŠΉμ§•:

  • μ•ˆμ „ν•œ μ„Έμ…˜ ID 생성 (UUID μ‚¬μš©)
  • μœ μ—°ν•œ μ„Έμ…˜ 데이터 μ €μž₯
  • μžλ™ 만료 μ‹œκ°„ 관리
  • μ„Έμ…˜ μ ‘κ·Ό μ‹œκ°„ 좔적
  • μ‚¬μš©μžλ³„ μ„Έμ…˜ 인덱싱
  • μ„Έμ…˜ 데이터 μ—…λ°μ΄νŠΈ κΈ°λŠ₯
  • μ„Έμ…˜ μ—°μž₯ 지원
  • μ‚¬μš©μžλ³„ μ„Έμ…˜ 관리
  • CSRF 보호 κΈ°λŠ₯ λ‚΄μž₯
  • μ•ˆμ „ν•œ 토큰 비ꡐ (타이밍 어택 λ°©μ§€)


5️⃣ κ³ κΈ‰ νŒ¨ν„΄

RedisλŠ” λ‹¨μˆœν•œ ν‚€-κ°’ μ €μž₯μ†Œλ₯Ό λ„˜μ–΄ λ‹€μ–‘ν•œ κ³ κΈ‰ νŒ¨ν„΄μ„ κ΅¬ν˜„ν•  수 μžˆλ‹€. λ©”μ‹œμ§€ 큐, λΆ„μ‚° 락, 속도 μ œν•œ, λ¦¬λ”λ³΄λ“œ λ“±μ˜ κΈ°λŠ₯을 효율적으둜 κ΅¬ν˜„ν•˜μ—¬ λ³΅μž‘ν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μš”κ΅¬μ‚¬ν•­μ„ ν•΄κ²°ν•  수 μžˆλ‹€.

import time
import random
import uuid
from datetime import datetime
from typing import Optional, Dict, List, Tuple, Callable, Any

class RedisAdvancedPatterns:
    """Redisλ₯Ό ν™œμš©ν•œ κ³ κΈ‰ νŒ¨ν„΄ κ΅¬ν˜„ 클래슀"""
    
    def __init__(self, client: RedisClient):
        """
        Redis κ³ κΈ‰ νŒ¨ν„΄ μ΄ˆκΈ°ν™”
        
        Args:
            client: Redis ν΄λΌμ΄μ–ΈνŠΈ μΈμŠ€ν„΄μŠ€
        """
        self.redis = client.redis
        self.logger = logging.getLogger(__name__)
    
    # λ©”μ‹œμ§€ 큐 νŒ¨ν„΄
    def enqueue(self, queue_name: str, message: Dict[str, Any]) -> bool:
        """
        λ©”μ‹œμ§€ 큐에 λ©”μ‹œμ§€ μΆ”κ°€
        
        Args:
            queue_name: 큐 이름
            message: λ©”μ‹œμ§€ 데이터
            
        Returns:
            bool: 성곡 μ—¬λΆ€
        """
        try:
            # λ©”μ‹œμ§€μ— 메타데이터 μΆ”κ°€
            full_message = {
                "id": str(uuid.uuid4()),
                "timestamp": datetime.now().isoformat(),
                "data": message
            }
            
            # 큐에 λ©”μ‹œμ§€ μΆ”κ°€ (였λ₯Έμͺ½μ— μΆ”κ°€)
            self.redis.rpush(queue_name, json.dumps(full_message))
            return True
        except (redis.RedisError, json.JSONDecodeError) as e:
            self.logger.error(f"λ©”μ‹œμ§€ 큐 μΆ”κ°€ μ‹€νŒ¨: {e}")
            return False
    
    def dequeue(self, queue_name: str, timeout: int = 0) -> Optional[Dict]:
        """
        λ©”μ‹œμ§€ νμ—μ„œ λ©”μ‹œμ§€ κ°€μ Έμ˜€κΈ° (λΈ”λ‘œν‚Ή)
        
        Args:
            queue_name: 큐 이름
            timeout: νƒ€μž„μ•„μ›ƒ(초), 0은 λ¬΄ν•œ λŒ€κΈ°
            
        Returns:
            Optional[Dict]: λ©”μ‹œμ§€ λ˜λŠ” None
        """
        try:
            # μ™Όμͺ½μ—μ„œ λ©”μ‹œμ§€ κ°€μ Έμ˜€κΈ° (BLPOP: λΈ”λ‘œν‚Ή μ™Όμͺ½ 팝)
            result = self.redis.blpop(queue_name, timeout)
            
            # νƒ€μž„μ•„μ›ƒλœ 경우
            if not result:
                return None
                
            # κ²°κ³ΌλŠ” (큐 이름, λ©”μ‹œμ§€) ν˜•νƒœ
            _, message_data = result
            
            # JSON 역직렬화
            return json.loads(message_data)
        except (redis.RedisError, json.JSONDecodeError) as e:
            self.logger.error(f"λ©”μ‹œμ§€ 큐 κ°€μ Έμ˜€κΈ° μ‹€νŒ¨: {e}")
            return None
    
    def queue_length(self, queue_name: str) -> int:
        """
        큐 길이 쑰회
        
        Args:
            queue_name: 큐 이름
            
        Returns:
            int: 큐 길이
        """
        try:
            return self.redis.llen(queue_name)
        except redis.RedisError as e:
            self.logger.error(f"큐 길이 쑰회 μ‹€νŒ¨: {e}")
            return 0
    
    # λΆ„μ‚° 락 νŒ¨ν„΄
    def acquire_lock(
        self, 
        lock_name: str, 
        owner: str = None,
        expiry: int = 10,
        retry_count: int = 3,
        retry_delay: float = 0.2
    ) -> Optional[str]:
        """
        λΆ„μ‚° 락 νšλ“
        
        Args:
            lock_name: 락 이름
            owner: 락 μ†Œμœ μž μ‹λ³„μž
            expiry: 락 만료 μ‹œκ°„(초)
            retry_count: μž¬μ‹œλ„ 횟수
            retry_delay: μž¬μ‹œλ„ 간격(초)
            
        Returns:
            Optional[str]: 락 μ‹λ³„μž λ˜λŠ” None
        """
        # 락 ν‚€
        lock_key = f"lock:{lock_name}"
        
        # 락 μ†Œμœ μž μ‹λ³„μž
        owner_id = owner or str(uuid.uuid4())
        
        # μž¬μ‹œλ„ 루프
        for attempt in range(retry_count + 1):
            # 락 μ‹œλ„ (NX: ν‚€κ°€ 없을 λ•Œλ§Œ μ„€μ •)
            success = self.redis.set(lock_key, owner_id, ex=expiry, nx=True)
            
            if success:
                self.logger.debug(f"락 νšλ“: {lock_name} (μ†Œμœ μž: {owner_id})")
                return owner_id
                
            # λ§ˆμ§€λ§‰ μ‹œλ„κ°€ μ•„λ‹ˆλ©΄ λŒ€κΈ° ν›„ μž¬μ‹œλ„
            if attempt < retry_count:
                # λ¬΄μž‘μœ„ μ§€ν„° μΆ”κ°€ (κ²½ν•© ν•΄κ²°)
                jitter = random.uniform(0, 0.1)
                time.sleep(retry_delay + jitter)
        
        self.logger.debug(f"락 νšλ“ μ‹€νŒ¨: {lock_name}")
        return None
    
    def release_lock(self, lock_name: str, owner_id: str) -> bool:
        """
        λΆ„μ‚° 락 ν•΄μ œ
        
        Args:
            lock_name: 락 이름
            owner_id: 락 μ†Œμœ μž μ‹λ³„μž
            
        Returns:
            bool: 성곡 μ—¬λΆ€
        """
        lock_key = f"lock:{lock_name}"
        
        # Lua 슀크립트λ₯Ό μ‚¬μš©ν•œ μ•ˆμ „ν•œ 락 ν•΄μ œ
        # μ†Œμœ μžκ°€ μΌμΉ˜ν•˜λŠ” κ²½μš°μ—λ§Œ μ‚­μ œ
        script = """
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
        """
        
        try:
            # 슀크립트 μ‹€ν–‰
            result = self.redis.eval(script, 1, lock_key, owner_id)
            success = result == 1
            
            if success:
                self.logger.debug(f"락 ν•΄μ œ: {lock_name} (μ†Œμœ μž: {owner_id})")
            else:
                self.logger.debug(f"락 ν•΄μ œ μ‹€νŒ¨: {lock_name} (μ†Œμœ μž 뢈일치 λ˜λŠ” 만료)")
                
            return success
        except redis.RedisError as e:
            self.logger.error(f"락 ν•΄μ œ 였λ₯˜: {e}")
            return False
    
    @contextmanager
    def distributed_lock(
        self, 
        lock_name: str, 
        expiry: int = 10,
        retry_count: int = 3,
        retry_delay: float = 0.2
    ):
        """
        λΆ„μ‚° 락 μ»¨ν…μŠ€νŠΈ λ§€λ‹ˆμ €
        
        Args:
            lock_name: 락 이름
            expiry: 락 만료 μ‹œκ°„(초)
            retry_count: μž¬μ‹œλ„ 횟수
            retry_delay: μž¬μ‹œλ„ 간격(초)
            
        Raises:
            RuntimeError: 락 νšλ“ μ‹€νŒ¨ μ‹œ
        """
        owner_id = None
        
        try:
            # 락 νšλ“
            owner_id = self.acquire_lock(
                lock_name, 
                expiry=expiry,
                retry_count=retry_count,
                retry_delay=retry_delay
            )
            
            if not owner_id:
                raise RuntimeError(f"락 νšλ“ μ‹€νŒ¨: {lock_name}")
                
            # 락 νšλ“ 성곡, μ»¨ν…μŠ€νŠΈ μ§„μž…
            yield
            
        finally:
            # 락 ν•΄μ œ (μ†Œμœ μžκ°€ μžˆλŠ” κ²½μš°μ—λ§Œ)
            if owner_id:
                self.release_lock(lock_name, owner_id)
    
    # 속도 μ œν•œ νŒ¨ν„΄
    def rate_limit(
        self, 
        key: str, 
        limit: int, 
        period: int
    ) -> Tuple[bool, int, int]:
        """
        속도 μ œν•œ 체크 (μŠ¬λΌμ΄λ”© μœˆλ„μš° μ•Œκ³ λ¦¬μ¦˜)
        
        Args:
            key: μ œν•œν•  ν‚€ (예: IP λ˜λŠ” μ‚¬μš©μž ID)
            limit: κΈ°κ°„ λ‚΄ ν—ˆμš© μš”μ²­ 수
            period: κΈ°κ°„(초)
            
        Returns:
            Tuple[bool, int, int]: (ν—ˆμš© μ—¬λΆ€, ν˜„μž¬ μš”μ²­ 수, 남은 초)
        """
        # 속도 μ œν•œ ν‚€ 및 ν˜„μž¬ μ‹œκ°„
        rate_key = f"ratelimit:{key}"
        now = int(time.time())
        
        # νŒŒμ΄ν”„λΌμΈμ„ ν†΅ν•œ μ›μžμ  μ—°μ‚°
        pipe = self.redis.pipeline()
        
        try:
            # 만료된 μš”μ²­ 제거 (ν˜„μž¬ μ‹œκ°„ - 기간보닀 이전 μš”μ²­)
            pipe.zremrangebyscore(rate_key, 0, now - period)
            
            # μƒˆ μš”μ²­ μΆ”κ°€
            pipe.zadd(rate_key, {str(now): now})
            
            # ν˜„μž¬ μš”μ²­ 수 쑰회
            pipe.zcard(rate_key)
            
            # ν‚€ 만료 μ„€μ • (κΈ°κ°„ + 1초)
            pipe.expire(rate_key, period + 1)
            
            # νŒŒμ΄ν”„λΌμΈ μ‹€ν–‰
            _, _, request_count, _ = pipe.execute()
            
            # μš”μ²­ ν—ˆμš© μ—¬λΆ€ κ²°μ •
            allowed = request_count <= limit
            
            # TTL 확인
            ttl = self.redis.ttl(rate_key)
            
            return allowed, request_count, ttl
            
        except redis.RedisError as e:
            self.logger.error(f"속도 μ œν•œ 체크 μ‹€νŒ¨: {e}")
            return False, 0, 0
    
    # λ¦¬λ”λ³΄λ“œ νŒ¨ν„΄
    def add_to_leaderboard(self, leaderboard: str, user: str, score: float) -> int:
        """
        λ¦¬λ”λ³΄λ“œμ— 점수 μΆ”κ°€/κ°±μ‹ 
        
        Args:
            leaderboard: λ¦¬λ”λ³΄λ“œ 이름
            user: μ‚¬μš©μž μ‹λ³„μž
            score: 점수
            
        Returns:
            int: μƒˆλ‘œ μΆ”κ°€λœ 경우 1, κ°±μ‹ λœ 경우 0
        """
        try:
            return self.redis.zadd(leaderboard, {user: score})
        except redis.RedisError as e:
            self.logger.error(f"λ¦¬λ”λ³΄λ“œ μΆ”κ°€ μ‹€νŒ¨: {e}")
            return 0
    
    def get_leaderboard(
        self, 
        leaderboard: str, 
        start: int = 0, 
        end: int = 9, 
        desc: bool = True
    ) -> List[Tuple[str, float]]:
        """
        λ¦¬λ”λ³΄λ“œ μˆœμœ„ 쑰회
        
        Args:
            leaderboard: λ¦¬λ”λ³΄λ“œ 이름
            start: μ‹œμž‘ 인덱슀 (0λΆ€ν„° μ‹œμž‘)
            end: μ’…λ£Œ 인덱슀
            desc: λ‚΄λ¦Όμ°¨μˆœ μ—¬λΆ€
            
        Returns:
            List[Tuple[str, float]]: (μ‚¬μš©μž, 점수) λͺ©λ‘
        """
        try:
            # λ‚΄λ¦Όμ°¨μˆœ λ˜λŠ” μ˜€λ¦„μ°¨μˆœ
            if desc:
                result = self.redis.zrevrange(leaderboard, start, end, withscores=True)
            else:
                result = self.redis.zrange(leaderboard, start, end, withscores=True)
                
            return result
        except redis.RedisError as e:
            self.logger.error(f"λ¦¬λ”λ³΄λ“œ 쑰회 μ‹€νŒ¨: {e}")
            return []
    
    def get_rank(self, leaderboard: str, user: str, desc: bool = True) -> Optional[int]:
        """
        λ¦¬λ”λ³΄λ“œ μˆœμœ„ 쑰회
        
        Args:
            leaderboard: λ¦¬λ”λ³΄λ“œ 이름
            user: μ‚¬μš©μž μ‹λ³„μž
            desc: λ‚΄λ¦Όμ°¨μˆœ μ—¬λΆ€
            
        Returns:
            Optional[int]: 0λΆ€ν„° μ‹œμž‘ν•˜λŠ” μˆœμœ„ λ˜λŠ” None
        """
        try:
            # λ‚΄λ¦Όμ°¨μˆœ λ˜λŠ” μ˜€λ¦„μ°¨μˆœ
            if desc:
                rank = self.redis.zrevrank(leaderboard, user)
            else:
                rank = self.redis.zrank(leaderboard, user)
                
            return rank
        except redis.RedisError as e:
            self.logger.error(f"μˆœμœ„ 쑰회 μ‹€νŒ¨: {e}")
            return None
    
    def get_score(self, leaderboard: str, user: str) -> Optional[float]:
        """
        λ¦¬λ”λ³΄λ“œ 점수 쑰회
        
        Args:
            leaderboard: λ¦¬λ”λ³΄λ“œ 이름
            user: μ‚¬μš©μž μ‹λ³„μž
            
        Returns:
            Optional[float]: 점수 λ˜λŠ” None
        """
        try:
            return self.redis.zscore(leaderboard, user)
        except redis.RedisError as e:
            self.logger.error(f"점수 쑰회 μ‹€νŒ¨: {e}")
            return None

βœ… νŠΉμ§•:

  • λ©”μ‹œμ§€ 큐 κ΅¬ν˜„ (μƒμ‚°μž-μ†ŒλΉ„μž νŒ¨ν„΄)
  • λΆ„μ‚° 락을 ν†΅ν•œ λ™μ‹œμ„± μ œμ–΄
  • μŠ¬λΌμ΄λ”© μœˆλ„μš° 속도 μ œν•œ
  • λ¦¬λ”λ³΄λ“œ 및 μˆœμœ„ μ‹œμŠ€ν…œ
  • Lua 슀크립트λ₯Ό ν†΅ν•œ μ›μžμ  μ—°μ‚°
  • νŒŒμ΄ν”„λΌμΈμ„ ν™œμš©ν•œ μ„±λŠ₯ μ΅œμ ν™”
  • μ»¨ν…μŠ€νŠΈ λ§€λ‹ˆμ € 지원
  • λΆ„μ‚° ν™˜κ²½ 지원
  • 효율적인 만료 μ‹œκ°„ 관리
  • κ²¬κ³ ν•œ 였λ₯˜ 처리


6️⃣ ν΄λŸ¬μŠ€ν„°λ§κ³Ό κ³ κ°€μš©μ„±

RedisλŠ” 단일 μ„œλ²„ ꡬ성을 λ„˜μ–΄ ν΄λŸ¬μŠ€ν„°λ§κ³Ό 볡제 κΈ°λŠ₯을 톡해 ν™•μž₯μ„±κ³Ό κ³ κ°€μš©μ„±μ„ μ œκ³΅ν•œλ‹€. λŒ€κ·œλͺ¨ 데이터와 νŠΈλž˜ν”½ 처리, μž₯μ•  쑰치 및 데이터 지속성을 μœ„ν•œ λ‹€μ–‘ν•œ ꡬ성이 κ°€λŠ₯ν•˜λ‹€.

from rediscluster import RedisCluster
import redis.sentinel

class RedisHighAvailability:
    """Redis ν΄λŸ¬μŠ€ν„°λ§ 및 κ³ κ°€μš©μ„± ꡬ성 클래슀"""
    
    def __init__(self):
        self.logger = logging.getLogger(__name__)
        self.cluster = None
        self.master = None
    
    def connect_cluster(self, startup_nodes, decode_responses=True):
        """Redis ν΄λŸ¬μŠ€ν„° μ—°κ²°"""
        try:
            self.cluster = RedisCluster(
                startup_nodes=startup_nodes,
                decode_responses=decode_responses,
                skip_full_coverage_check=True
            )
            self.logger.info("Redis ν΄λŸ¬μŠ€ν„° μ—°κ²° 성곡")
            return True
        except Exception as e:
            self.logger.error(f"Redis ν΄λŸ¬μŠ€ν„° μ—°κ²° μ‹€νŒ¨: {e}")
            return False
    
    def connect_sentinel(self, sentinel_list, master_name, password=None):
        """Redis Sentinel μ—°κ²° (λ§ˆμŠ€ν„°-슬레이브 ꡬ성)"""
        try:
            sentinel = redis.sentinel.Sentinel(
                sentinel_list,
                socket_timeout=1.0,
                password=password
            )
            
            # λ§ˆμŠ€ν„° μ—°κ²°
            self.master = sentinel.master_for(
                master_name,
                socket_timeout=0.5,
                password=password,
                decode_responses=True
            )
            
            # 슬레이브 μ—°κ²° (읽기 μ „μš©)
            self.slave = sentinel.slave_for(
                master_name,
                socket_timeout=0.5,
                password=password,
                decode_responses=True
            )
            
            self.logger.info(f"Redis Sentinel μ—°κ²° 성곡 (λ§ˆμŠ€ν„°: {master_name})")
            return True
        except Exception as e:
            self.logger.error(f"Redis Sentinel μ—°κ²° μ‹€νŒ¨: {e}")
            return False
    
    def get_cluster_info(self):
        """ν΄λŸ¬μŠ€ν„° 정보 쑰회"""
        if not self.cluster:
            return None
            
        try:
            nodes = self.cluster.cluster_nodes()
            slots = self.cluster.cluster_slots()
            info = self.cluster.cluster_info()
            
            return {
                "nodes": nodes,
                "slots": slots,
                "info": info
            }
        except Exception as e:
            self.logger.error(f"ν΄λŸ¬μŠ€ν„° 정보 쑰회 μ‹€νŒ¨: {e}")
            return None
    
    def write_to_master(self, key, value):
        """λ§ˆμŠ€ν„°μ— 데이터 μ“°κΈ°"""
        if not self.master:
            return False
            
        try:
            return self.master.set(key, value)
        except Exception as e:
            self.logger.error(f"λ§ˆμŠ€ν„° μ“°κΈ° μ‹€νŒ¨: {e}")
            return False
    
    def read_from_slave(self, key):
        """μŠ¬λ ˆμ΄λΈŒμ—μ„œ 데이터 읽기"""
        if not hasattr(self, 'slave') or not self.slave:
            return None
            
        try:
            return self.slave.get(key)
        except Exception as e:
            self.logger.error(f"슬레이브 읽기 μ‹€νŒ¨: {e}")
            return None

class RedisPubSub:
    """Redisλ₯Ό ν™œμš©ν•œ λ°œν–‰/ꡬ독 κ΅¬ν˜„ 클래슀"""
    
    def __init__(self, client):
        self.redis = client.redis
        self.pubsub = self.redis.pubsub()
        self.logger = logging.getLogger(__name__)
        self.thread = None
    
    def subscribe(self, channels):
        """채널 ꡬ독"""
        try:
            self.pubsub.subscribe(*channels)
            self.logger.info(f"채널 ꡬ독: {channels}")
            return True
        except Exception as e:
            self.logger.error(f"채널 ꡬ독 μ‹€νŒ¨: {e}")
            return False
    
    def pattern_subscribe(self, patterns):
        """νŒ¨ν„΄ ꡬ독"""
        try:
            self.pubsub.psubscribe(*patterns)
            self.logger.info(f"νŒ¨ν„΄ ꡬ독: {patterns}")
            return True
        except Exception as e:
            self.logger.error(f"νŒ¨ν„΄ ꡬ독 μ‹€νŒ¨: {e}")
            return False
    
    def publish(self, channel, message):
        """λ©”μ‹œμ§€ λ°œν–‰"""
        try:
            receivers = self.redis.publish(channel, json.dumps(message))
            self.logger.debug(f"λ©”μ‹œμ§€ λ°œν–‰: {channel}, μˆ˜μ‹ μž: {receivers}")
            return receivers
        except Exception as e:
            self.logger.error(f"λ©”μ‹œμ§€ λ°œν–‰ μ‹€νŒ¨: {e}")
            return 0
    
    def start_listening(self, callback, sleep_time=0.1):
        """λ°±κ·ΈλΌμš΄λ“œμ—μ„œ λ©”μ‹œμ§€ μˆ˜μ‹  μ‹œμž‘"""
        self.thread = self.pubsub.run_in_thread(
            sleep_time=sleep_time,
            callback=callback
        )
        return self.thread
    
    def stop_listening(self):
        """λ©”μ‹œμ§€ μˆ˜μ‹  쀑지"""
        if self.thread:
            self.thread.stop()
            self.thread = None
    
    def get_message(self, ignore_subscribe_messages=True):
        """단일 λ©”μ‹œμ§€ μˆ˜μ‹ """
        return self.pubsub.get_message(
            ignore_subscribe_messages=ignore_subscribe_messages
        )
    
    def close(self):
        """ꡬ독 μ’…λ£Œ 및 μžμ› 정리"""
        self.stop_listening()
        self.pubsub.unsubscribe()
        self.pubsub.punsubscribe()
        self.pubsub.close()

# ν΄λŸ¬μŠ€ν„° μ„€μ • 예제
def setup_redis_cluster():
    """Redis ν΄λŸ¬μŠ€ν„° μ„€μ • 및 μ—°κ²° 예제"""
    startup_nodes = [
        {"host": "redis-node1", "port": 7000},
        {"host": "redis-node2", "port": 7001},
        {"host": "redis-node3", "port": 7002}
    ]
    
    ha_manager = RedisHighAvailability()
    
    if ha_manager.connect_cluster(startup_nodes):
        print("ν΄λŸ¬μŠ€ν„° μ—°κ²° 성곡")
        
        # ν΄λŸ¬μŠ€ν„° 정보 좜λ ₯
        cluster_info = ha_manager.get_cluster_info()
        if cluster_info:
            print(f"ν΄λŸ¬μŠ€ν„° μƒνƒœ: {cluster_info['info'].get('cluster_state')}")
            print(f"λ…Έλ“œ 수: {len(cluster_info['nodes'])}")
    else:
        print("ν΄λŸ¬μŠ€ν„° μ—°κ²° μ‹€νŒ¨")

βœ… νŠΉμ§•:

  • λ‹€μ–‘ν•œ κ³ κ°€μš©μ„± ꡬ성 지원
  • ν΄λŸ¬μŠ€ν„° λͺ¨λ“œ: μˆ˜ν‰ ν™•μž₯ 및 데이터 λΆ„μ‚°
  • Sentinel λͺ¨λ“œ: μžλ™ μž₯μ•  쑰치 및 λ§ˆμŠ€ν„°-슬레이브 ꡬ성
  • ν΄λŸ¬μŠ€ν„° μƒνƒœ λͺ¨λ‹ˆν„°λ§ κΈ°λŠ₯
  • 읽기/μ“°κΈ° λΆ„λ¦¬λ‘œ λΆ€ν•˜ λΆ„μ‚° κ°€λŠ₯
  • λ°œν–‰/ꡬ독(Pub/Sub) νŒ¨ν„΄ κ΅¬ν˜„
  • μ‹€μ‹œκ°„ λ©”μ‹œμ§€ 처리 및 이벀트 기반 μ•„ν‚€ν…μ²˜
  • 멀티채널 및 νŒ¨ν„΄ 기반 ꡬ독
  • λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œλ‘œ 비동기 λ©”μ‹œμ§€ 처리
  • μžμ› μ•ˆμ „ν•œ 정리 및 μ’…λ£Œ 처리


μ£Όμš” 팁

βœ… λͺ¨λ²” 사둀:

  • ν‚€ 섀계: 의미 μžˆλŠ” 접두사와 κ΅¬λΆ„μžλ₯Ό μ‚¬μš©ν•˜μ—¬ 체계적인 ν‚€ ꡬ쑰 섀계

    user:1000:profile - μ‚¬μš©μž ν”„λ‘œν•„
    user:1000:posts - μ‚¬μš©μž κ²Œμ‹œλ¬Ό
    post:5000:comments - κ²Œμ‹œλ¬Ό λŒ“κΈ€
    
  • λ©”λͺ¨λ¦¬ μ΅œμ ν™”: λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰ μ΅œμ†Œν™”λ₯Ό μœ„ν•œ 데이터 ꡬ쑰 선택

    • μž‘μ€ ν•΄μ‹œλŠ” λ¬Έμžμ—΄λ³΄λ‹€ 효율적 (ziplist 인코딩)
    • μˆ«μžλŠ” μ •μˆ˜λ‘œ μ €μž₯ (int 인코딩)
    • ν•„μš”ν•œ κ²½μš°μ—λ§Œ TTL μ„€μ •
  • μ„±λŠ₯ λͺ¨λ‹ˆν„°λ§: μ£Όμš” μ§€ν‘œ μΆ”μ μœΌλ‘œ μ„±λŠ₯ 문제 μ‘°κΈ° 발견

    # λ©”λͺ¨λ¦¬, ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²°, λͺ…λ Ή 수 λ“± λͺ¨λ‹ˆν„°λ§
    info = redis.info()
    
    # 느린 둜그 μ„€μ • 및 확인
    redis.config_set('slowlog-log-slower-than', 10000)  # 10ms
    slow_logs = redis.slowlog_get(10)
  • λ°±μ—… μ „λž΅: 데이터 손싀 λ°©μ§€λ₯Ό μœ„ν•œ RDB 및 AOF μ„€μ •

    # redis.conf μ„€μ • 예
    save 900 1
    appendonly yes
    appendfsync everysec
    
  • λ³΄μ•ˆ κ°•ν™”: 인증, λͺ…λ Ήμ–΄ μ œν•œ, λ„€νŠΈμ›Œν¬ μ ‘κ·Ό μ œμ–΄

    # redis.conf λ³΄μ•ˆ μ„€μ •
    requirepass StrongPassword
    rename-command FLUSHALL ""  # μœ„ν—˜ λͺ…λ Ήμ–΄ λΉ„ν™œμ„±ν™”
    bind 127.0.0.1  # 둜컬 μ ‘κ·Όλ§Œ ν—ˆμš©
    
  • 효율적인 νŒŒμ΄ν”„λΌμΈ μ‚¬μš©: λ„€νŠΈμ›Œν¬ μ§€μ—° κ°μ†Œλ₯Ό μœ„ν•œ λͺ…λ Ή 배치 처리

    # λŒ€λŸ‰ 데이터 처리 μ‹œ νŒŒμ΄ν”„λΌμΈ ν™œμš©
    with redis.pipeline() as pipe:
        for i in range(10000):
            pipe.set(f"key:{i}", f"value:{i}")
        pipe.execute()
  • ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° 관리: 컀λ„₯μ…˜ ν’€ μ‚¬μš©μœΌλ‘œ μžμ› νš¨μœ¨ν™”

    pool = redis.ConnectionPool(max_connections=10)
    client = redis.Redis(connection_pool=pool)
  • Lua 슀크립트 ν™œμš©: λ³΅μž‘ν•œ μž‘μ—…μ˜ μ›μžμ  보μž₯

    # Lua 슀크립트둜 μ›μžμ  μ—°μ‚° μˆ˜ν–‰
    script = """
    local current = redis.call('GET', KEYS[1])
    if current then
        return redis.call('SET', KEYS[1], tonumber(current) + tonumber(ARGV[1]))
    end
    return nil
    """
    redis.eval(script, 1, 'counter', 5)
  • μ „λ¬Έ κΈ°λŠ₯ ν™œμš©: Redis λͺ¨λ“ˆ 및 ν™•μž₯ κΈ°λŠ₯ ν™œμš©

    • RedisSearch: μ „λ¬Έ 검색 μ—”μ§„
    • RedisTimeSeries: μ‹œκ³„μ—΄ 데이터 처리
    • RedisGears: μ„œλ²„μΈ‘ 데이터 처리 μ—”μ§„
    • RedisAI: λ¨Έμ‹ λŸ¬λ‹ λͺ¨λΈ ν˜ΈμŠ€νŒ…
  • ν™•μž₯μ„± κ³„νš: μ²˜μŒλΆ€ν„° ν™•μž₯성을 κ³ λ €ν•œ 섀계

    • 샀딩 ν‚€ 식별
    • ν΄λŸ¬μŠ€ν„° ν˜Έν™˜ λͺ…λ Ήμ–΄ μ‚¬μš©
    • 닀쀑 λ°μ΄ν„°λ² μ΄μŠ€ λŒ€μ‹  ν‚€ 접두사 μ‚¬μš©

<br/>

---
⚠️ **GitHub.com Fallback** ⚠️