Skip to main content
Summary

자료구조를 고르면 기능의 모양이 정해진다

Redis를 잘 쓴다는 것은 명령어를 많이 외우는 일이 아니다. 해결하려는 문제를 Redis 자료구조에 맞게 표현하는 일에 가깝다.

예를 들어 "최근 본 상품 10개"는 순서가 중요하다. "사용자가 관심 있는 태그"는 중복이 없어야 한다. "게임 점수 랭킹"은 점수 기준 정렬이 필요하다. 이 세 문제를 모두 String이나 Hash로 억지로 풀 수도 있지만, 그러면 정렬, 중복 제거, 범위 조회를 애플리케이션 코드에서 직접 처리해야 한다.

Redis의 강점은 여기서 나온다. List, Set, Sorted Set은 단순히 여러 값을 담는 그릇이 아니라, 자주 필요한 동작을 명령 하나로 처리하게 해 주는 자료구조다.

이번 장에서는 세 가지 질문으로 접근한다.

  • 순서대로 넣고 꺼내야 하는가? → List

  • 중복 없이 포함 여부를 빠르게 판단해야 하는가? → Set

  • 점수나 우선순위로 정렬해야 하는가? → Sorted Set

List, Set, Sorted Set으로 서비스 기능 만들기AI 생성 이미지

List: 큐와 최근 활동 목록 만들기

Redis List는 여러 문자열 값을 순서대로 저장하는 자료구조다. 양쪽 끝에 값을 넣거나 꺼내는 작업에 적합하다. 그래서 간단한 작업 큐, 최근 활동 목록, 알림 목록 같은 기능에 자주 사용된다.

가장 기본적인 큐 패턴은 한쪽에서 넣고 반대쪽에서 꺼내는 방식이다.

LPUSH queue:email "job:1001"
LPUSH queue:email "job:1002"
RPOP queue:email

LPUSH는 리스트의 왼쪽, 즉 머리 부분에 값을 넣는다. RPOP은 오른쪽 끝의 값을 꺼내고 삭제한다. 이렇게 사용하면 먼저 들어온 작업이 먼저 나가는 FIFO 큐처럼 동작한다. LPUSH queue:email job:1001, LPUSH queue:email job:1002 순서로 넣었다면 RPOP은 job:1001을 먼저 반환한다.

작업을 처리하는 워커(worker)는 보통 계속 큐를 확인한다. 단순히 RPOP을 반복 호출하면 큐가 비어 있을 때도 Redis에 계속 요청을 보내게 된다. 이때는 BLPOP이나 BRPOP 같은 블로킹 명령을 사용할 수 있다. 앞의 예시처럼 왼쪽에 넣고 오른쪽에서 꺼내는 구조라면 블로킹 버전도 오른쪽에서 꺼내는 BRPOP을 쓰면 된다.

BRPOP queue:email 5

BRPOP은 리스트에 값이 있으면 오른쪽 끝에서 즉시 꺼내고, 없으면 지정한 시간 동안 기다린다. 위 예시는 최대 5초 기다린다. 타임아웃을 0으로 주면 값이 들어올 때까지 계속 대기한다. 백그라운드 작업 서버에서는 이런 블로킹 방식이 불필요한 반복 조회를 줄여 준다.

다만 이 큐는 "간단한 큐"다. 워커가 작업을 꺼낸 직후 장애가 나면, 해당 작업은 이미 큐에서 사라진 상태다. 재처리 보장이 필요한 결제, 정산, 메일 발송 같은 작업에는 처리 중 목록을 따로 두거나, 이후 장에서 다룰 Stream, 메시지 브로커, Lua 스크립트 등을 검토해야 한다.

List는 최근 활동 목록에도 잘 맞는다.

LPUSH user:1001:recent-products product:77
LPUSH user:1001:recent-products product:31
LPUSH user:1001:recent-products product:88
LRANGE user:1001:recent-products 0 9

새 활동을 왼쪽에 넣고, LRANGE 0 9로 최신 10개만 읽는다. 이때 목록이 계속 커지면 메모리 낭비가 생긴다. 최근 목록은 보통 전체 이력이 아니라 "최근 N개"만 필요하다. 그래서 값을 넣은 뒤 LTRIM으로 길이를 제한하는 패턴을 함께 사용한다.

LPUSH user:1001:recent-products product:88
LTRIM user:1001:recent-products 0 49

이렇게 하면 최신 50개만 유지한다. Redis에서 컬렉션을 사용할 때는 "얼마나 많이 쌓일 수 있는가"를 항상 먼저 생각해야 한다.

Set: 중복 없는 태그와 관계 표현하기

Set은 중복을 허용하지 않는 값의 모음이다. 순서는 중요하지 않지만, 어떤 값이 포함되어 있는지 빠르게 확인해야 할 때 적합하다.

게시글 태그를 예로 들어 보자.

SADD post:5001:tags redis cache backend
SADD post:5001:tags redis
SMEMBERS post:5001:tags

SADD는 Set에 값을 추가한다. 이미 존재하는 값은 다시 추가되지 않는다. 위 예시에서 redis를 두 번 추가해도 Set 안에는 한 번만 저장된다. 태그, 권한, 좋아요한 사용자 목록처럼 중복이 의미 없는 데이터에 잘 맞는다.

태그를 제거할 때는 SREM을 사용한다.

SREM post:5001:tags cache

특정 태그가 있는지 확인하려면 전체를 가져올 필요가 없다.

SISMEMBER post:5001:tags redis

SISMEMBER는 값이 Set에 있으면 1, 없으면 0을 반환한다. 예를 들어 사용자가 이미 좋아요를 눌렀는지 확인할 때 유용하다.

SADD post:5001:liked-users user:1001
SISMEMBER post:5001:liked-users user:1001

친구 관계나 추천 기능도 Set으로 표현할 수 있다. 사용자별 친구 목록을 Set으로 저장해 보자.

SADD user:1001:friends user:2001 user:2002 user:2003
SADD user:1002:friends user:2002 user:2003 user:2009
SINTER user:1001:friends user:1002:friends

SINTER는 여러 Set의 교집합을 반환한다. 위 명령은 두 사용자의 공통 친구를 찾는다. 같은 원리로 "내가 팔로우하는 사람 중 특정 이벤트에 참여한 사람", "사용자가 선택한 태그와 상품 태그가 겹치는지" 같은 기능도 만들 수 있다. Redis 공식 문서에서도 SINTER는 주어진 Set들의 교집합 멤버를 반환하며, 존재하지 않는 키는 빈 Set처럼 취급한다고 설명한다.1

하지만 교집합은 주의해야 한다. 작은 Set 몇 개를 비교할 때는 매우 편리하지만, 수십만 명 단위의 친구 목록이나 팔로워 목록을 자주 교집합으로 계산하면 Redis에 부담이 된다. 특히 요청마다 큰 Set을 여러 개 교차 계산하는 구조라면 결과를 캐시하거나, 미리 계산된 추천 후보를 별도 키에 저장하는 방식이 필요하다.

Set은 "중복 없음"과 "포함 여부 확인"에 강하다. 반대로 정렬된 순서, 최신순, 점수순이 필요하다면 Set만으로는 부족하다.

Sorted Set: 실시간 랭킹 만들기

Sorted Set은 Set처럼 멤버(member)를 중복 없이 저장하되, 각 멤버에 점수(score)를 함께 둔다. Redis는 이 점수를 기준으로 멤버를 정렬한다. 그래서 랭킹, 인기 게시글, 검색어 순위, 우선순위 큐에 자주 쓰인다.

게임 점수 랭킹을 만들어 보자.

ZADD leaderboard:game:daily 1200 user:1001
ZADD leaderboard:game:daily 980 user:1002
ZADD leaderboard:game:daily 1430 user:1003

ZADD는 Sorted Set에 멤버와 점수를 저장한다. 같은 멤버를 다시 추가하면 점수가 갱신된다.

ZADD leaderboard:game:daily 1510 user:1001

이제 user:1001의 점수는 1510이 된다. 특정 사용자의 점수를 확인하려면 ZSCORE를 사용한다.

ZSCORE leaderboard:game:daily user:1001

상위 랭킹을 조회할 때는 점수가 높은 순서가 필요하다. 전통적으로는 ZREVRANGE를 많이 사용했다.

ZREVRANGE leaderboard:game:daily 0 9 WITHSCORES

이 명령은 점수가 높은 멤버부터 10개를 반환한다. WITHSCORES를 붙이면 멤버와 점수를 함께 받을 수 있다. 다만 Redis 6.2부터는 ZREVRANGE 대신 ZRANGE ... REV 사용이 권장된다.2

ZRANGE leaderboard:game:daily 0 9 REV WITHSCORES

낮은 점수부터 보고 싶다면 REV를 빼면 된다.

ZRANGE leaderboard:game:daily 0 9 WITHSCORES

ZRANGE의 시작과 끝 인덱스는 0부터 시작하고, 끝 인덱스도 결과에 포함된다. 따라서 0 9는 10개를 의미한다. -1은 마지막 요소를 뜻한다. Redis 공식 문서에 따르면 ZRANGE는 반환하는 요소 수가 많아질수록 비용도 커진다.3

랭킹 기능을 설계할 때는 전체 순위를 한 번에 가져오지 않는 것이 중요하다.

ZRANGE leaderboard:game:daily 0 99 REV WITHSCORES

상위 100명 정도는 괜찮을 수 있다. 하지만 매 요청마다 10만 명 전체를 가져와 애플리케이션에서 잘라 쓰는 방식은 좋지 않다. 필요한 범위만 Redis에 요청해야 한다.

사용자의 현재 등수까지 필요하다면 ZREVRANK 같은 명령을 함께 사용할 수 있다. 이 장의 핵심은 Sorted Set이 "점수 갱신"과 "정렬된 범위 조회"를 자료구조 수준에서 제공한다는 점이다.

컬렉션이 커질 때 먼저 생각할 것

List, Set, Sorted Set은 기능을 빠르게 만들게 해 주지만, 크기가 커지면 비용이 드러난다. 컬렉션 자료구조를 사용할 때는 다음 네 가지를 점검해야 한다.

첫째, 조회 범위를 제한해야 한다. LRANGE, ZRANGE, ZREVRANGE는 범위를 지정할 수 있다. 그런데 범위를 크게 잡으면 그만큼 많은 데이터를 Redis가 읽고 네트워크로 전송한다. "일단 전체 조회 후 애플리케이션에서 필터링"은 작은 데이터에서는 편하지만, 운영 데이터에서는 장애 원인이 된다.

둘째, 오래된 데이터를 정리해야 한다. 최근 활동 목록은 LTRIM으로 최대 길이를 제한한다. 일간 랭킹은 leaderboard:game:2026-05-30처럼 날짜를 키에 포함하고 TTL을 줄 수 있다. 태그나 친구 관계처럼 장기 보관이 필요한 데이터는 TTL보다 명시적 삭제 정책이 더 어울린다.

셋째, 큰 교집합을 조심해야 한다. SINTER는 공통 태그나 공통 친구를 구할 때 편리하지만, 입력 Set이 커질수록 계산 비용이 커진다. 대규모 추천 시스템에서는 요청 시점에 매번 교집합을 계산하기보다, 배치 작업이나 이벤트 처리로 후보군을 미리 만들어 두는 경우가 많다.

넷째, 메모리는 공짜가 아니다. Redis는 메모리 기반 저장소다. 컬렉션 하나에 수백만 개 멤버를 넣는 설계가 가능하더라도, 그것이 항상 좋은 설계는 아니다. 키를 기간별·도메인별로 나눌지, 상위 N개만 유지할지, 오래된 항목을 삭제할지 미리 정해야 한다.

어떤 자료구조를 선택할까

간단히 정리하면 다음과 같다.

문제적합한 자료구조대표 명령
작업을 순서대로 처리ListLPUSH, RPOP, BRPOP
최근 활동 N개 유지ListLPUSH, LRANGE, LTRIM
중복 없는 태그 저장SetSADD, SREM, SISMEMBER
공통 친구 찾기SetSINTER
실시간 점수 랭킹Sorted SetZADD, ZRANGE, ZREVRANGE, ZSCORE

Redis 컬렉션 자료구조는 애플리케이션 코드를 줄여 주지만, 설계를 대신해 주지는 않는다. 어떤 값이 얼마나 쌓이는지, 얼마나 자주 읽히는지, 어떤 기준으로 삭제되는지를 함께 정해야 한다.

List는 순서가 있는 흐름에 적합하다. Set은 중복 없는 관계에 적합하다. Sorted Set은 점수로 정렬되는 상태에 적합하다. 이 기준만 잡아도 Redis를 단순 캐시가 아니라 서비스 기능을 만드는 도구로 사용할 수 있다.

참고

1SINTER | Redis Docs

2ZREVRANGE | Redis Docs

3ZRANGE | Redis Docs

책 목록

Redis 입문과 실전 운영

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기