키 관리 — TTL, EXPIRE, 네이밍 컨벤션, 키 스캔
수백만 개의 키가 있는 Redis에서, 키를 체계적으로 관리하고 안전하게 탐색하려면 어떻게 해야 할까요?
개념 정의
Redis의 키(Key)는 데이터에 접근하는 유일한 식별자입니다. 키 관리란 TTL(Time To Live) 설정, 네이밍 규칙, 키 탐색 세 가지를 체계적으로 다루는 것을 말합니다. 이 세 가지가 제대로 갖춰지지 않으면 메모리 낭비, 키 충돌, 서비스 장애로 이어질 수 있습니다.
왜 필요한가
- 메모리는 유한합니다: TTL 없는 키가 쌓이면 메모리 부족으로 서비스가 중단됩니다
- 키를 찾아야 합니다: 프로덕션에서 특정 패턴의 키를 안전하게 찾는 방법이 필요합니다
- 협업이 필요합니다: 여러 서비스가 하나의 Redis를 공유하면 키 충돌 위험이 생깁니다
TTL 설정 — EXPIRE와 PEXPIRE
기본 명령어
# 초 단위 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 확인 및 제거
# 남은 TTL 확인 (초)
TTL session:abc123
# 3500 (남은 초)
# -1 (만료 시간 없음)
# -2 (키가 존재하지 않음)
# 남은 TTL 확인 (밀리초)
PTTL session:abc123
# TTL 제거 (키는 유지, 만료만 해제)
PERSIST session:abc123
TTL 정밀도와 주의점
Redis의 TTL은 밀리초 정밀도로 관리됩니다. 하지만 몇 가지 주의할 점이 있습니다.
# 주의 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 명령어의 옵션 조합
# 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"으로 설정
네이밍 컨벤션
기본 원칙: 콜론 구분 계층 구조
{서비스}:{엔티티}:{식별자}:{속성}
실제 예시를 살펴봅니다.
# 사용자 세션
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:123 | User:Profile:123 |
| 콜론으로 구분 | cache:product:456 | cache_product_456 |
| 서비스명 접두사 | order:item:789 | item:789 |
| 과도하게 길지 않게 | u:12345:name (극단적 축약) | user_service:user_profile_information:12345 |
| 적절한 길이 유지 | user:12345:profile | — |
키 크기의 영향
# 키 이름도 메모리를 사용합니다
# 키가 길수록 비교/해싱에 시간이 더 걸림
# 키 100만 개일 때 이름 길이별 메모리 차이 (대략)
# 10바이트 키: ~80MB
# 50바이트 키: ~120MB
# 200바이트 키: ~280MB
# 너무 짧으면 가독성 문제
SET u:1:n "Alice" # 무슨 뜻인지 알기 어려움
# 적절한 균형
SET user:1:name "Alice" # 명확하면서 적절한 길이
키 탐색 — SCAN vs KEYS
KEYS가 위험한 이유
# KEYS는 전체 키 공간을 한 번에 순회
# 키가 100만 개면 수 초간 서버가 블로킹됨
KEYS user:* # 프로덕션에서 절대 사용 금지!
# 시간 복잡도: O(N) — N은 전체 키 수
SCAN의 동작 방식
# 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 파라미터의 의미
# COUNT는 "대략 이 정도 작업하고 돌아와라"의 힌트
# 반환되는 키 수가 정확히 COUNT개는 아님
SCAN 0 COUNT 10 # 대략 10개 정도 처리
SCAN 0 COUNT 1000 # 대략 1000개 정도 처리
# 키가 적으면 COUNT보다 적게 반환
# 매칭되는 키가 없으면 빈 배열 반환 (커서는 계속 진행)
프로그래밍 언어에서의 SCAN 사용
# 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 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 명령어
# 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이 없는 키 찾아서 정리
# 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: 키 개수 모니터링
# 전체 키 수 (O(1) — 항상 안전)
DBSIZE
# 타입별 키 통계는 SCAN으로 수집
# (INFO keyspace는 DB별 키 수만 제공)
패턴 3: 대용량 키 찾기
# 메모리를 많이 차지하는 키 찾기
# 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으로 비동기 삭제
# DEL은 동기적 — 큰 키 삭제 시 블로킹
DEL large_set_with_millions # 위험!
# UNLINK는 비동기적 (4.0+)
UNLINK large_set_with_millions # 백그라운드에서 메모리 해제
키 이벤트 알림 (Keyspace Notifications)
키의 변경 사항을 실시간으로 감지할 수 있습니다.
# 키 이벤트 알림 활성화
CONFIG SET notify-keyspace-events KEA
# 만료 이벤트 구독
SUBSCRIBE __keyevent@0__:expired
# 특정 키의 모든 이벤트 구독
SUBSCRIBE __keyspace@0__:user:12345
# 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로 키 현황을 모니터링할 수 있습니다
댓글 로딩 중...