Developer Tools

Redis, 캐싱 그 이상: 데이터 구조, Pub/Sub, 그리고 실시간 애플리케이션

Redis를 단순한 캐시 이상으로 활용하는 방법을 알아보세요 — 데이터 구조, pub/sub 메시징, 스트림, 그리고 빠르고 확장 가능한 애플리케이션을 구축하기 위한 패턴을 탐구합니다.

10분 읽기

서버 룸과 빛나는 연결망

대부분의 개발자는 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는 스택에서 여러 인프라 요소를 대체할 수 있습니다 — 그것도 모두 밀리초 미만의 속도로 말이죠.