레디스의 자료구조들은 굳이 알아야 할까?
- 개발
- 2026. 7. 8. 08:58

레디스에는 다양한 명령어와 자료구조가 존재합니다. 하지만 막상 어떤 자료구조들이 있는지 하나씩 찾아보며 학습하는 경우는 많지 않습니다. 제가 그 이유를 생각해 보니 크게 두 가지였습니다. 첫 번째는, 프로그래밍을 학습하면서 리스트, 집합, 맵과 같은 기본적인 자료구조를 이미 익혔다고 생각했습니다. 그래서 자료구조 자체는 이미 알고 있다고 생각하며, 굳이 다시 학습할 필요성을 느끼지 못했습니다. 두 번째는, 세상에는 수많은 자료구조가 존재하기 때문에 모든 것을 미리 학습하는 것은 비효율적이라고 생각했습니다. 필요할 때마다 찾아보고 사용하는 것이 더 효율적이라고 판단하게 된다고 생각하였습니다. 그럼에도 제가 레디스의 자료구조를 학습하기로 마음먹은 이유가 있습니다. 레디스의 자료구조는 단순히 데이터를 저장하는 컨테이너가 아니라, 각 자료구조에 대해 원자적인 연산(Atomic Operation) 을 제공합니다. 그렇다면 과연 모든 자료구조가 어떤 원자적 연산을 제공하며, 이를 어떻게 활용할 수 있을지 궁금해졌습니다. 물론 레디스의 모든 자료구조를 학습할 생각은 없습니다. 하지만 필요한 자료구조를 판단할 수 있을 정도의 이해는 갖추고 싶었습니다. 과연 '필요할 때만 찾아보면 된다'는 과거의 생각이 정말 맞았을까요? 이번 글에서는 그 생각을 직접 깨보려고 합니다.
대표적인 자료구조 5종 부터 파악해봅시다.
레디스를 처음 접하면 생각보다 자료구조의 종류가 많아 보입니다. 하지만 실제 서비스에서 가장 많이 사용되는 자료구조는 다음 다섯 가지입니다. String, Hash, List, Set, Sorted Set이 존재합니다.

이 다섯 가지를 이해하면 대부분의 Redis 활용 사례를 이해할 수 있습니다. 이제 각 자료구조가 어떤 특징을 가지며, 어떤 원자적 연산을 제공하는지 하나씩 알아보겠습니다.
String
이름 때문에 문자열만 저장하는 자료구조라고 생각하기 쉽습니다. 하지만 Redis의 String은 문자의 연결이 아니라 바이트(Byte)의 연속된 데이터를 의미합니다. 따라서 텍스트뿐만 아니라 숫자, JSON, 직렬화된 객체, 이미지와 같은 바이너리 데이터까지 저장할 수 있습니다.
그렇다면, 어째서 Redis는 바이트 단위로 저장할까요?
가장 큰 이유는 범용성입니다. 문자 단위로 저장한다면 인코딩(UTF-8, EUC-KR 등)을 신경 써야 하고, 이미지나 직렬화된 객체 같은 바이너리 데이터는 저장하기 어려워집니다. 바이트 단위로 저장하면 이런 구분 없이 모든 데이터를 그대로 담을 수 있습니다.
이를 실제로 저장해봅시다.

이모지를 저장하고 Get을 시켜보겠습니다.


바이트단위로 저장이 된것을 확인 할 수 있었습니다.
그렇다면, String으로 저장된 데이터는 어떤식으로 이용할 수 있을까요?
String은 데이터를 바이트 단위로 저장합니다. Redis는 싱글 스레드로 명령을 처리하기 때문에, String의 모든 단일 명령은 원자적으로 실행됩니다. 즉, 여러 클라이언트가 동시에 같은 값을 조작해도 각 명령은 끊김 없이 실행됩니다. 이러한 원자성 덕분에 String은 캐시, 분산 락, 카운터 등 값 하나를 안전하게 다뤄야 하는 상황에서 널리 활용됩니다.
하지만 이 원자성은 '단일 값' 단위입니다. 만약 논리적으로 함께 다뤄져야 할 값들이 여럿이라면, 예를 들어 사용자의 이름과 나이가 언제나 함께 갱신되어야 한다면 String을 두 개 사용해서는 원자성을 확보할 수 없습니다. 이런 경우엔 필드 단위로 여러 값을 함께 관리할 수 있는 Hash 같은 다른 자료구조가 필요합니다.
Hash
Hash는 String과 달리 하나의 키 안에서 필드 단위로 접근/수정이 가능합니다. 앞서 Redis의 명령이 원자적으로 실행된다는 점을 살펴봤습니다. 그렇다면 Hash 역시 원자성을 가진다는 뜻이 됩니다.
그렇다면 Hash의 원자성은 String과 무엇이 다를까요?
100명의 사용자가 동시에 사용자 정보를 수정하는 상황을 가정해 보겠습니다.
먼저 String으로 사용자 정보를 저장했다고 가정해 보겠습니다. 이름과 나이를 각각 다른 키에 저장하고 있다면, 이름을 변경하는 SET 명령과 나이를 변경하는 SET 명령은 서로 별개의 명령으로 실행됩니다.
예를 들어 A 클라이언트가 이름을 먼저 변경했다고 하겠습니다. 아직 나이를 변경하기 전에 B 클라이언트가 데이터를 조회한다면 새로운 이름과 기존 나이를 함께 조회할 수 있습니다. 즉, 하나의 사용자 정보가 중간 상태로 노출되어 일관성이 깨질 수 있습니다.
반면 Hash는 하나의 HSET 명령으로 여러 필드를 동시에 수정할 수 있습니다. 예를 들어 이름과 나이를 하나의 HSET 명령으로 변경한다면, 해당 명령은 원자적으로 실행됩니다. 따라서 다른 클라이언트는 수정이 모두 완료된 상태 또는 수정되기 전 상태만 조회할 수 있으며, 이름만 변경되고 나이는 변경되지 않은 중간 상태를 볼 수 없습니다.
즉, Hash가 String보다 더 원자적인 자료구조인 것이 아닙니다. 두 자료구조 모두 Redis 명령은 원자적으로 실행됩니다. 차이점은 String은 여러 값을 수정하려면 여러 개의 명령이 필요하지만, Hash는 여러 필드를 하나의 명령으로 수정할 수 있다는 점입니다. 이 차이로 인해 여러 필드를 함께 관리해야 하는 경우에는 Hash가 데이터의 일관성을 유지하기에 더 적합합니다.
그렇다면, Hash는 언제 사용할까요?
String과 Hash가 크게 다르지 않으니 사용처도 비슷할 것이라 예상할 수 있습니다. 이는 반은 맞고 반은 틀린 답입니다.
사용처의 카테고리 자체는 비슷합니다.
캐싱, 세션, 카운터 등 String이 잘 쓰이는 곳에서 Hash도 자주 쓰입니다. 다만 어느 쪽이 더 자연스러운지는 상황에 따라 다릅니다. 분산 락, 바이너리 통 저장, 단순 캐시된 문자열 같은 경우는 String이 더 자연스럽습니다. 반면 하나의 객체를 이루는 여러 필드를 관리하거나, 부분 갱신이 잦은 상태를 다뤄야 한다면 Hash가 더 자연스럽습니다.
즉, 어떤 자료구조를 쓰느냐는 개발자의 재량이지만, '자연스러움'을 기준으로 판단하는 것이 좋습니다.
List
이제부터 단일 객체가 아닌, 여러 객체를 다루는 자료구조들을 알아봅시다. 그 중 가장 먼저 학습할 자료구조는 List입니다.
프로그래밍 언어에서 List는 여러 값을 순서대로 저장할 수 있는 자료구조입니다.
예를 들어 자바로 작성하면 다음과 같습니다.
List<String> strs = new ArratList<>();
strs.add("1");
strs.add("2");
strs.add("3");List는 입력한 순서를 유지하고, 중복된 값도 허용한다는 특징이 있습니다. Redis에서는 이러한 List를 어떻게 다루는지 확인해봅시다.
단일 값으로는 String과 Hash가 있습니다. (굳이 따진다면, String은 값이고 Hash는 객체지만 딱히 분류하지는 않겠습니다.)
그렇다면, Redis List에는 어떠한 값이 들어갈 수 있을까요? 그리고 어떻게 사용할까요?
결론부터 말하면, Redis List의 원소로는 String(바이트 시퀀스)만 넣을 수 있습니다. Hash처럼 구조화된 자료구조를 List 원소로 담는 것은 불가능합니다. 이는 Redis의 설계 철학에서 비롯됩니다. Redis는 '단순함'을 최우선으로 두어, 자료구조의 중첩을 허용하지 않습니다. 모든 자료구조는 최상위 키에만 존재하며, 그 안의 원소 값은 언제나 바이트 시퀀스입니다.
그렇다면, List 자료구조는 어떻게 원자성을 지킬 수 있을까?
List의 원자성은 3가지 레벨로 나눠 생각할 수 있습니다.
기본적으로 Redis는 싱글 스레드로 명령을 처리하기 때문에 List의 모든 단일 명령이 원자적으로 실행됩니다.
RPUSH tasks "job" → 원자적 추가
LPOP tasks → 원자적 꺼내기
LRANGE tasks 0 -1 → 원자적 스냅샷 조회즉, 여러 클라이언트가 동시에 조작해도 각 명령은 끊김 없이 실행됩니다. 원자성의 원천은 자료구조가 아닌 Redis의 실행 모델에 있기 때문입니다. 그 다음으로, Hash가 여러 필드를 한 명령으로 다뤘듯 List도 여러 원소를 한 명령으로 처리할 수 있습니다.
RPUSH tasks "job1" "job2" "job3"세 원소가 원자적으로 한번에 추가되고, 중간에 다른 클라이언트가 낄 수 없습니다. 이 덕분에 큐잉 작업을 안전하게 처리할 수 있습니다.
마지막으로, List에는 리스트 간 원자적 이동이라는 특수 연산이 있습니다.
LMOVE (또는 옛 RPOPLPUSH)
LMOVE source dest LEFT RIGHT한 리스트에서 꺼내 다른 리스트로 옮기는 작업이 한 명령으로 이뤄지기 때문에 원자적입니다. 이것이 중요한 이유는, 만약 두 명령으로 나눠 처리한다면 다음과 같은 유실 위험이 있기 때문입니다.
LPOP source → "job" 꺼냄
↓ 여기서 서버 죽으면?
→ job 유실!
RPUSH dest "job"LMOVE는 이 사이의 유실 가능성을 원천 차단합니다. 또한, BLPOP 같은 블로킹 팝 명령도 있습니다. 값이 없으면 원자적으로 대기하고, 값이 생기면 원자적으로 꺼내옵니다. 폴링 없이 안전한 큐 소비 패턴에 활용됩니다.
그렇다면, List는 어떻게 사용 할까요?
List는 앞서 살펴봤듯이 순서가 존재하며, 중복을 허용하는 자료구조입니다. 그렇기 때문에 입력한 순서를 유지해야 하는 상황에서 주로 사용됩니다. 예를 들어 메시지 큐, 최근 목록, 히스토리, 스택 등에서 활용할 수 있습니다.
Set
앞서 살펴본 List와 비교하면, Set은 크게 달라 보이지 않을 수 있습니다. 차이점은 중복을 허용하지 않고, 순서 또한 유지하지 않는다는 점입니다. 프로그래밍 언어의 Set 자료구조를 떠올려보면, 중복을 자동으로 제거해주고 존재 확인이 빠르다는 특징이 있습니다.
Redis에서 Set은 어떤 특별함이 있을까?
사실 프로그래밍 Set과 굉장히 유사합니다. 하지만 Redis는 모든 값들을 원자단위로 관리합니다. 전통적으로 Set은 집합 연산에 능합니다. 이를 Redis에서는 원자단위로 할 수 있다는 점입니다. 예를 들어, 교집합, 합집합, 차집합이 있습니다.
만약 프로그래밍상에서 이들을 구하려면 여러 연산이 뒤섞여서 찾아야 합니다.
Set의 진짜 특별함은 집합 연산에 있습니다. 교집합, 합집합, 차집합 같은 연산을 원자적인 단일 명령으로 처리할 수 있습니다.
예를 들어 Set A에는 {1, 2, 3}이, Set B에는 {3, 4, 5}가 있다고 해봅시다.
- 합집합 A ∪ B = {1, 2, 3, 4, 5}
- 교집합 A ∩ B = {3}
- 차집합 A - B = {1, 2}
일반적인 프로그래밍 언어에서 이런 연산을 하려면 두 집합을 순회하며 조건을 검사하는 여러 단계가 필요합니다. Redis에서는 이를 명령 하나로 원자적으로 처리합니다. 게다가 서로 다른 클라이언트가 각기 관리하는 집합에 대해서도 동일하게 동작하기 때문에, 분산 환경에서의 집합 관계 계산도 자연스럽게 가능해집니다.
그렇다면 어떻게 활용되어질까?
복습해보면, List는 중복은 상관없고 순서가 중요한 경우에 주로 사용된다고 했습니다. 반면 Set의 가장 큰 특징은 중복을 허용하지 않는다는 점입니다. 대표적인 활용 예로는 태그 관리, 팔로우 관계, 방문자 분석, 좋아요 이력, 권한 관리 등이 있습니다. 중복 없이 값을 담아야
하거나, 특정 값의 존재 여부를 빠르게 확인해야 하거나, 두 집합의 공통 원소나 차이를 계산해야 하는 경우 Set이 잘 어울립니다.
특히 팔로우 관계에서 '공통 친구', 방문자 분석에서 '충성 유저 vs 신규 유저 vs 이탈 유저' 같은 계산이 Set의 집합 연산 하나로
해결됩니다.
Sorted Set
Sorted Set은 특별합니다. 단순한 Set이 아니라 정렬까지 지원하기 때문입니다. Set과 마찬가지로 중복은 제거되지만, 각 원소에 ’점수(score)‘를 부여해 그 점수를 기준으로 자동 정렬됩니다.
그런데 여기서 한 가지 의문이 생깁니다. List나 Set은 원소를 추가하기만 합니다. 하지만 Sorted Set은 원소 추가와 함께 정렬까지 수행합니다. 한 명령 안에서 두 가지 일이 벌어지는 셈인데, 이것이 원자적으로 가능할까요? 그리고 성능은 어떻게 유지될까요?
Sorted Set이 원자적으로 정렬과 원소 추가를 할 수 는 방법?
원자성의 원천은 다른 자료구조와 같습니다. Redis는 싱글 스레드로 명령을 하나씩 처리하기 때문에, 명령 안에서 아무리 여러 단계를 거치더라도 그 명령이 실행되는 동안 다른 클라이언트가 끼어들 수 없습니다. 그렇다면 성능은 어떻게 유지될까요? 원소를 넣을 때마다 전체를 정렬한다면 원소가 많아질수록 급격히 느려질 것입니다. Redis는 Skip List라는 자료구조를 활용해 이 문제를 해결합니다. Skip List는 '매번 정렬'하는 것이 아니라 '항상 정렬된 상태를 유지'하는 자료구조로, 원소가 100만 개여도 삽입 위치를 로그 시간에 찾아냅니다.
즉, Sorted Set은 원자적인 동시에 빠릅니다. Redis의 실행 모델이 원자성을 보장하고, Skip List의 특성이 성능을 보장합니다.
그렇다면, Sorted Set은 어떻게 활용할까요?
Sorted Set은 순위, 우선순위, 시간 순서 같은 '점수로 표현할 수 있는 정렬 기준'이 필요할 때 자연스럽습니다. 대표적인 활용 예로는 실시간 랭킹(게임 점수, 인기 게시글), 우선순위 큐(긴급 작업 먼저), 시계열 데이터(활동 이력, 로그), 지연 큐(예약 발송), 자동완성(빈도 기반 추천) 등이 있습니다.
특히 실시간 랭킹에서 점수 증가와 순위 재정렬이 명령 하나로 원자적으로 처리된다는 점이 강력합니다. 일반적인 데이터베이스에서는 트랜잭션과 잠금을 신경 써야 하는 작업이 Sorted Set에서는 자연스럽게 해결됩니다.
이 외에도 Stream이라는 자료구조가 5.0에서 추가되었습니다. 로그형 자료구조로 이벤트 스트리밍이나 메시지 큐에 활용되지만, 개념이 다른 차원이라 별도 학습이 필요합니다. 이 글에서는 기본 5개 자료구조에 집중합니다.
모든 자료구조의 기본적인 명령어를 표로 알아보자.
추가 (ADD/SET/PUSH)
| 패턴 | String | Hash | List | Set | Sorted Set |
| 기본 추가 | SET | HSET | LPUSH / RPUSH | SADD | ZADD |
| 조건부 추가 | SET ... NX | HSETNX | - | - | ZADD ... NX |
조회 (GET)
| 패턴 | String | Hash | List | Set | Sorted Set |
| 단건 조회 | GET | HGET | LINDEX | SISMEMBER | ZSCORE |
| 다건 조회 | MGET | HMGET | LRANGE | SMISMEMBER | ZRANGE |
| 전체 조회 | GET | HGETALL | LRANGE 0 -1 | SMEMBERS | ZRANGE 0 -1 |
삭제 (DEL / REM)
| 패턴 | String | Hash | List | Set | Sorted Set |
| 값/필드 삭제 | DEL (키) | HDEL | LREM | SREM | ZREM |
| 꺼내면서 삭제 | GETDEL | - | LPOP / RPOP | SPOP | ZPOPMIN / ZPOPMAX |
개수 (LEN / CARD)
| 패턴 | String | Hash | List | Set | Sorted Set |
| 개수 | STRLEN (바이트) | HLEN | LLEN | SCARD | ZCARD |
존재 확인 (EXISTS)
| 패턴 | String | Hash | List | Set | Sorted Set |
| 존재 확인 | EXISTS (키) | HEXISTS | - | SISMEMBER | ZSCORE (nil 여부) |
증가 (INCRBY)
| 패턴 | String | Hash | List | Set | Sorted Set |
| 원자 증가 | INCR / INCRBY | HINCRBY | - | - | ZINCRBY |
반복 순회 (SCAN)
| 패턴 | String | Hash | List | Set | Sorted Set |
| 대용량 순회 | SCAN (키) | HSCAN | - | SSCAN | ZSCAN |
블로킹 (B 접두사)
| 패턴 | List | Sorted Set |
| 블로킹 꺼내기 | BLPOP / BRPOP | BZPOPMIN / BZPOPMAX |
예외
| 예외 명령 | 이유 |
| MSET / MGET | Multiple의 M (String 전용) |
| GETSET / GETDEL / GETEX | 복합 동작 (String) |
| SINTER / SUNION / SDIFF | Set 집합 연산 (교/합/차) |
| LMOVE / RPOPLPUSH | List 간 원자 이동 |
| ZRANGEBYSCORE / ZRANGEBYLEX | Sorted Set 범위 종류 |
마무리
이번 글에서는 Redis의 자료구조에 대해 살펴봤습니다. 학습을 진행하면서 Redis의 모든 자료구조가 원자적으로 동작한다는 것을 알게 되었습니다. 그러다보니 자연스럽게 "어떻게 원자적으로 가능한지"가 궁금해졌습니다. 여러 자료를 찾아보며 '아, 이래서 원자적이구나' 하고 깨닫게 되었습니다.
또한 자료구조들이 비슷해 보여도 각자의 사용처가 확실하게 구분된다는 점을 느꼈습니다. 처음에는 "필요할 때만 찾아보면되겠지" 싶었지만, 원리를 이해하고 나니 언제 어떤 자료구조를 선택해야 하는지 감이 잡히더군요.
몇 가지 남은 아쉬움도 있습니다. 첫째, Stream이라는 자료구조는 다루지 않았습니다. 지금까지 살펴본 자료구조와 성격이 상이하다고 느꼈기 때문입니다. 기본 자료구조와는 다른 카테고리로 느껴져 이 글에 함께 담기에는 무리가 있었습니다. 이 부분은 나중에 별도의 글로 풀어보려고 합니다. 둘째, 각 자료구조 간 비교가 충분하지 않은 것도 사실입니다. 자바 자료구조와의 대비를 통해 실무 감각을 더 살려볼 수 있을텐데, 이 부분도 다음 기회에 시도해보려고 합니다.
긴 글 읽어주셔서 감사합니다.
'개발' 카테고리의 다른 글
| 레디스가 싱글 스레드임에도 불구하고 빠른이유 (0) | 2026.07.04 |
|---|---|
| 경쟁조건을 해결하거나 완화하기 위한 다양한 기술들 (0) | 2026.06.29 |
| Rate Limiter (intro.) (0) | 2026.06.28 |
| 로그인은 어떤 방식으로 개발해야 안전할까? (0) | 2026.06.14 |
| 배치 작업시 예외가 발생한다면 어떻게 처리할까? (0) | 2026.05.06 |