Skip to content

필사 모드: Redis 완전 가이드 2025: 캐싱 전략, 데이터 구조, Pub/Sub, 그리고 Redis Stack까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

Redis는 2025년 현재 가장 널리 사용되는 인메모리 데이터 스토어입니다. 단순한 캐시를 넘어 메시지 브로커, 세션 스토어, 실시간 리더보드, Rate Limiter, 분산 잠금까지 다양한 역할을 수행합니다. Redis 7.4의 새 기능과 Redis Stack의 등장, 그리고 Valkey 포크 논쟁까지 — 이 글에서 Redis의 모든 것을 다룹니다.

1. Redis 개요

Redis란?

Redis(Remote Dictionary Server)는 인메모리 키-값 데이터 스토어입니다. 모든 데이터를 메모리에 저장하므로 마이크로초 단위의 응답 시간을 제공합니다.

Redis 7.4 주요 기능

- **Redis Functions** — Lua 스크립트의 진화. 라이브러리 형태로 관리

- **ACL v2** — 세분화된 접근 제어

- **Client-side caching** — 클라이언트 측 캐시 무효화 지원

- **Multi-part AOF** — 지속성 향상

Valkey 포크 이야기

2024년 Redis가 라이선스를 변경하면서(SSPL + RSALv2), Linux Foundation은 Redis 7.2를 기반으로 Valkey를 포크했습니다. AWS, Google, Oracle 등이 Valkey를 지원합니다. 현재 두 프로젝트는 호환성을 유지하지만, 장기적으로 분기될 가능성이 있습니다.

2. 기본 데이터 구조 5가지

2.1 String

가장 기본적인 데이터 타입. 최대 512MB까지 저장 가능합니다.

기본 SET/GET

SET user:1:name "Alice"

GET user:1:name

숫자 증감 (원자적 연산)

SET page:views 0

INCR page:views # 1

INCRBY page:views 10 # 11

만료 시간 설정

SET session:abc123 "user_data" EX 3600 # 1시간 후 만료

TTL session:abc123 # 남은 시간 확인

SET 옵션

SET lock:resource "owner1" NX EX 30 # NX: 키가 없을 때만 설정

SET user:1:name "Bob" XX # XX: 키가 있을 때만 업데이트

**활용 사례**: 세션 토큰, 카운터, 임시 데이터, 분산 잠금

2.2 List

이중 연결 리스트. 양쪽 끝에서 O(1) push/pop 가능합니다.

기본 연산

LPUSH queue:emails "email1" "email2" "email3"

RPOP queue:emails # "email1" (FIFO 큐)

범위 조회

LRANGE queue:emails 0 -1 # 모든 요소

블로킹 팝 (메시지 큐로 활용)

BRPOP queue:emails 30 # 30초 대기 후 팝

트리밍 (최근 N개 유지)

LPUSH notifications:user1 "new_msg"

LTRIM notifications:user1 0 99 # 최근 100개만 유지

**활용 사례**: 메시지 큐, 최근 활동 피드, 작업 큐

2.3 Set

순서 없는 고유 요소의 집합. 집합 연산(합집합, 교집합, 차집합) 지원합니다.

기본 연산

SADD tags:post:1 "python" "redis" "backend"

SADD tags:post:2 "python" "django" "orm"

멤버십 확인

SISMEMBER tags:post:1 "python" # 1 (true)

집합 연산

SINTER tags:post:1 tags:post:2 # "python" (교집합)

SUNION tags:post:1 tags:post:2 # 합집합

SDIFF tags:post:1 tags:post:2 # tags:post:1에만 있는 요소

랜덤 추출

SRANDMEMBER tags:post:1 2 # 랜덤 2개

**활용 사례**: 태그 시스템, 고유 방문자 추적, 친구 관계, 추천 시스템

2.4 Sorted Set (ZSet)

점수(score)로 정렬된 고유 요소의 집합. 리더보드에 최적입니다.

리더보드 구현

ZADD leaderboard 1500 "player:alice"

ZADD leaderboard 2300 "player:bob"

ZADD leaderboard 1800 "player:charlie"

랭킹 조회 (높은 점수 순)

ZREVRANGE leaderboard 0 2 WITHSCORES

1) "player:bob" 2) "2300"

3) "player:charlie" 4) "1800"

5) "player:alice" 6) "1500"

특정 멤버 랭킹 (0부터 시작)

ZREVRANK leaderboard "player:alice" # 2

점수 증가

ZINCRBY leaderboard 500 "player:alice" # 2000

범위 검색

ZRANGEBYSCORE leaderboard 1500 2000 WITHSCORES

**활용 사례**: 리더보드, 우선순위 큐, 시간순 이벤트, Rate Limiting

2.5 Hash

필드-값 쌍의 맵. 객체를 표현하기에 적합합니다.

사용자 프로필 저장

HSET user:1 name "Alice" email "alice@example.com" age "30" role "admin"

개별 필드 조회

HGET user:1 name # "Alice"

전체 조회

HGETALL user:1

필드 증감

HINCRBY user:1 age 1 # 31

존재 확인

HEXISTS user:1 email # 1 (true)

여러 필드 한 번에

HMGET user:1 name email role

**활용 사례**: 사용자 프로필, 설정 값, 세션 데이터, 장바구니

3. 고급 데이터 구조

3.1 HyperLogLog

대량의 고유 요소 수를 추정하는 확률적 자료 구조. 메모리 12KB로 최대 2의 64승 개 카운트 가능(오차율 0.81%).

고유 방문자 수 추정

PFADD visitors:2025-03-23 "user1" "user2" "user3"

PFADD visitors:2025-03-23 "user1" "user4" # user1은 중복

PFCOUNT visitors:2025-03-23 # 4

여러 날 합산

PFMERGE visitors:week visitors:2025-03-23 visitors:2025-03-24

PFCOUNT visitors:week

3.2 Bitmap

비트 단위 연산. 불리언 상태 추적에 메모리 효율적입니다.

일일 출석 체크

SETBIT attendance:2025-03-23 1001 1 # 사용자 1001 출석

SETBIT attendance:2025-03-23 1002 1

SETBIT attendance:2025-03-23 1003 0 # 결석

출석 여부 확인

GETBIT attendance:2025-03-23 1001 # 1

출석 인원 수

BITCOUNT attendance:2025-03-23 # 2

연속 출석 (AND 연산)

BITOP AND consecutive attendance:2025-03-22 attendance:2025-03-23

BITCOUNT consecutive

3.3 Geospatial

위치 기반 데이터. 반경 검색, 거리 계산을 지원합니다.

위치 추가 (경도, 위도)

GEOADD stores 126.9784 37.5665 "gangnam-store"

GEOADD stores 127.0276 37.4979 "samsung-store"

GEOADD stores 126.9316 37.5563 "hongdae-store"

거리 계산

GEODIST stores "gangnam-store" "hongdae-store" km # ~5.2 km

반경 검색 (Redis 6.2+)

GEOSEARCH stores FROMLONLAT 126.9784 37.5665 BYRADIUS 10 km ASC COUNT 5

3.4 Redis Streams

로그 기반 메시지 구조. Kafka와 유사하게 Consumer Group을 지원합니다.

메시지 추가

XADD events * type "order" user_id "123" amount "50000"

XADD events * type "payment" user_id "123" status "completed"

읽기

XRANGE events - + COUNT 10

Consumer Group 생성

XGROUP CREATE events analytics-group $ MKSTREAM

Consumer로 읽기

XREADGROUP GROUP analytics-group consumer-1 COUNT 5 BLOCK 2000 STREAMS events >

ACK (처리 완료)

XACK events analytics-group "1679000000000-0"

미처리 메시지 확인

XPENDING events analytics-group

4. 캐싱 패턴

4.1 Cache-Aside (Lazy Loading)

가장 보편적인 패턴. 애플리케이션이 캐시를 직접 관리합니다.

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def get_user(user_id: int) -> dict:

cache_key = f"user:{user_id}"

1. 캐시 확인

cached = r.get(cache_key)

if cached:

return json.loads(cached)

2. 캐시 미스 -> DB 조회

user = db.query(User).filter(User.id == user_id).first()

if not user:

return None

3. 캐시에 저장

r.setex(cache_key, 3600, json.dumps(user.to_dict()))

return user.to_dict()

def update_user(user_id: int, data: dict):

DB 업데이트

db.query(User).filter(User.id == user_id).update(data)

db.commit()

캐시 무효화

r.delete(f"user:{user_id}")

4.2 Write-Through

쓰기 시 캐시와 DB를 동시에 업데이트합니다.

def save_user(user_id: int, data: dict):

cache_key = f"user:{user_id}"

DB와 캐시 동시 업데이트

db.query(User).filter(User.id == user_id).update(data)

db.commit()

r.setex(cache_key, 3600, json.dumps(data))

4.3 Write-Behind (Write-Back)

쓰기를 캐시에 먼저 하고, 비동기로 DB에 반영합니다.

def save_user_async(user_id: int, data: dict):

cache_key = f"user:{user_id}"

캐시에 먼저 쓰기

r.setex(cache_key, 3600, json.dumps(data))

비동기로 DB에 반영 (Celery 등)

sync_to_db_task.delay(user_id, data)

4.4 Read-Through

캐시 라이브러리가 DB 조회를 대신 처리합니다.

캐싱 패턴 비교

| 패턴 | 읽기 성능 | 쓰기 성능 | 일관성 | 복잡도 |

|------|---------|---------|--------|-------|

| Cache-Aside | 높음 | 보통 | 최종 일관성 | 낮음 |

| Write-Through | 높음 | 낮음 | 강한 일관성 | 중간 |

| Write-Behind | 높음 | 높음 | 최종 일관성 | 높음 |

| Read-Through | 높음 | 보통 | 최종 일관성 | 중간 |

5. 캐시 무효화

5.1 TTL 기반

가장 단순한 방법. 일정 시간 후 자동 삭제됩니다.

SET product:123 "data" EX 300 # 5분 후 만료

5.2 이벤트 기반 무효화

데이터 변경 시 명시적으로 캐시를 삭제합니다.

def update_product(product_id: int, data: dict):

db.update(product_id, data)

관련 캐시 모두 삭제

r.delete(f"product:{product_id}")

r.delete(f"product_list:category:{data['category_id']}")

r.delete("product_list:featured")

5.3 버전 키

키에 버전을 포함하여 일괄 무효화합니다.

def get_product_list(category_id: int) -> list:

version = r.get(f"product_version:{category_id}") or "1"

cache_key = f"products:cat:{category_id}:v:{version}"

cached = r.get(cache_key)

if cached:

return json.loads(cached)

products = db.query(Product).filter(Product.category_id == category_id).all()

r.setex(cache_key, 3600, json.dumps([p.to_dict() for p in products]))

return [p.to_dict() for p in products]

def invalidate_category(category_id: int):

버전을 증가시켜 기존 캐시를 무효화

r.incr(f"product_version:{category_id}")

5.4 Thundering Herd 방지

캐시 만료 시 동시에 많은 요청이 DB로 몰리는 문제를 방지합니다.

def get_with_jitter(key: str, ttl: int = 3600) -> dict:

cached = r.get(key)

if cached:

return json.loads(cached)

분산 잠금으로 하나의 요청만 DB 조회

lock_key = f"lock:{key}"

if r.set(lock_key, "1", nx=True, ex=10):

try:

data = fetch_from_db(key)

TTL에 랜덤 지터 추가

jitter = random.randint(0, 300)

r.setex(key, ttl + jitter, json.dumps(data))

return data

finally:

r.delete(lock_key)

else:

다른 프로세스가 갱신 중 — 짧은 대기 후 재시도

time.sleep(0.1)

return get_with_jitter(key, ttl)

6. Pub/Sub와 Streams

6.1 Pub/Sub 기본

구독자

SUBSCRIBE notifications:user:123

발행자

PUBLISH notifications:user:123 "You have a new message!"

패턴 구독

PSUBSCRIBE notifications:*

Python 구독자

r = redis.Redis()

pubsub = r.pubsub()

pubsub.subscribe("notifications:user:123")

for message in pubsub.listen():

if message["type"] == "message":

print(f"Received: {message['data']}")

6.2 Redis Streams vs Kafka

| 기능 | Redis Streams | Kafka |

|------|-------------|-------|

| 메시지 지속성 | 메모리 + AOF | 디스크 |

| 처리량 | 수만/초 | 수십만/초 |

| Consumer Group | 지원 | 지원 |

| 메시지 재생 | 지원 | 지원 |

| 파티셔닝 | 미지원 | 지원 |

| 운영 복잡도 | 낮음 | 높음 |

| 적합한 규모 | 중소 | 대규모 |

6.3 Streams를 활용한 이벤트 드리븐 패턴

r = redis.Redis(decode_responses=True)

이벤트 발행

def publish_event(stream: str, event_type: str, data: dict):

r.xadd(stream, {"type": event_type, **data}, maxlen=10000)

Consumer Group 기반 처리

def consume_events(stream: str, group: str, consumer: str):

그룹 생성 (이미 존재하면 무시)

try:

r.xgroup_create(stream, group, id="0", mkstream=True)

except redis.ResponseError:

pass

while True:

messages = r.xreadgroup(

group, consumer,

{stream: ">"},

count=10,

block=5000,

)

for stream_name, entries in messages:

for msg_id, fields in entries:

try:

process_event(fields)

r.xack(stream_name, group, msg_id)

except Exception as e:

print(f"Error processing {msg_id}: {e}")

def process_event(fields: dict):

event_type = fields.get("type")

if event_type == "order_created":

handle_order(fields)

elif event_type == "payment_completed":

handle_payment(fields)

7. Redis Stack

Redis Stack은 Redis에 JSON, Search, TimeSeries, Bloom Filter 모듈을 추가한 확장 버전입니다.

7.1 RedisJSON

JSON 문서 저장

JSON.SET user:1 $ '{"name":"Alice","age":30,"address":{"city":"Seoul","zip":"06000"},"tags":["python","redis"]}'

경로 기반 조회

JSON.GET user:1 $.name # "Alice"

JSON.GET user:1 $.address.city # "Seoul"

부분 업데이트

JSON.SET user:1 $.age 31

JSON.ARRAPPEND user:1 $.tags '"fastapi"'

숫자 증감

JSON.NUMINCRBY user:1 $.age 1

7.2 RediSearch (전문 검색)

인덱스 생성

FT.CREATE idx:products

ON JSON

PREFIX 1 product:

SCHEMA

$.name AS name TEXT WEIGHT 5.0

$.description AS description TEXT

$.price AS price NUMERIC SORTABLE

$.category AS category TAG

문서 추가

JSON.SET product:1 $ '{"name":"Redis in Action","description":"Complete guide to Redis","price":45000,"category":"book"}'

JSON.SET product:2 $ '{"name":"Python Cookbook","description":"Python recipes and patterns","price":38000,"category":"book"}'

검색

FT.SEARCH idx:products "Redis guide"

FT.SEARCH idx:products "@category:{book} @price:[30000 50000]"

FT.SEARCH idx:products "@name:(Python)" SORTBY price ASC

7.3 RedisTimeSeries

시계열 데이터 생성

TS.CREATE sensor:temperature:1 RETENTION 86400000 LABELS sensor_id 1 type temperature

데이터 추가

TS.ADD sensor:temperature:1 * 23.5

TS.ADD sensor:temperature:1 * 24.1

TS.ADD sensor:temperature:1 * 22.8

범위 조회

TS.RANGE sensor:temperature:1 - + COUNT 10

집계 (5분 평균)

TS.RANGE sensor:temperature:1 - + AGGREGATION avg 300000

다운샘플링 규칙

TS.CREATERULE sensor:temperature:1 sensor:temperature:1:avg AGGREGATION avg 300000

8. Lua 스크립팅

8.1 기본 Lua 스크립트

원자적 읽기-수정-쓰기

EVAL "

local current = redis.call('GET', KEYS[1])

if current then

local new_val = tonumber(current) + tonumber(ARGV[1])

redis.call('SET', KEYS[1], new_val)

return new_val

end

return nil

" 1 counter 5

8.2 Rate Limiter (Sliding Window)

Python + Lua Rate Limiter

RATE_LIMIT_SCRIPT = """

local key = KEYS[1]

local limit = tonumber(ARGV[1])

local window = tonumber(ARGV[2])

local now = tonumber(ARGV[3])

-- 윈도우 밖의 오래된 요청 제거

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 현재 요청 수 확인

local count = redis.call('ZCARD', key)

if count < limit then

-- 허용: 새 요청 추가

redis.call('ZADD', key, now, now .. ':' .. math.random())

redis.call('EXPIRE', key, window)

return 1

else

-- 거부

return 0

end

"""

r = redis.Redis()

rate_limit_sha = r.script_load(RATE_LIMIT_SCRIPT)

def is_allowed(user_id: str, limit: int = 100, window: int = 60) -> bool:

key = f"ratelimit:{user_id}"

now = int(time.time() * 1000)

result = r.evalsha(rate_limit_sha, 1, key, limit, window * 1000, now)

return bool(result)

8.3 분산 잠금 (Redlock 알고리즘)

class DistributedLock:

def __init__(self, redis_client: redis.Redis, resource: str, ttl: int = 10):

self.redis = redis_client

self.resource = f"lock:{resource}"

self.ttl = ttl

self.token = str(uuid.uuid4())

def acquire(self) -> bool:

return bool(self.redis.set(

self.resource, self.token,

nx=True, ex=self.ttl,

))

def release(self) -> bool:

Lua 스크립트로 원자적 확인 + 삭제

script = """

if redis.call('GET', KEYS[1]) == ARGV[1] then

return redis.call('DEL', KEYS[1])

end

return 0

"""

return bool(self.redis.eval(script, 1, self.resource, self.token))

def __enter__(self):

if not self.acquire():

raise Exception("Could not acquire lock")

return self

def __exit__(self, *args):

self.release()

사용

r = redis.Redis()

with DistributedLock(r, "order:process:123"):

임계 영역 — 하나의 프로세스만 실행

process_order(123)

9. Redis Cluster

9.1 해시 슬롯

Redis Cluster는 16384개의 해시 슬롯으로 데이터를 분산합니다. 키의 CRC16 해시값을 16384로 나눈 나머지가 슬롯 번호입니다.

노드 구성 예시:

Node A: 슬롯 0-5460

Node B: 슬롯 5461-10922

Node C: 슬롯 10923-16383

각 노드에 1개의 복제본(Replica)을 추가하면:

Node A -> Replica A'

Node B -> Replica B'

Node C -> Replica C'

9.2 클러스터 설정

클러스터 생성 (6노드: 3 마스터 + 3 레플리카)

redis-cli --cluster create \

127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \

127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \

--cluster-replicas 1

클러스터 상태 확인

redis-cli -c -p 7001 cluster info

redis-cli -c -p 7001 cluster nodes

9.3 Failover와 Resharding

수동 페일오버

redis-cli -c -p 7004 cluster failover

리샤딩 (슬롯 이동)

redis-cli --cluster reshard 127.0.0.1:7001

노드 추가

redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001

노드 제거

redis-cli --cluster del-node 127.0.0.1:7001 <node-id>

9.4 해시 태그

같은 슬롯에 키를 배치하여 다중 키 연산을 가능하게 합니다.

해시 태그 사용 — 중괄호 안의 부분으로 슬롯 결정

SET {user:1}:profile "data"

SET {user:1}:settings "data"

SET {user:1}:sessions "data"

이 세 키는 같은 슬롯에 배치됨 -> MULTI 키 연산 가능

10. Redis 실전 활용

10.1 세션 스토어

r = redis.Redis(decode_responses=True)

def create_session(user_id: int, data: dict) -> str:

session_id = str(uuid.uuid4())

session_data = {

"user_id": str(user_id),

"created_at": str(time.time()),

**data,

}

r.hset(f"session:{session_id}", mapping=session_data)

r.expire(f"session:{session_id}", 86400) # 24시간

return session_id

def get_session(session_id: str) -> dict | None:

data = r.hgetall(f"session:{session_id}")

if not data:

return None

접근 시 TTL 갱신

r.expire(f"session:{session_id}", 86400)

return data

def destroy_session(session_id: str):

r.delete(f"session:{session_id}")

10.2 Rate Limiter (Fixed Window)

def check_rate_limit(user_id: str, limit: int = 100, window: int = 60) -> bool:

key = f"rate:{user_id}:{int(time.time()) // window}"

pipe = r.pipeline()

pipe.incr(key)

pipe.expire(key, window)

count, _ = pipe.execute()

return count <= limit

10.3 리더보드

class Leaderboard:

def __init__(self, name: str):

self.key = f"leaderboard:{name}"

def add_score(self, player_id: str, score: float):

r.zadd(self.key, {player_id: score})

def increment_score(self, player_id: str, delta: float):

r.zincrby(self.key, delta, player_id)

def get_rank(self, player_id: str) -> int | None:

rank = r.zrevrank(self.key, player_id)

return rank + 1 if rank is not None else None

def get_top(self, count: int = 10) -> list[tuple[str, float]]:

return r.zrevrange(self.key, 0, count - 1, withscores=True)

def get_around(self, player_id: str, count: int = 5) -> list:

rank = r.zrevrank(self.key, player_id)

if rank is None:

return []

start = max(0, rank - count)

end = rank + count

return r.zrevrange(self.key, start, end, withscores=True)

사용

lb = Leaderboard("weekly")

lb.add_score("player:alice", 1500)

lb.increment_score("player:alice", 200)

print(lb.get_top(10))

print(lb.get_rank("player:alice"))

10.4 작업 큐 (Simple Job Queue)

def enqueue_job(queue: str, job_data: dict):

job = {

"id": str(uuid.uuid4()),

"data": job_data,

"created_at": time.time(),

}

r.lpush(f"queue:{queue}", json.dumps(job))

def dequeue_job(queue: str, timeout: int = 30) -> dict | None:

result = r.brpop(f"queue:{queue}", timeout=timeout)

if result:

_, job_json = result

return json.loads(job_json)

return None

def worker(queue: str):

while True:

job = dequeue_job(queue)

if job:

try:

process_job(job["data"])

except Exception as e:

실패 시 재시도 큐에 추가

r.lpush(f"queue:{queue}:failed", json.dumps(job))

11. Redis vs Memcached vs DragonflyDB

| 기능 | Redis | Memcached | DragonflyDB |

|------|-------|-----------|-------------|

| 데이터 구조 | 다양 (String, List, Set 등) | String만 | Redis 호환 |

| 지속성 | RDB + AOF | 없음 | 스냅샷 |

| 클러스터링 | Redis Cluster | 클라이언트 측 | 단일 노드 (멀티스레드) |

| 멀티스레드 | 단일 스레드 (I/O 스레드) | 멀티스레드 | 멀티스레드 |

| 메모리 효율 | 보통 | 높음 | 높음 |

| Pub/Sub | 지원 | 미지원 | 지원 |

| Lua 스크립트 | 지원 | 미지원 | 지원 |

| 처리량 | ~100K ops/s | ~100K ops/s | ~400K ops/s |

| 최대 값 크기 | 512MB | 1MB | 512MB |

| 적합한 사례 | 범용 | 단순 캐시 | 고성능 단일 노드 |

12. 클라이언트 라이브러리 코드 예시

12.1 Spring Data Redis (Java)

@Configuration

public class RedisConfig {

@Bean

public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

RedisTemplate<String, Object> template = new RedisTemplate<>();

template.setConnectionFactory(factory);

template.setKeySerializer(new StringRedisSerializer());

template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

return template;

}

}

@Service

public class UserCacheService {

private final RedisTemplate<String, Object> redisTemplate;

private final Duration TTL = Duration.ofHours(1);

public UserCacheService(RedisTemplate<String, Object> redisTemplate) {

this.redisTemplate = redisTemplate;

}

public void cacheUser(String userId, UserDto user) {

String key = "user:" + userId;

redisTemplate.opsForValue().set(key, user, TTL);

}

public UserDto getCachedUser(String userId) {

String key = "user:" + userId;

return (UserDto) redisTemplate.opsForValue().get(key);

}

// Sorted Set 리더보드

public void addScore(String leaderboard, String playerId, double score) {

redisTemplate.opsForZSet().add("lb:" + leaderboard, playerId, score);

}

public Set<ZSetOperations.TypedTuple<Object>> getTopPlayers(String leaderboard, int count) {

return redisTemplate.opsForZSet().reverseRangeWithScores("lb:" + leaderboard, 0, count - 1);

}

}

12.2 ioredis (Node.js)

const redis = new Redis({

host: 'localhost',

port: 6379,

retryStrategy: (times) => Math.min(times * 50, 2000),

maxRetriesPerRequest: 3,

});

// 기본 캐싱

async function getUser(userId) {

const cacheKey = `user:${userId}`;

const cached = await redis.get(cacheKey);

if (cached) return JSON.parse(cached);

const user = await db.findUser(userId);

if (user) {

await redis.setex(cacheKey, 3600, JSON.stringify(user));

}

return user;

}

// Pipeline (배치 처리)

async function getMultipleUsers(userIds) {

const pipeline = redis.pipeline();

userIds.forEach(id => pipeline.get(`user:${id}`));

const results = await pipeline.exec();

return results.map(([err, val]) => val ? JSON.parse(val) : null);

}

// Pub/Sub

const subscriber = new Redis();

subscriber.subscribe('notifications', (err, count) => {

console.log(`Subscribed to ${count} channels`);

});

subscriber.on('message', (channel, message) => {

console.log(`Received on ${channel}: ${message}`);

});

// 발행

await redis.publish('notifications', JSON.stringify({ type: 'alert', message: 'Server update' }));

12.3 redis-py (Python)

비동기 Redis 클라이언트

pool = aioredis.ConnectionPool.from_url(

"redis://localhost:6379",

max_connections=20,

decode_responses=True,

)

r = aioredis.Redis(connection_pool=pool)

파이프라인

async def batch_operations():

async with r.pipeline(transaction=True) as pipe:

pipe.set("key1", "value1")

pipe.set("key2", "value2")

pipe.get("key1")

results = await pipe.execute()

return results

Pub/Sub (비동기)

async def subscribe_handler():

pubsub = r.pubsub()

await pubsub.subscribe("events")

async for message in pubsub.listen():

if message["type"] == "message":

data = json.loads(message["data"])

await process_event(data)

캐시 데코레이터

def redis_cache(ttl: int = 3600):

def decorator(func):

@functools.wraps(func)

async def wrapper(*args, **kwargs):

cache_key = f"cache:{func.__name__}:{args}:{kwargs}"

cached = await r.get(cache_key)

if cached:

return json.loads(cached)

result = await func(*args, **kwargs)

await r.setex(cache_key, ttl, json.dumps(result))

return result

return wrapper

return decorator

@redis_cache(ttl=600)

async def get_product_list(category_id: int):

return await db.get_products(category_id)

13. 면접 질문 15선

Q1. Redis가 싱글 스레드인데 어떻게 빠른가요?

Redis는 이벤트 루프 기반의 싱글 스레드로 동작하며, 모든 데이터가 메모리에 있어 디스크 I/O가 없습니다. 또한 epoll/kqueue 기반의 I/O 멀티플렉싱으로 수천 개의 연결을 효율적으로 처리합니다. Redis 6.0부터 I/O 스레드를 지원하여 네트워크 처리를 병렬화합니다.

Q2. Cache-Aside와 Write-Through의 차이는?

Cache-Aside는 애플리케이션이 캐시를 직접 관리합니다(읽기 시 캐시 미스면 DB 조회 후 캐시 저장). Write-Through는 쓰기 시 캐시와 DB를 동시에 업데이트합니다. Cache-Aside는 구현이 간단하고, Write-Through는 데이터 일관성이 더 강합니다.

Q3. Redis의 지속성 방식(RDB, AOF)을 설명하세요.

RDB(Redis Database)는 특정 시점의 스냅샷을 디스크에 저장합니다. AOF(Append Only File)는 모든 쓰기 연산을 로그로 기록합니다. RDB는 빠른 복구에 적합하고, AOF는 데이터 손실을 최소화합니다. 둘 다 사용하는 것이 권장됩니다.

Q4. Redis Cluster에서 MGET이 실패하는 이유는?

Redis Cluster에서 키는 해시 슬롯에 분산됩니다. MGET의 키들이 서로 다른 슬롯에 있으면 단일 명령으로 처리할 수 없습니다. 해시 태그(중괄호)를 사용하여 같은 슬롯에 키를 배치하거나, 클라이언트에서 여러 요청으로 분할해야 합니다.

Q5. Thundering Herd 문제란?

캐시 키가 만료되는 순간 수많은 요청이 동시에 DB로 몰리는 현상입니다. 해결 방법: 분산 잠금으로 하나의 요청만 DB 접근 허용, TTL에 랜덤 지터 추가, 백그라운드에서 캐시 사전 갱신(proactive refresh).

Q6. Redis Pub/Sub과 Streams의 차이는?

Pub/Sub은 Fire-and-Forget 방식으로, 구독자가 없으면 메시지가 소실됩니다. Streams는 메시지를 영구 저장하고, Consumer Group으로 안정적인 메시지 처리를 보장합니다. Streams는 ACK, 재처리, 이력 조회가 가능합니다.

Q7. Redis에서 큰 키(Big Key)가 문제가 되는 이유는?

큰 키는 삭제 시 Redis를 블로킹하고, 네트워크 대역폭을 소모하며, 클러스터에서 데이터 편중을 일으킵니다. UNLINK 명령으로 비동기 삭제하고, 큰 해시를 여러 작은 해시로 분할하는 것이 좋습니다.

Q8. Redis의 메모리 정책(eviction policy)을 설명하세요.

maxmemory에 도달하면 eviction 정책에 따라 키를 제거합니다. noeviction(새 쓰기 거부), allkeys-lru(LRU), allkeys-lfu(LFU), volatile-lru(TTL 있는 키 중 LRU), volatile-ttl(만료 시간 가까운 키 우선) 등이 있습니다.

Q9. Lua 스크립트를 사용하는 이유는?

Redis에서 여러 명령을 원자적으로 실행하기 위해 사용합니다. 네트워크 왕복을 줄이고, 서버 측에서 복잡한 로직을 원자적으로 실행합니다. 대표적 사례: Rate Limiter, 분산 잠금 해제, 조건부 업데이트.

Q10. Redis Sentinel과 Redis Cluster의 차이는?

Sentinel은 마스터-슬레이브 구조의 고가용성 솔루션입니다(자동 페일오버). Cluster는 데이터를 여러 노드에 분산하는 수평 확장 솔루션입니다. 소규모에는 Sentinel, 대규모 데이터에는 Cluster가 적합합니다.

Q11. Redis의 파이프라인이란?

여러 명령을 한 번에 서버로 보내고, 응답을 일괄적으로 받는 기법입니다. 네트워크 왕복 횟수를 줄여 성능을 크게 향상시킵니다. 100개의 명령을 개별로 보내면 100번 왕복하지만, 파이프라인은 1번입니다.

Q12. Redis에서 키 만료 처리는 어떻게 동작하나요?

두 가지 방식을 조합합니다. Lazy expiration은 키에 접근할 때 만료 여부를 확인합니다. Active expiration은 100ms마다 임의의 만료 키를 샘플링하여 삭제합니다. 이 조합으로 만료된 키가 메모리를 과도하게 차지하는 것을 방지합니다.

Q13. Redlock 알고리즘이란?

Martin Kleppmann이 비판하고 Salvatore Sanfilippo가 제안한 분산 잠금 알고리즘입니다. N개의 독립적 Redis 인스턴스에서 과반수(N/2+1)에 성공적으로 잠금을 획득하면 잠금이 유효합니다. 단일 인스턴스 잠금보다 안전하지만, 완벽하지는 않습니다.

Q14. Redis의 slow log란?

실행 시간이 특정 임계값을 초과하는 명령을 기록합니다. `slowlog-log-slower-than`으로 임계값(마이크로초)을 설정합니다. SLOWLOG GET으로 느린 명령을 확인하고 최적화할 수 있습니다.

Q15. Redis를 세션 스토어로 사용할 때의 장단점은?

장점: 빠른 읽기/쓰기, TTL 자동 만료, 수평 확장 가능, 서버 간 세션 공유. 단점: 메모리 비용, Redis 장애 시 세션 손실 위험, 네트워크 의존성. 지속성(AOF)과 복제를 활성화하여 리스크를 줄일 수 있습니다.

14. 퀴즈

NX(Not eXists)는 키가 존재하지 않을 때만 설정합니다. 분산 잠금 획득에 사용됩니다. XX(eXists)는 키가 이미 존재할 때만 업데이트합니다. 기존 값을 안전하게 갱신할 때 사용합니다.

ZADD의 시간 복잡도는 O(log N)입니다. Sorted Set은 내부적으로 Skip List를 사용하여 정렬 상태를 유지하며, Skip List의 삽입 연산이 O(log N)이기 때문입니다.

KEYS는 모든 키를 순회하는 O(N) 연산으로, 키가 많으면 Redis를 장시간 블로킹합니다. 대신 SCAN 명령을 사용해야 합니다. SCAN은 커서 기반으로 점진적으로 순회하여 블로킹을 방지합니다.

낙관적 잠금(Optimistic Locking)을 구현합니다. WATCH로 키를 감시하고, MULTI로 트랜잭션을 시작하고, EXEC로 실행합니다. WATCH한 키가 다른 클라이언트에 의해 변경되면 트랜잭션이 실패합니다. 충돌이 적은 상황에서 효율적입니다.

정확한 고유 카운트는 O(N) 메모리가 필요합니다(모든 요소 저장). HyperLogLog는 어떤 크기의 집합이든 12KB 고정 메모리만 사용합니다. 1억 명의 고유 방문자를 추적할 때, Set은 수 GB가 필요하지만 HyperLogLog는 12KB로 충분합니다. 대부분의 분석 사례에서 0.81% 오차는 허용 가능합니다.

참고 자료

1. [Redis 공식 문서](https://redis.io/docs/)

2. [Redis University](https://university.redis.com/)

3. [Redis Stack 문서](https://redis.io/docs/stack/)

4. [Valkey 프로젝트](https://valkey.io/)

5. [Redis in Action (Manning)](https://www.manning.com/books/redis-in-action)

6. [ioredis GitHub](https://github.com/redis/ioredis)

7. [redis-py 문서](https://redis-py.readthedocs.io/)

8. [Spring Data Redis](https://spring.io/projects/spring-data-redis)

9. [Redis Best Practices](https://redis.io/docs/management/optimization/)

10. [Redlock 알고리즘](https://redis.io/docs/manual/patterns/distributed-locks/)

11. [Martin Kleppmann의 Redlock 분석](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html)

12. [Redis Cluster 튜토리얼](https://redis.io/docs/management/scaling/)

13. [DragonflyDB](https://www.dragonflydb.io/)

14. [Redis Streams 가이드](https://redis.io/docs/data-types/streams/)

15. [RediSearch 문서](https://redis.io/docs/stack/search/)

현재 단락 (1/532)

Redis는 2025년 현재 가장 널리 사용되는 인메모리 데이터 스토어입니다. 단순한 캐시를 넘어 메시지 브로커, 세션 스토어, 실시간 리더보드, Rate Limiter, 분산 잠...

작성 글자: 0원문 글자: 20,742작성 단락: 0/532