Redis, 캐싱 그 이상: 데이터 구조, Pub/Sub, 그리고 실시간 애플리케이션
Redis를 단순한 캐시 이상으로 활용하는 방법을 알아보세요 — 데이터 구조, pub/sub 메시징, 스트림, 그리고 빠르고 확장 가능한 애플리케이션을 구축하기 위한 패턴을 탐구합니다.
대부분의 개발자는 Redis를 "그 빠른 캐시"로만 알고 있습니다. 하지만 Redis는 세션 관리, 실시간 리더보드, 속도 제한, 메시지 큐 등 훨씬 더 많은 기능을 처리할 수 있는 완전한 기능을 갖춘 인메모리 데이터 구조 서버입니다 — 모두 밀리초 미만의 지연 시간으로 말이죠.
Redis가 빠른 이유
Redis는 모든 데이터를 메모리에 저장하고, 단일 스레드 이벤트 루프로 명령을 처리합니다. 읽기 시 디스크 I/O 없음, 락 경합 없음, 컨텍스트 전환 없음. 그 결과: 보통 수준의 하드웨어에서도 초당 100,000회 이상의 작업이 가능합니다.
일반적인 지연 시간 비교:
┌─────────────────┬──────────────┐
│ Operation │ Latency │
├─────────────────┼──────────────┤
│ Redis GET │ 0.1 ms │
│ PostgreSQL SEL │ 1-5 ms │
│ REST API call │ 50-200 ms │
│ Disk read │ 5-10 ms │
└─────────────────┴──────────────┘
핵심 데이터 구조
Redis는 단순한 키-값 저장소가 아닙니다. 일반적인 애플리케이션 요구사항에 직접 매핑되는 다양한 데이터 구조를 지원합니다.
Strings — 기본 구성 요소
String은 최대 512 MB의 텍스트, 숫자, 또는 바이너리 데이터를 저장합니다:
SET user:1001:name "Alice"
GET user:1001:name # "Alice"
# 원자적 카운터
INCR page:views # 1, 2, 3, ...
INCRBY cart:total 2500 # 25.00 추가 (센트 단위 저장)
# 만료 키 (TTL)
SET session:abc123 "{...}" EX 3600 # 1시간 후 만료
TTL session:abc123 # 남은 시간(초)
Hashes — 경량 객체
Hash는 직렬화 오버헤드 없이 객체를 저장하는 데 적합합니다:
HSET user:1001 name "Alice" email "alice@example.com" plan "pro"
HGET user:1001 name # "Alice"
HGETALL user:1001 # 모든 필드와 값
HINCRBY user:1001 logins 1 # 원자적 필드 증가
Hash는 각 필드를 별도의 키로 저장하는 것보다 메모리를 10배 적게 사용합니다.
Lists — 큐와 피드
양쪽 끝에서 push/pop을 지원하는 순서가 있는 컬렉션:
LPUSH notifications:alice "New comment on your post"
LPUSH notifications:alice "You have a new follower"
LRANGE notifications:alice 0 9 # 최신 알림 10개
# 큐로 사용 (생산자/소비자)
RPUSH queue:emails "send-welcome"
LPOP queue:emails # 다음 작업 처리
Sets — 고유 컬렉션
고유한 문자열의 순서 없는 컬렉션:
SADD tags:post:42 "redis" "database" "nosql"
SMEMBERS tags:post:42 # 모든 태그
SISMEMBER tags:post:42 "redis" # true
# Set 연산
SINTER tags:post:42 tags:post:99 # 공통 태그
SUNION tags:post:42 tags:post:99 # 모든 태그 합산
Sorted Sets — 리더보드와 순위
각 멤버에 점수가 있는 자동 정렬 Set:
ZADD leaderboard 1500 "alice" 2300 "bob" 1800 "charlie"
ZREVRANGE leaderboard 0 2 WITHSCORES # 상위 3명 플레이어
ZRANK leaderboard "alice" # 순위 (0부터 시작)
ZINCRBY leaderboard 100 "alice" # Alice 100점 획득
이것이 바로 대규모 게임 리더보드, 트렌딩 게시물, 우선순위 큐가 구축되는 방식입니다.
실전 패턴
패턴 1: 세션 저장
데이터베이스 기반 세션 대신 즉각적인 조회를 위해 Redis를 사용하세요:
import redis
r = redis.Redis()
# 세션 저장
r.setex(f"session:{token}", 3600, json.dumps(user_data))
# 세션 조회
data = r.get(f"session:{token}")
왜 사용할까요? 데이터베이스 세션은 요청당 1~5ms를 추가합니다. Redis 세션은 0.1ms를 추가합니다. 초당 1,000건의 요청에서, 이는 매 초마다 5초를 절약하는 셈입니다.
패턴 2: 속도 제한
3개의 명령으로 구현하는 슬라이딩 윈도우 속도 제한:
# 사용자당 분당 100회 요청 허용
MULTI
INCR rate:user:1001
EXPIRE rate:user:1001 60
EXEC
# 확인: INCR 결과가 100을 초과하면 요청 거부
패턴 3: Cache-Aside 패턴으로 캐싱
가장 일반적인 캐싱 패턴:
def get_user(user_id):
# 1. 캐시 확인
cached = redis.get(f"user:{user_id}")
if cached:
return json.loads(cached)
# 2. 캐시 미스 → 데이터베이스 조회
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 3. 캐시 채우기 (5분 후 만료)
redis.setex(f"user:{user_id}", 300, json.dumps(user))
return user
패턴 4: 실시간 이벤트를 위한 Pub/Sub
Redis Pub/Sub은 서비스 간 실시간 메시징을 가능하게 합니다:
# 구독자 (메시지 수신 대기)
SUBSCRIBE chat:room:42
# 발행자 (메시지 전송)
PUBLISH chat:room:42 "Hello everyone!"
애플리케이션 코드에서:
# 발행자
redis.publish("notifications", json.dumps({
"type": "new_order",
"order_id": 12345
}))
# 구독자
pubsub = redis.pubsub()
pubsub.subscribe("notifications")
for message in pubsub.listen():
handle_notification(message)
패턴 5: 분산 락
여러 서버에서 발생하는 경쟁 조건 방지:
# 락 획득 (NX = 존재하지 않을 때만, EX = 만료)
SET lock:invoice:gen "worker-1" NX EX 30
# 락 해제 (소유자만 — Lua 스크립트 사용)
Redis Streams — 이벤트 소싱과 메시지 큐
Streams은 Kafka 스타일의 이벤트 로그에 대한 Redis의 답입니다:
# 이벤트 추가
XADD orders * user_id 1001 product "Widget" qty 3
XADD orders * user_id 1002 product "Gadget" qty 1
# 최신 이벤트 읽기
XRANGE orders - + COUNT 10
# 소비자 그룹 (여러 워커)
XGROUP CREATE orders processors $ MKSTREAM
XREADGROUP GROUP processors worker-1 COUNT 5 BLOCK 2000 STREAMS orders >
Streams은 소비자 그룹과 함께 영속적이고 재생 가능한 이벤트 로그를 제공합니다 — 마이크로서비스 통신에 완벽합니다.
성능 팁
1. 대량 작업에는 파이프라이닝 사용
100번의 왕복 대신, 100개의 명령을 한 번에 전송하세요:
pipe = redis.pipeline()
for i in range(100):
pipe.set(f"key:{i}", f"value:{i}")
pipe.execute() # 단일 왕복
2. 올바른 데이터 구조 선택
- 카운터가 필요한가요? →
INCR(GET + SET 대신) - 객체가 필요한가요? → Hash (직렬화된 JSON 문자열 대신)
- 고유 항목이 필요한가요? → Set (List + 중복 제거 로직 대신)
- 순위가 필요한가요? → Sorted Set (조회 후 정렬 대신)
3. 모든 것에 TTL 설정
메모리는 유한합니다. 캐시 키에는 항상 만료 시간을 설정하세요:
SET cache:api:result "{...}" EX 300 # 5분 TTL
4. 키 명명 규칙 사용
resource:id:field
user:1001:profile
cache:api:v2:products
session:abc123
queue:emails:pending
영속성: RDB vs AOF
Redis는 내구성을 위해 데이터를 디스크에 저장할 수 있습니다:
| 모드 | 작동 방식 | 트레이드오프 |
|---|---|---|
| RDB | 특정 시점 스냅샷 | 빠른 복구, 데이터 손실 가능성 |
| AOF | 모든 쓰기 작업 로깅 | 느리지만, 데이터 손실 최소화 |
| 둘 다 | RDB + AOF 결합 | 최상의 내구성 |
# redis.conf에서
save 900 1 # 900초 내 1개 키 변경 시 스냅샷
appendonly yes # AOF 활성화
appendfsync everysec # 매 초마다 Fsync
빠른 명령어 참조
특정 Redis 명령어를 찾고 싶으신가요? **Redis Cheat Sheet**를 사용해 보세요 — 데이터 구조별로 정리된 200개 이상의 명령어를 문법, 예제, 원클릭 복사 기능과 함께 제공합니다.
마무리
Redis는 단순한 캐시 그 이상입니다. 데이터 구조는 실제 애플리케이션 요구사항에 깔끔하게 매핑됩니다:
- Strings → 세션, 카운터, 간단한 캐시
- Hashes → 사용자 프로필, 설정 객체
- Lists → 큐, 활동 피드, 최근 항목
- Sets → 태그, 고유 방문자, 관계
- Sorted Sets → 리더보드, 순위, 우선순위 큐
- Streams → 이벤트 소싱, 메시지 큐
- Pub/Sub → 실시간 알림, 채팅
캐싱부터 시작하되, 거기서 멈추지 마세요. Redis는 스택에서 여러 인프라 요소를 대체할 수 있습니다 — 그것도 모두 밀리초 미만의 속도로 말이죠.