🗄️
데이터베이스
Key-Value Store
키-값 저장소
고유한 키를 통해 값을 저장하고 조회하는 NoSQL 데이터베이스로, Redis, DynamoDB, etcd 등이 대표적이며 캐싱, 세션 관리, 설정 저장 등에 활용됩니다.
키-값 저장소
고유한 키를 통해 값을 저장하고 조회하는 NoSQL 데이터베이스로, Redis, DynamoDB, etcd 등이 대표적이며 캐싱, 세션 관리, 설정 저장 등에 활용됩니다.
Key-Value Store(키-값 저장소)는 데이터를 고유한 키와 해당 키에 연결된 값의 쌍으로 저장하는 가장 단순한 형태의 NoSQL 데이터베이스입니다. 해시 테이블과 유사한 구조로, O(1) 시간 복잡도로 데이터를 읽고 쓸 수 있어 극도로 빠른 성능을 제공합니다.
대표적인 Key-Value Store로는 Redis(인메모리, 다양한 자료구조 지원), Amazon DynamoDB(완전관리형, 서버리스), etcd(분산 시스템 설정 저장), Memcached(순수 캐시) 등이 있습니다. 각각 특화된 용도가 있어, 요구사항에 맞게 선택해야 합니다.
주요 사용 사례로는 캐싱(데이터베이스 쿼리 결과, API 응답 캐시), 세션 관리(웹 애플리케이션 사용자 세션 저장), 실시간 데이터(리더보드, 조회수, 좋아요 카운터), 분산 락(동시성 제어), 메시지 큐(Redis의 Pub/Sub, Streams) 등이 있습니다.
Key-Value Store의 한계도 있습니다. 키로만 조회 가능하여 복잡한 쿼리나 범위 검색이 어렵습니다(DynamoDB는 Sort Key로 일부 해결). 데이터 간 관계 표현이 불가하며, 트랜잭션 지원이 제한적입니다. 따라서 RDBMS와 함께 보조 저장소로 사용하는 경우가 많습니다.
import redis
import json
from datetime import timedelta
from typing import Optional, Any
# Redis 클라이언트 초기화
redis_client = redis.Redis(
host='localhost',
port=6379,
db=0,
decode_responses=True # 문자열 자동 디코딩
)
# ================================
# 1. 기본 Key-Value 연산
# ================================
def cache_user(user_id: int, user_data: dict, ttl_seconds: int = 3600):
"""사용자 정보를 캐시에 저장"""
key = f"user:{user_id}"
redis_client.setex(key, ttl_seconds, json.dumps(user_data))
def get_cached_user(user_id: int) -> Optional[dict]:
"""캐시에서 사용자 정보 조회"""
key = f"user:{user_id}"
data = redis_client.get(key)
return json.loads(data) if data else None
# 사용
cache_user(123, {"name": "홍길동", "email": "hong@example.com"})
user = get_cached_user(123)
print(user) # {'name': '홍길동', 'email': 'hong@example.com'}
# ================================
# 2. 캐시 패턴: Cache-Aside
# ================================
def get_user_with_cache(user_id: int, db_session) -> dict:
"""캐시 우선 조회, 없으면 DB에서 로드"""
cache_key = f"user:{user_id}"
# 1. 캐시 확인
cached = redis_client.get(cache_key)
if cached:
print("Cache HIT")
return json.loads(cached)
# 2. 캐시 미스 -> DB 조회
print("Cache MISS")
user = db_session.query(User).get(user_id)
if user:
user_dict = {"id": user.id, "name": user.name, "email": user.email}
# 3. 캐시에 저장 (1시간 TTL)
redis_client.setex(cache_key, 3600, json.dumps(user_dict))
return user_dict
return None
# ================================
# 3. 분산 락 구현
# ================================
import uuid
def acquire_lock(lock_name: str, timeout: int = 10) -> Optional[str]:
"""분산 락 획득"""
lock_key = f"lock:{lock_name}"
lock_id = str(uuid.uuid4())
# SET NX (Not eXists) - 키가 없을 때만 설정
acquired = redis_client.set(lock_key, lock_id, nx=True, ex=timeout)
return lock_id if acquired else None
def release_lock(lock_name: str, lock_id: str) -> bool:
"""분산 락 해제 (Lua 스크립트로 원자적 처리)"""
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
"""
result = redis_client.eval(script, 1, lock_key, lock_id)
return result == 1
# 사용
lock_id = acquire_lock("payment:order:123")
if lock_id:
try:
# 결제 처리 (동시에 하나만 실행됨)
process_payment(order_id=123)
finally:
release_lock("payment:order:123", lock_id)
# ================================
# 4. 조회수/카운터 (Atomic Operations)
# ================================
def increment_view_count(article_id: int) -> int:
"""게시글 조회수 증가 (원자적 연산)"""
key = f"article:{article_id}:views"
return redis_client.incr(key)
def get_view_count(article_id: int) -> int:
"""조회수 조회"""
key = f"article:{article_id}:views"
count = redis_client.get(key)
return int(count) if count else 0
# ================================
# 5. 리더보드 (Sorted Set 활용)
# ================================
def update_leaderboard(game_id: str, user_id: str, score: float):
"""리더보드 점수 업데이트"""
key = f"leaderboard:{game_id}"
redis_client.zadd(key, {user_id: score})
def get_top_players(game_id: str, count: int = 10) -> list:
"""상위 N명 조회 (점수 내림차순)"""
key = f"leaderboard:{game_id}"
# ZREVRANGE: 점수 높은 순으로 조회
return redis_client.zrevrange(key, 0, count - 1, withscores=True)
def get_user_rank(game_id: str, user_id: str) -> Optional[int]:
"""특정 사용자의 순위 조회 (0부터 시작)"""
key = f"leaderboard:{game_id}"
rank = redis_client.zrevrank(key, user_id)
return rank + 1 if rank is not None else None # 1부터 시작하도록
# 사용
update_leaderboard("game1", "user:123", 1500)
update_leaderboard("game1", "user:456", 2200)
top_10 = get_top_players("game1", 10)
my_rank = get_user_rank("game1", "user:123")
print(f"내 순위: {my_rank}위")