백엔드 데이터 인프라 53편. Redis Sorted Set — 점수 기준 정렬되는 Set. ZADD·ZRANGE·ZRANGEBYSCORE·ZRANK·ZINCRBY 같은 핵심 명령어와 Top N 랭킹·실시간 인기 검색어·sliding window rate limiter 패턴까지 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 130편 중 53편이에요. 52편 에서 Redis Set 의 집합 연산 강점을 잡았다면, 이번 53편은 Sorted Set(정렬된 집합) — 점수(score) 가 붙은 Set. 랭킹·실시간 인기·sliding window rate limiter(슬라이딩 윈도우 호출 제한) 의 표준 도구로, Redis 자료구조 중에서도 가장 강력한 비밀 무기 중 하나예요.
Sorted Set이 어렵게 느껴지는 이유
Sorted Set 은 Set + Hash + 정렬 의 하이브리드라 처음 보면 기능이 너무 많아서 헷갈려요.
첫째, 이름이 Sorted Set 인데 단순 Set 의 정렬판이 아닙니다. 각 멤버에 score 가 추가로 붙고, score 기준으로 자동 정렬 되는 별도 자료구조예요. Set 처럼 중복 X 라는 점만 닮았고, 사실상 (member, score) 쌍 의 컬렉션.
둘째, 명령어가 가장 많아요. Z* 계열만 30개 이상. ZRANGE·ZREVRANGE·ZRANGEBYSCORE·ZRANGEBYLEX·ZRANK·ZSCORE·ZADD·ZINCRBY·ZPOPMIN·ZPOPMAX·ZINTERSTORE·... 어디서부터 외워야 할지 막막.
해법 — 자주 쓰는 8개만 먼저 손에 익히면 일상 90% 해결돼요. 나머지는 "이런 게 있구나" 만 알고 검색.
이 글에서 Sorted Set 의 핵심 8개 명령어 와, 랭킹·인기 검색어·rate limiter 라는 빛나는 3가지 패턴을 풀어 가요.
핵심 명령어 8종
ZADD — 멤버 + 점수 추가
> ZADD leaderboard 1500 "user42"
(integer) 1
> ZADD leaderboard 2300 "user99" 1800 "user77"
(integer) 2
복잡도 = O(log N). 옵션 다양 (NX·XX·GT·LT·CH·INCR):
> ZADD leaderboard GT 1600 "user42" # 기존 점수보다 크면 갱신
> ZADD leaderboard INCR 50 "user42" # 기존 점수에 +50
ZRANGE / ZREVRANGE — 순위 기준 조회
> ZADD board 100 "A" 200 "B" 150 "C"
> ZRANGE board 0 -1 WITHSCORES # 오름차순 전체
1) "A"
2) "100"
3) "C"
4) "150"
5) "B"
6) "200"
> ZREVRANGE board 0 9 WITHSCORES # 내림차순 Top 10
1) "B"
2) "200"
3) "C"
4) "150"
5) "A"
6) "100"
ZREVRANGE 0 9 = Top 10 랭킹의 표준 한 줄. 점수 같은 멤버는 사전순(lexicographical) 으로 정렬.
여기서 시험 함정 — Redis 6.2+ 에서 ZRANGE 가 REV·BYSCORE·BYLEX 옵션을 흡수해 통합됐어요. 새 코드라면 ZRANGE ... REV 사용 권장, 옛 명령어 ZREVRANGE·ZRANGEBYSCORE 는 deprecated(권장 폐기) 흐름.
ZRANGEBYSCORE — 점수 범위 조회
> ZRANGEBYSCORE board 150 200
1) "C"
2) "B"
> ZRANGEBYSCORE board (150 +inf # (150 = 150 제외, +inf = 무한대
1) "B"
( = 제외, +inf·-inf = 무한대. "점수 1,000~2,000 사이 사용자" 같은 쿼리.
ZRANK / ZREVRANK — 멤버의 순위 (O(log N))
> ZRANK board "C" # 오름차순 순위 (0-based)
(integer) 1
> ZREVRANK board "B" # 내림차순 순위 (Top 1)
(integer) 0
"내가 전체 몇 등인가" 가 한 줄. 1억 명 중에서도 μs 단위 응답.
ZSCORE — 멤버 점수
> ZSCORE board "B"
"200"
ZINCRBY — 점수 증감 (atomic, 중간 끼어듦 없는 원자 연산)
> ZINCRBY board 50 "A"
"150" # 기존 100 + 50
카운터를 점수에 박으면 자동 정렬되는 카운터 — 인기 검색어·trending 같은 곳의 핵심.
ZCARD — 멤버 수
> ZCARD board
(integer) 3
ZPOPMIN / ZPOPMAX — 최저·최고 점수 멤버 빼기
> ZPOPMAX board # 최고 점수 빼서 반환 + 제거
1) "B"
2) "200"
> ZPOPMIN board # 최저 점수
Priority Queue(우선순위 큐) 의 표준 구현. 점수에 우선순위·timestamp·ETA(예상 처리 시간) 박고 ZPOPMIN 으로 다음에 처리할 작업 꺼내기.
한 줄 정리 — Sorted Set 핵심 8개 = ZADD·ZRANGE·ZRANGEBYSCORE·ZRANK·ZSCORE·ZINCRBY·ZCARD·ZPOPMIN/MAX. 이것만 익혀도 일상 90%.
패턴 1: 랭킹 / 리더보드
게임·앱의 Top N 점수 — Sorted Set의 가장 클래식한 활용처.
# 점수 업데이트
> ZADD leaderboard 1500 "user42"
> ZADD leaderboard 2300 "user99"
> ZADD leaderboard 1800 "user77"
# Top 10
> ZREVRANGE leaderboard 0 9 WITHSCORES
# 내 순위
> ZREVRANK leaderboard "user42"
# 내 점수 ±100 안의 사용자 (주변 경쟁자)
> my_score=$(ZSCORE leaderboard "user42") # 1500
> ZRANGEBYSCORE leaderboard 1400 1600 WITHSCORES
# 점수 증가 (atomic)
> ZINCRBY leaderboard 50 "user42"
핵심 효율 — 1,000만 명 랭킹 중 내 순위 조회 가 O(log N) ≈ 24번 연산 만에 끝남. DB로 같은 쿼리 (SELECT COUNT(*) WHERE score > my_score) 는 인덱스 잘 박혀 있어도 ms 단위, Sorted Set 은 μs 단위.
패턴 2: 실시간 인기 검색어 / Trending
"최근 1시간 인기 검색어 Top 10" — 점수에 검색 횟수 박고 ZRANGE 하나면 끝.
# 검색이 들어올 때마다
> ZINCRBY trending:search 1 "redis"
> ZINCRBY trending:search 1 "spring boot"
# Top 10
> ZREVRANGE trending:search 0 9 WITHSCORES
# 1시간마다 reset (cron 또는 키 TTL)
> EXPIRE trending:search 3600
확장 — 1시간 단위 윈도우 가 필요하면 키에 시간 prefix:
> ZINCRBY trending:2026-05-17-12 1 "redis" # 12시 윈도우
> ZINCRBY trending:2026-05-17-13 1 "redis" # 13시 윈도우
여러 시간 윈도우를 합쳐 "최근 3시간 trending" 도 ZUNIONSTORE 로 가능.
패턴 3: Sliding Window Rate Limiter
여기가 정말 중요한 자리. "분당 60회 API 호출 제한" 같은 rate limit 을 Sorted Set 점수에 timestamp 박아 풀어요.
알고리즘
- 요청 들어올 때마다 현재 timestamp 를 score 로 ZADD
- 현재 시간 - 60초 이전 항목을 ZREMRANGEBYSCORE 로 제거 (윈도우 밖)
- 남은 항목 수 ZCARD 로 카운트
- 카운트가 60 이상 → 차단, 아니면 통과
Lua 스크립트 한 줄 구현
Lua(레디스 서버에서 도는 임베디드 스크립트 언어) 로 위 4단계를 한 번에 묶으면 race condition 없이 끝나요.
-- KEYS[1] = "rate:user42"
-- ARGV[1] = 현재 timestamp (밀리초)
-- ARGV[2] = 윈도우 (밀리초, 예: 60000)
-- ARGV[3] = 최대 허용 횟수 (예: 60)
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call("ZREMRANGEBYSCORE", key, 0, now - window)
local count = redis.call("ZCARD", key)
if count >= limit then
return 0
end
redis.call("ZADD", key, now, now)
redis.call("EXPIRE", key, window / 1000)
return 1
장점 — 정확한 sliding window. "고정 윈도우(분 단위 카운터)" 는 경계 시점에 2배 트래픽이 통과 하는 버그가 있는데, Sliding Window 는 항상 정확히 마지막 60초 를 본다는 게 차이.
비교 — Token Bucket / Leaky Bucket / Fixed Window / Sliding Window 4가지 알고리즘 중 Sliding Window 가 가장 정확하고 Sorted Set 으로 자연스럽게 구현돼요.
패턴 4: Priority Queue
작업 큐에서 우선순위가 높은 작업이 먼저 처리되어야 할 때.
# 작업 추가 (점수 = 우선순위, 낮을수록 우선)
> ZADD jobs 1 "urgent-job-001"
> ZADD jobs 5 "normal-job-002"
> ZADD jobs 1 "urgent-job-003"
> ZADD jobs 10 "low-job-004"
# 다음 작업 꺼내기 (가장 우선)
> ZPOPMIN jobs
1) "urgent-job-001"
2) "1"
> ZPOPMIN jobs
1) "urgent-job-003"
2) "1"
ZPOPMIN 은 최저 점수 멤버 를 빼서 반환 — 낮은 점수가 더 우선 이라는 규칙으로 우선순위 큐 가 자연스러움.
Scheduled Job(예약 작업) — 점수에 예정 실행 시각(timestamp) 박고, 현재 시각 이전 항목만 ZRANGEBYSCORE 로 가져와 처리하면 지연 큐(delayed queue) 가 됨.
# 10분 후 실행 작업
> ZADD scheduled $(($(date +%s) + 600)) "send-email-123"
# 워커: 현재 시각 이전 모두 가져오기
> ZRANGEBYSCORE scheduled 0 $(date +%s)
집합 연산 — ZINTERSTORE·ZUNIONSTORE
여기까지 따라오셨다면 한 가지 의문 — "Set 의 SINTER 처럼 Sorted Set 도 교집합 가능?". 정답 — 가능하고 점수도 합쳐져요.
> ZADD scores:math 90 "Alice" 85 "Bob"
> ZADD scores:english 80 "Alice" 95 "Bob"
> ZINTERSTORE total 2 scores:math scores:english
> ZRANGE total 0 -1 WITHSCORES
1) "Alice"
2) "170" # 90 + 80
3) "Bob"
4) "180" # 85 + 95
기본 = 점수 합산. AGGREGATE MIN/MAX 로 변경 가능. 여러 카테고리 점수를 합쳐 종합 랭킹 같은 자리에 자연스러움.
시험 직전 한 번 더 — Sorted Set 함정 압축 노트
- Sorted Set = Set + score + 자동 정렬 (점수 기준)
- 모델 = (member, score) 쌍 의 컬렉션
- Set 정렬판 이 아니라 별도 타입
- 핵심 8개 =
ZADD·ZRANGE·ZRANGEBYSCORE·ZRANK·ZSCORE·ZINCRBY·ZCARD·ZPOPMIN/MAX ZADD= O(log N), 옵션NX·XX·GT·LT·CH·INCRZRANGE REV 0 9 WITHSCORES= Top 10 랭킹의 한 줄- 점수 같은 멤버 = 사전순 정렬
- Redis 6.2+ =
ZRANGE가REV·BYSCORE·BYLEX옵션 흡수,ZREVRANGE·ZRANGEBYSCOREdeprecated 흐름 ZRANGEBYSCORE범위 =(제외,+inf·-inf무한대ZRANK·ZREVRANK= O(log N) 순위 조회, 1억 명 중에서도 μsZSCORE= 멤버 점수 조회ZINCRBY= atomic 점수 증감 — 인기 검색어의 핵심ZPOPMIN/MAX= Priority Queue 의 표준- 자주 쓰는 4 패턴 = 랭킹 / Trending / Sliding Window Rate Limiter / Priority Queue
- 랭킹 =
ZADD+ZREVRANGE 0 N+ZREVRANK me - 1,000만 명 내 순위 조회 = O(log N) ≈ 24번 연산
- Trending =
ZINCRBY+ 시간 prefix 키 +EXPIRE - Sliding Window Rate Limiter = score에 timestamp +
ZREMRANGEBYSCORE+ZCARD - Rate Limit 4 알고리즘 중 = Sliding Window 가 가장 정확
- Priority Queue = score에 우선순위 +
ZPOPMIN - Delayed Queue = score에 실행 timestamp +
ZRANGEBYSCORE 0 now - 집합 연산 =
ZINTERSTORE·ZUNIONSTORE, 기본 score 합산 - 내부 구조 = listpack(작은) · skiplist + hashtable(큰) 자동 전환
- 설정 =
zset-max-listpack-entries·zset-max-listpack-value
공식 문서: Redis Sorted Sets 에서 Sorted Set 명령어 전체 reference 를 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 48편 — Redis 데이터 타입 13종 한 번에 정리
- 49편 — Redis String 깊이 + 분산 락 패턴
- 50편 — Redis Hash + 객체 캐싱 패턴
- 51편 — Redis List + 큐·캡 리스트 패턴
- 52편 — Redis Set + 집합 연산 패턴
다음 글: