Theme:

수백만 개의 키가 있는 Redis에서, 키를 체계적으로 관리하고 안전하게 탐색하려면 어떻게 해야 할까요?

개념 정의

Redis의 키(Key)는 데이터에 접근하는 유일한 식별자입니다. 키 관리란 TTL(Time To Live) 설정, 네이밍 규칙, 키 탐색 세 가지를 체계적으로 다루는 것을 말합니다. 이 세 가지가 제대로 갖춰지지 않으면 메모리 낭비, 키 충돌, 서비스 장애로 이어질 수 있습니다.

왜 필요한가

  • 메모리는 유한합니다: TTL 없는 키가 쌓이면 메모리 부족으로 서비스가 중단됩니다
  • 키를 찾아야 합니다: 프로덕션에서 특정 패턴의 키를 안전하게 찾는 방법이 필요합니다
  • 협업이 필요합니다: 여러 서비스가 하나의 Redis를 공유하면 키 충돌 위험이 생깁니다

TTL 설정 — EXPIRE와 PEXPIRE

기본 명령어

BASH
# 초 단위 TTL 설정
SET session:abc123 "user_data"
EXPIRE session:abc123 3600          # 3600초(1시간) 후 만료

# SET과 동시에 TTL 설정 (권장)
SET session:abc123 "user_data" EX 3600

# 밀리초 단위 TTL 설정
SET rate:api:user1 1 PX 1000        # 1000밀리초(1초) 후 만료
PEXPIRE rate:api:user1 1000         # 기존 키에 밀리초 TTL 설정

# 절대 시각으로 만료 설정 (Unix timestamp)
EXPIREAT session:abc123 1711843200  # 특정 시각에 만료
PEXPIREAT session:abc123 1711843200000  # 밀리초 정밀도

TTL 확인 및 제거

BASH
# 남은 TTL 확인 (초)
TTL session:abc123
# 3500 (남은 초)
# -1   (만료 시간 없음)
# -2   (키가 존재하지 않음)

# 남은 TTL 확인 (밀리초)
PTTL session:abc123

# TTL 제거 (키는 유지, 만료만 해제)
PERSIST session:abc123

TTL 정밀도와 주의점

Redis의 TTL은 밀리초 정밀도로 관리됩니다. 하지만 몇 가지 주의할 점이 있습니다.

BASH
# 주의 1: 키를 덮어쓰면 TTL이 사라짐
SET key "value1" EX 100
SET key "value2"          # TTL이 제거됨!
TTL key                   # -1 (만료 없음)

# 해결: SET 시 KEEPTTL 옵션 사용 (6.0+)
SET key "value2" KEEPTTL  # 기존 TTL 유지

# 주의 2: RENAME은 TTL을 이전함
SET old_key "data" EX 100
RENAME old_key new_key
TTL new_key               # 기존 TTL이 유지됨

SET 명령어의 옵션 조합

BASH
# NX: 키가 없을 때만 설정 (분산 락에 사용)
SET lock:order:123 "worker-1" NX EX 30

# XX: 키가 있을 때만 설정 (업데이트 용도)
SET config:timeout "5000" XX

# GET: 이전 값을 반환하면서 새 값 설정 (6.2+)
SET counter:page "100" GET  # 이전 값 반환 후 "100"으로 설정

네이밍 컨벤션

기본 원칙: 콜론 구분 계층 구조

PLAINTEXT
{서비스}:{엔티티}:{식별자}:{속성}

실제 예시를 살펴봅니다.

BASH
# 사용자 세션
session:user:12345                  # 사용자 12345의 세션

# API 레이트 리밋
rate:api:user:12345:minute          # 분당 API 호출 횟수

# 캐시
cache:product:detail:789            # 상품 789의 상세 캐시
cache:search:result:{hash}          # 검색 결과 캐시

# 분산 락
lock:order:create:456               # 주문 456 생성 락

# 카운터
counter:page:view:2026-03-19        # 일별 페이지뷰

네이밍 규칙

규칙좋은 예나쁜 예
소문자 사용user:profile:123User:Profile:123
콜론으로 구분cache:product:456cache_product_456
서비스명 접두사order:item:789item:789
과도하게 길지 않게u:12345:name (극단적 축약)user_service:user_profile_information:12345
적절한 길이 유지user:12345:profile

키 크기의 영향

BASH
# 키 이름도 메모리를 사용합니다
# 키가 길수록 비교/해싱에 시간이 더 걸림

# 키 100만 개일 때 이름 길이별 메모리 차이 (대략)
# 10바이트 키: ~80MB
# 50바이트 키: ~120MB
# 200바이트 키: ~280MB

# 너무 짧으면 가독성 문제
SET u:1:n "Alice"     # 무슨 뜻인지 알기 어려움

# 적절한 균형
SET user:1:name "Alice"  # 명확하면서 적절한 길이

키 탐색 — SCAN vs KEYS

KEYS가 위험한 이유

BASH
# KEYS는 전체 키 공간을 한 번에 순회
# 키가 100만 개면 수 초간 서버가 블로킹됨
KEYS user:*   # 프로덕션에서 절대 사용 금지!

# 시간 복잡도: O(N) — N은 전체 키 수

SCAN의 동작 방식

BASH
# SCAN은 커서 기반으로 점진적 순회
# 커서 0으로 시작, 반환된 커서가 0이면 완료

SCAN 0 MATCH user:* COUNT 100
# 1) "17"           ← 다음 커서
# 2) 1) "user:123"  ← 매칭된 키들
#    2) "user:456"

SCAN 17 MATCH user:* COUNT 100
# 1) "0"            ← 커서가 0이면 순회 완료
# 2) 1) "user:789"

SCAN의 특성

  • 점진적: 한 번에 소량의 키만 반환하므로 블로킹이 짧습니다
  • 커서 기반: 상태를 서버가 아닌 클라이언트가 관리합니다
  • 중복 가능: 순회 중 키가 추가/삭제되면 중복 반환될 수 있습니다
  • 누락 가능: 순회 도중 추가된 키는 반환되지 않을 수 있습니다

COUNT 파라미터의 의미

BASH
# COUNT는 "대략 이 정도 작업하고 돌아와라"의 힌트
# 반환되는 키 수가 정확히 COUNT개는 아님

SCAN 0 COUNT 10     # 대략 10개 정도 처리
SCAN 0 COUNT 1000   # 대략 1000개 정도 처리

# 키가 적으면 COUNT보다 적게 반환
# 매칭되는 키가 없으면 빈 배열 반환 (커서는 계속 진행)

프로그래밍 언어에서의 SCAN 사용

PYTHON
# Python redis-py: scan_iter가 내부적으로 SCAN을 반복 호출
import redis
r = redis.Redis()

# 패턴에 매칭되는 모든 키를 순회
for key in r.scan_iter(match='session:*', count=100):
    ttl = r.ttl(key)
    if ttl == -1:  # TTL이 없는 세션 키 발견
        r.expire(key, 3600)  # 1시간 TTL 설정
JAVA
// Java Jedis: SCAN 사용
ScanParams params = new ScanParams()
    .match("cache:*")
    .count(100);

String cursor = "0";
do {
    ScanResult<String> result = jedis.scan(cursor, params);
    cursor = result.getCursor();
    for (String key : result.getResult()) {
        // 키 처리
    }
} while (!cursor.equals("0"));

타입별 SCAN 명령어

BASH
# Hash의 필드 순회
HSCAN user:1 0 MATCH name* COUNT 10

# Set의 멤버 순회
SSCAN tags:post:1 0 MATCH java* COUNT 10

# Sorted Set의 멤버 순회
ZSCAN leaderboard 0 MATCH user:* COUNT 10

키 관리 실전 패턴

패턴 1: TTL이 없는 키 찾아서 정리

PYTHON
# TTL이 설정되지 않은 캐시 키 탐색
orphan_keys = []
for key in r.scan_iter(match='cache:*', count=500):
    if r.ttl(key) == -1:
        orphan_keys.append(key)

# 일괄 TTL 설정
pipe = r.pipeline()
for key in orphan_keys:
    pipe.expire(key, 86400)  # 24시간
pipe.execute()

print(f"TTL 설정 완료: {len(orphan_keys)}개 키")

패턴 2: 키 개수 모니터링

BASH
# 전체 키 수 (O(1) — 항상 안전)
DBSIZE

# 타입별 키 통계는 SCAN으로 수집
# (INFO keyspace는 DB별 키 수만 제공)

패턴 3: 대용량 키 찾기

BASH
# 메모리를 많이 차지하는 키 찾기
# redis-cli --bigkeys는 내부적으로 SCAN 사용
redis-cli --bigkeys

# 특정 키의 메모리 사용량 확인 (4.0+)
MEMORY USAGE user:12345
# (integer) 256  ← 바이트 단위

# 키 타입 확인
TYPE user:12345
# "hash"

# 키의 인코딩 방식 확인
OBJECT ENCODING user:12345
# "listpack"

패턴 4: UNLINK으로 비동기 삭제

BASH
# DEL은 동기적 — 큰 키 삭제 시 블로킹
DEL large_set_with_millions    # 위험!

# UNLINK는 비동기적 (4.0+)
UNLINK large_set_with_millions # 백그라운드에서 메모리 해제

키 이벤트 알림 (Keyspace Notifications)

키의 변경 사항을 실시간으로 감지할 수 있습니다.

BASH
# 키 이벤트 알림 활성화
CONFIG SET notify-keyspace-events KEA

# 만료 이벤트 구독
SUBSCRIBE __keyevent@0__:expired

# 특정 키의 모든 이벤트 구독
SUBSCRIBE __keyspace@0__:user:12345
PYTHON
# Python으로 만료 이벤트 수신
import redis

r = redis.Redis()
pubsub = r.pubsub()
pubsub.subscribe('__keyevent@0__:expired')

for message in pubsub.listen():
    if message['type'] == 'message':
        expired_key = message['data'].decode()
        print(f"만료된 키: {expired_key}")
        # 만료 후 처리 로직 (예: 세션 정리)

주의: Keyspace Notification은 최선 노력(best-effort)으로 전달되며, 보장된 메시지 큐가 아닙니다. 클라이언트가 연결이 끊긴 동안의 이벤트는 유실됩니다.

정리

Redis 키 관리의 핵심을 요약하면 다음과 같습니다.

  • TTL은 SET ... EX 형태로 키 생성 시 함께 설정하는 것이 가장 안전합니다
  • KEEPTTL 옵션으로 키 업데이트 시 TTL 유실을 방지할 수 있습니다
  • 키 네이밍은 서비스:엔티티:식별자 패턴을 사용하여 체계적으로 관리합니다
  • 프로덕션에서는 KEYS * 대신 SCAN을 사용해야 합니다
  • 큰 키 삭제는 DEL 대신 UNLINK로 비동기 처리합니다
  • DBSIZE, MEMORY USAGE, --bigkeys로 키 현황을 모니터링할 수 있습니다
댓글 로딩 중...