Post
Redis와 SQL의 만남: 캐싱 전략으로 데이터베이스 성능 극대화
왜 이 주제가 중요한가?
SQL 기반 애플리케이션에서 데이터베이스는 병목이 된다. 같은 쿼리를 반복 실행하면 디스크 I/O가 증가하고 응답 시간이 늘어난다.
Redis를 캐시 레이어로 추가하면 자주 조회하는 데이터를 메모리에 저장해 쿼리 실행 횟수를 줄일 수 있다. 이는 데이터베이스 부하를 크게 감소시킨다.
핵심 개념
-
캐시 계층의 역할 SQL 쿼리 결과를 Redis에 저장하고, 동일한 요청이 들어오면 데이터베이스 대신 Redis에서 즉시 반환한다.
-
캐시 무효화 전략 데이터가 변경되면 해당 캐시를 삭제해야 한다. TTL(Time To Live)을 설정하거나 이벤트 기반으로 무효화한다.
-
직렬화와 역직렬화 SQL 결과(보통 JSON)를 Redis에 저장하려면 문자열로 변환하고, 조회할 때 원래 형태로 복원해야 한다.
-
핫 데이터 식별 모든 쿼리를 캐시할 필요는 없다. 자주 조회되고 변경이 적은 데이터를 우선적으로 캐시한다.
-
캐시 스탬피드 방지 캐시 만료 시점에 동시에 많은 요청이 들어오면 데이터베이스가 과부하된다. 락이나 확률적 갱신으로 방지한다.
실전 예제
기본 캐싱 패턴
import redis
import json
from datetime import datetime
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
def get_user_by_id(user_id):
# 1. 캐시에서 먼저 확인
cache_key = f"user:{user_id}"
cached_user = redis_client.get(cache_key)
if cached_user:
print(f"캐시 히트: user_id={user_id}")
return json.loads(cached_user)
# 2. 캐시 미스 시 데이터베이스 조회
print(f"캐시 미스: 데이터베이스에서 조회 user_id={user_id}")
user = query_database(f"SELECT * FROM users WHERE id = {user_id}")
# 3. 결과를 캐시에 저장 (TTL: 3600초)
if user:
redis_client.setex(cache_key, 3600, json.dumps(user))
return user
데이터 변경 시 캐시 무효화
def update_user(user_id, new_data):
# 1. 데이터베이스 업데이트
execute_query(f"UPDATE users SET name='{new_data['name']}' WHERE id={user_id}")
# 2. 해당 캐시 삭제
cache_key = f"user:{user_id}"
redis_client.delete(cache_key)
print(f"캐시 삭제: {cache_key}")
return {"status": "success", "user_id": user_id}
캐시 스탬피드 방지 (확률적 갱신)
import random
def get_product_with_protection(product_id):
cache_key = f"product:{product_id}"
cached_product = redis_client.get(cache_key)
ttl = redis_client.ttl(cache_key)
# 캐시가 있고 만료 시간이 남아있으면 반환
if cached_product and ttl > 0:
# 만료 시간이 10% 이하 남았으면 10% 확률로 미리 갱신
if ttl < 360 and random.random() < 0.1:
refresh_cache_async(product_id)
return json.loads(cached_product)
# 캐시 미스 또는 만료
product = query_database(f"SELECT * FROM products WHERE id = {product_id}")
if product:
redis_client.setex(cache_key, 3600, json.dumps(product))
return product
자주 하는 실수
-
모든 쿼리를 캐시하려는 시도 변경이 빈번한 데이터를 캐시하면 오래된 정보를 제공하게 된다. 읽기가 많고 변경이 적은 데이터만 캐시하자.
-
캐시 무효화 로직 누락 데이터베이스를 업데이트했지만 캐시를 지우지 않으면 불일치가 발생한다. 모든 쓰기 작업 후 관련 캐시를 반드시 삭제하자.
-
직렬화 형식 불일치 JSON으로 저장했는데 pickle로 로드하려고 하면 에러가 난다. 저장과 로드 시 동일한 형식을 사용하자.
-
TTL 설정 없이 캐시 저장 TTL이 없으면 캐시가 영구 저장되어 메모리를 낭비한다. 모든 캐시에 적절한 TTL을 설정하자.
-
동시성 문제 무시 여러 스레드에서 동시에 캐시를 갱신하면 race condition이 발생한다. 락이나 원자적 연산을 사용하자.
오늘의 실습 체크리스트
- Redis 로컬 환경 설정 (docker run redis 또는 설치)
- Python redis 라이브러리 설치 (
pip install redis) - 간단한 SELECT 쿼리 결과를 Redis에 캐시하는 코드 작성
- 캐시 히트/미스 로그를 출력하는 함수 구현
- UPDATE 작업 후 캐시 삭제 로직 추가
- TTL 설정이 제대로 작동하는지 redis-cli로 확인
- 동일한 쿼리를 10번 실행해 응답 시간 차이 측정
- 캐시 무효화 없이 데이터 변경 시 문제점 재현해보기
댓글