Redis 데이터 구조 완전 가이드: String

Redis 데이터 구조를 제대로 알아야 하는 이유

Redis를 단순 key-value 캐시로만 사용하는 팀이 많습니다. 하지만 Redis는 String, List, Hash, Set, Sorted Set, Stream, HyperLogLog, Bitmap 등 8가지 이상의 데이터 구조를 제공하며, 각 구조를 적절히 선택하면 애플리케이션 코드 수십 줄을 Redis 명령어 한 줄로 대체할 수 있습니다.

이 글에서는 각 데이터 구조의 내부 동작, 시간 복잡도, 실전 사용 패턴, 메모리 최적화 전략, 그리고 잘못된 선택으로 발생하는 성능 문제까지 다룹니다.

데이터 구조 한눈에 비교

데이터 구조 핵심 특징 시간 복잡도 (주요 연산) 대표 사용 사례
String 바이너리 안전 문자열, 최대 512MB GET/SET: O(1) 캐시, 카운터, 세션
List 양방향 연결 리스트 LPUSH/RPOP: O(1) 메시지 큐, 최근 활동
Hash 필드-값 쌍의 맵 HGET/HSET: O(1) 객체 저장, 사용자 프로필
Set 고유 값 집합 SADD/SISMEMBER: O(1) 태그, 고유 방문자
Sorted Set 점수 기반 정렬 집합 ZADD: O(log N) 리더보드, 랭킹
Stream append-only 로그 XADD: O(1) 이벤트 소싱, 로그 수집
HyperLogLog 카디널리티 근사 추정 PFADD: O(1) UV 카운트 (12KB 고정)
Bitmap 비트 단위 연산 SETBIT/GETBIT: O(1) 출석 체크, 기능 플래그

String: 캐시 그 이상의 활용

String은 가장 기본적인 타입이지만 단순 캐시 외에도 원자적 카운터, 분산 락, 비트 연산 등 다양하게 활용됩니다.

# 기본 캐시 (TTL 포함)
SET user:1001:profile '{"name":"Theo","role":"admin"}' EX 3600

# 원자적 카운터 (페이지뷰, 좋아요)
INCR article:5001:views
INCRBY article:5001:views 10

# 분산 락 (SET NX + EX)
SET lock:order:9001 "worker-1" NX EX 30

# 조건부 업데이트 (이미 존재할 때만)
SET user:1001:token "abc123" XX EX 1800

String vs Hash: 객체 저장 시 선택 기준

기준 String (JSON) Hash
부분 읽기/쓰기 전체 GET/SET 필요 HGET/HSET으로 필드 단위 접근
메모리 효율 작은 객체에서 유리 ziplist 인코딩 시 더 효율적
TTL 설정 키 단위 TTL 키 단위 TTL (필드별 TTL 불가)
직렬화 비용 JSON parse 필요 없음

Hash: 객체를 필드 단위로 다루기

# 사용자 프로필 저장
HSET user:1001 name "Theo" email "theo@example.com" role "admin" login_count 0

# 개별 필드 읽기
HGET user:1001 email

# 여러 필드 읽기
HMGET user:1001 name role

# 필드 단위 카운터
HINCRBY user:1001 login_count 1

# 전체 객체 읽기
HGETALL user:1001

# 특정 필드 존재 여부
HEXISTS user:1001 email

Hash는 필드 수가 128개 이하이고 각 값이 64바이트 이하일 때 ziplist로 인코딩되어 메모리를 절약합니다. 이 임계값은 hash-max-ziplist-entrieshash-max-ziplist-value로 조정할 수 있습니다.

List: 큐와 최근 항목 관리

# 메시지 큐 패턴
LPUSH queue:emails '{"to":"user@example.com","subject":"Welcome"}'
BRPOP queue:emails 30  # 30초 블로킹 팝

# 최근 활동 피드 (최신 50개만 유지)
LPUSH feed:user:1001 '{"action":"comment","post_id":5001}'
LTRIM feed:user:1001 0 49

# 최근 본 상품
LPUSH recent:user:1001 "product:3001"
LREM recent:user:1001 1 "product:3001"  # 중복 제거 후 재삽입
LPUSH recent:user:1001 "product:3001"
LTRIM recent:user:1001 0 19

BRPOP은 리스트가 비어있을 때 블로킹 대기하므로 폴링 없이 큐를 구현할 수 있습니다. 다만 안정적인 메시지 큐가 필요하면 Redis Stream이나 전용 메시지 브로커를 사용하세요.

Set: 고유 값 집합과 집합 연산

# 태그 시스템
SADD post:5001:tags "redis" "database" "nosql"
SADD post:5002:tags "redis" "cache" "performance"

# 두 글의 공통 태그
SINTER post:5001:tags post:5002:tags
# → "redis"

# 글 5001에만 있는 태그
SDIFF post:5001:tags post:5002:tags
# → "database", "nosql"

# 고유 방문자 (일별)
SADD visitors:2026-02-20 "user:1001" "user:1002" "user:1001"
SCARD visitors:2026-02-20
# → 2 (중복 자동 제거)

# 친구 추천 (공통 친구)
SINTER friends:user:1001 friends:user:1002

Sorted Set: 실시간 랭킹의 정석

# 게임 리더보드
ZADD leaderboard 1500 "player:alice"
ZADD leaderboard 2300 "player:bob"
ZADD leaderboard 1800 "player:charlie"

# 상위 10명 (점수 내림차순)
ZREVRANGE leaderboard 0 9 WITHSCORES

# 특정 플레이어 순위 (0-based)
ZREVRANK leaderboard "player:bob"
# → 0 (1등)

# 점수 증가
ZINCRBY leaderboard 200 "player:alice"

# 점수 범위 조회
ZRANGEBYSCORE leaderboard 1500 2000 WITHSCORES

# 시간 기반 슬라이딩 윈도우 (Rate Limiting)
ZADD requests:user:1001 1708423200 "req:1"
ZREMRANGEBYSCORE requests:user:1001 0 1708419600  # 1시간 이전 제거
ZCARD requests:user:1001  # 현재 윈도우 내 요청 수

Sorted Set의 ZADD는 O(log N)이므로 수백만 개 요소에서도 빠르게 동작합니다. 리더보드, 타임라인, 우선순위 큐, Rate Limiting 등 점수 기반 정렬이 필요한 모든 곳에 적합합니다.

Stream: 이벤트 소싱과 소비자 그룹

# 이벤트 추가
XADD events:orders * action "created" order_id "9001" amount "50000"
XADD events:orders * action "paid" order_id "9001" method "card"

# 소비자 그룹 생성
XGROUP CREATE events:orders order-processors $ MKSTREAM

# 소비자 그룹으로 읽기
XREADGROUP GROUP order-processors worker-1 COUNT 10 BLOCK 5000 STREAMS events:orders >

# 처리 완료 ACK
XACK events:orders order-processors "1708423200000-0"

# 미처리 메시지 확인 (PEL)
XPENDING events:orders order-processors - + 10

Stream은 Kafka의 소비자 그룹과 유사한 개념을 제공합니다. XREADGROUP으로 여러 워커가 메시지를 분산 처리하고, XACK으로 처리 완료를 확인합니다. 메시지 유실 방지가 필요한 비동기 처리에 적합합니다.

HyperLogLog: 12KB로 수백만 고유 값 카운트

# 일별 고유 방문자 수 추정
PFADD uv:2026-02-20 "user:1001" "user:1002" "user:1003"
PFADD uv:2026-02-20 "user:1001"  # 중복은 무시

# 고유 방문자 수 (0.81% 오차)
PFCOUNT uv:2026-02-20

# 주간 UV 합산
PFMERGE uv:week7 uv:2026-02-14 uv:2026-02-15 uv:2026-02-16 uv:2026-02-17 uv:2026-02-18 uv:2026-02-19 uv:2026-02-20
PFCOUNT uv:week7

Set으로 1000만 사용자를 저장하면 수백 MB가 필요하지만, HyperLogLog는 12KB로 동일한 카디널리티를 추정합니다. 정확한 값이 아닌 근사치로 충분한 분석 용도에 적합합니다.

Bitmap: 비트 하나로 상태 관리

# 출석 체크 (사용자 ID를 비트 오프셋으로)
SETBIT attendance:2026-02-20 1001 1
SETBIT attendance:2026-02-20 1002 1

# 출석 여부 확인
GETBIT attendance:2026-02-20 1001
# → 1

# 일별 출석 인원
BITCOUNT attendance:2026-02-20

# 연속 출석자 (AND 연산)
BITOP AND attendance:streak attendance:2026-02-19 attendance:2026-02-20
BITCOUNT attendance:streak

메모리 최적화 전략

전략 적용 대상 효과
ziplist 인코딩 유지 Hash, List, Sorted Set 소규모 데이터에서 메모리 50~70% 절약
키 네이밍 축약 전체 u:1001 vs user:1001 — 대량 키에서 유의미
TTL 설정 전체 만료 데이터 자동 정리
HyperLogLog 대체 Set (카운팅 목적) 수백 MB → 12KB
OBJECT ENCODING 확인 전체 의도치 않은 인코딩 변환 감지
# 인코딩 확인
OBJECT ENCODING user:1001
# → "ziplist" (최적) 또는 "hashtable" (임계값 초과)

흔한 실수와 해결법

실수 1: 대규모 키에 KEYS * 사용

KEYS *는 O(N)으로 전체 키를 스캔하며 Redis를 블로킹합니다. 운영 환경에서는 SCAN 커서를 사용하세요.

SCAN 0 MATCH user:* COUNT 100

실수 2: 큰 Hash/Set에 HGETALL/SMEMBERS 사용

수만 개 필드를 한 번에 가져오면 네트워크 지연과 메모리 급증이 발생합니다. HSCAN, SSCAN으로 분할 조회하세요.

실수 3: List를 랜덤 접근 용도로 사용

LINDEXLINSERT는 O(N)입니다. 랜덤 접근이 필요하면 Hash나 Sorted Set을 사용하세요.

실수 4: 핫 키(Hot Key) 집중

특정 키에 트래픽이 집중되면 해당 샤드가 병목이 됩니다. 키를 분산하거나 로컬 캐시 레이어를 추가하세요.

운영 체크리스트

  1. INFO memory로 메모리 사용량과 단편화율(mem_fragmentation_ratio) 모니터링
  2. OBJECT ENCODING으로 주요 키의 인코딩 타입 확인
  3. ziplist 임계값을 데이터 패턴에 맞게 조정
  4. SLOWLOG GET 10으로 느린 명령어 주기적 점검
  5. maxmemory-policy 설정 확인 (allkeys-lru 권장)
  6. 큰 키 탐지: redis-cli --bigkeys 주기적 실행
  7. TTL 없는 키 비율 점검 — 메모리 누수 원인

마무리

Redis의 진짜 힘은 다양한 데이터 구조에 있습니다. String으로 캐시와 카운터를, Hash로 객체를, Sorted Set으로 랭킹을, Stream으로 이벤트 처리를, HyperLogLog로 대규모 카디널리티 추정을 구현하면 애플리케이션 복잡도를 크게 줄일 수 있습니다. 데이터 특성에 맞는 구조를 선택하는 것이 Redis 성능 최적화의 시작입니다.

Redis 캐시 계층에서 발생하는 스탬피드 문제와 방어 패턴은 Redis 캐시 스탬피드 방지 가이드에서 다루고 있습니다. 데이터베이스 인덱스 설계와 함께 읽으면 캐시-DB 계층 전체를 최적화할 수 있습니다 — PostgreSQL 인덱스 설계 가이드를 참고하세요.

운영 중인 Redis에서 redis-cli --bigkeysOBJECT ENCODING을 실행해 보세요. 예상과 다른 인코딩이나 비정상적으로 큰 키를 발견하면 댓글로 공유해 주세요. 구조 변경 전략을 안내드리겠습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux