Redis를 단순 Key-Value 저장소로만 알고 있다면 절반만 알고 있는 것입니다. Redis는 상황에 따라 선택할 수 있는 다양한 자료구조를 제공합니다. 올바른 자료구조를 선택하면 코드가 단순해지고 성능이 높아집니다.
| 타입 | 설명 | 대표 명령 |
|---|---|---|
| String | 문자열, 숫자, 바이너리 | SET, GET, INCR |
| Hash | 필드-값 쌍의 맵 | HSET, HGET, HGETALL |
| List | 순서 있는 문자열 목록 | LPUSH, RPOP, LRANGE |
| Set | 중복 없는 문자열 집합 | SADD, SMEMBERS, SISMEMBER |
| Sorted Set | 점수로 정렬된 집합 | ZADD, ZRANGE, ZRANK |
| Bitmap | 비트 단위 연산 | SETBIT, GETBIT, BITCOUNT |
| HyperLogLog | 확률적 유일 원소 개수 | PFADD, PFCOUNT |
| Stream | 메시지 스트림 로그 | XADD, XREAD, XGROUP |
같은 데이터를 저장하더라도 어떤 작업이 주로 필요한지에 따라 타입이 달라집니다.
user:1000:name "철수"
user:1000:token "eyJhbGci..."
page:views 42837
캐시, 세션 토큰, 카운터처럼 단순한 값 하나를 저장할 때 씁니다.
user:1000
name "철수"
email "cs@example.com"
role "admin"
JSON으로 직렬화해 String에 넣을 수도 있지만, Hash를 쓰면 필드 하나만 읽거나 수정할 수 있습니다.
# String 방식: 이름 하나 수정해도 전체 JSON 교체
SET user:1000 '{"name":"영희","email":"yh@example.com","role":"admin"}'
# Hash 방식: 이름만 바꾸기
HSET user:1000 name "영희"
notifications:user:1000
["알림3", "알림2", "알림1"]
최근 알림, 최근 방문 기록, 작업 큐처럼 순서와 위치가 중요할 때 씁니다.
post:1000:liked_by
{user:1, user:5, user:8}
좋아요 누른 사람, 팔로워, 태그처럼 중복이 없어야 하고 포함 여부가 중요할 때 씁니다.
game:ranking
"player:A" 점수 9800
"player:B" 점수 8500
"player:C" 점수 7200
실시간 랭킹, 우선순위 큐, 시간 기반 정렬처럼 점수로 정렬이 필요할 때 씁니다.
올바른 자료구조를 선택하면 O(1) 또는 O(log N) 연산으로 처리할 수 있습니다.
| 작업 | String | Hash | Set | Sorted Set |
|---|---|---|---|---|
| 단일 조회/추가 | O(1) | O(1) | O(1) | O(log N) |
| 전체 조회 | — | O(N) | O(N) | O(N) |
| 포함 여부 확인 | — | O(1) | O(1) | O(log N) |
| 순위 조회 | — | — | — | O(log N) |
단일 조회·추가는 O(1), Sorted Set의 삽입·순위 조회는 O 복잡도를 가집니다.
O(1): 데이터 크기와 무관하게 일정한 시간이 걸리는 연산. O(log N): 데이터가 두 배가 되어도 연산 시간이 1 증가하는 수준으로 매우 빠릅니다.
모든 데이터를 String에 JSON으로 담는 경우가 많습니다. 간단하지만 다음 문제가 있습니다.
역직렬화(Deserialization): JSON 문자열을 프로그래밍 언어의 객체로 변환하는 과정. CPU 연산이 필요하며 데이터가 클수록 비용이 높아집니다.
Hash는 이 문제를 해결합니다. 단, Hash도 필드 개수가 수십만 개가 되면 메모리 효율이 떨어집니다. 적정 크기 내에서 사용하는 것이 좋습니다.