Redis 기본 자료구조 (String, List, Set, Hash, Sorted Set)

 

Redis를 처음 접하면 메모리 기반의 빠른 캐시 서버 정도로 이해하기 쉽습니다. 하지만 Redis의 핵심은 속도보다, 데이터의 성격에 따라 자료구조를 직접 선택할 수 있다는 점에 더 가깝습니다. 단일 값을 다루는 경우, 순서가 있는 목록을 다루는 경우, 중복 없는 집합을 관리하는 경우는 전부 요구사항이 다르고, Redis는 그 차이를 하나의 구조로 억지로 처리하지 않고 자료구조를 나누어 제공합니다.

 

이 글에서는 String, List, Set, Hash, Sorted Set 다섯 가지 기본 자료구조를 중심으로, 내부 구조와 주요 명령어, 시간복잡도, 실무에서 주의할 점까지 정리해 보겠습니다.


String

특징

Redis에서 가장 많이 쓰이는 자료구조입니다. 이름은 String이지만 내부적으로는 바이트 배열(byte array) 기반으로 저장되기 때문에, 텍스트뿐 아니라 숫자, JSON 직렬화 결과, 바이너리 데이터도 담을 수 있습니다.

 

Redis는 Key-Value 저장소이고, 모든 키는 기본적으로 String 타입입니다. 값도 String이 될 수 있으므로, 가장 단순한 형태의 Redis 사용은 String 키에 String 값을 저장하는 구조입니다.

 

 

최대 512MB까지 저장할 수 있지만, 실무에서 큰 값을 하나의 키에 넣는 방식은 권장되지 않습니다. Redis는 메모리에 데이터를 올려두고 처리하기 때문에, 하나의 키에 100MB를 넣어두면 조회할 때마다 100MB를 네트워크로 전송해야 합니다. 대역폭 소모는 물론이고 다른 요청들의 처리도 지연됩니다. 그래서 대부분 chunk 형태로 작게 쪼개서 다루는 방식이 일반적입니다.

 

내부 구조: SDS

Redis는 C 언어로 구현되어 있지만, C의 기본 문자열을 그대로 사용하지는 않습니다. 대신 SDS(Simple Dynamic String)라는 자체 문자열 구조를 씁니다. SDS는 `len`(문자열 길이), `free`(남은 여유 공간), `buf[]`(실제 데이터) 세 가지 필드로 구성됩니다.

 

 

C 문자열은 길이를 확인하려면 끝까지 순회해야 하므로 O(n)이 걸립니다. SDS는 `len` 필드에 길이를 별도로 들고 있기 때문에 O(1)로 확인할 수 있습니다. Append가 발생할 때도 `free` 공간이 남아 있으면 메모리 재할당 없이 처리됩니다. 또한 C를 그대로 쓰면 버퍼 오버플로우 위험이 있는데, SDS는 LAN과 Free를 통해 이를 방지합니다. Binary Safe하기 때문에 `\0`이 포함된 바이너리 데이터도 안전하게 저장할 수 있습니다.

 

인코딩 방식

Redis는 값의 형태와 크기에 따라 내부 인코딩을 자동으로 선택합니다.

 

int는 값이 정수일 때 사용됩니다. 문자열 대신 정수 형태로 다루기 때문에 메모리 효율이 좋습니다.

 

embstr은 44바이트 이하의 짧은 문자열에 사용됩니다. Redis 객체 헤더와 SDS를 연속된 메모리에 배치해서 한 번의 메모리 할당으로 처리합니다. 속도가 빠르고 CPU 캐시 히트율도 높아집니다. 다만 embstr은 읽기 전용으로 처리되기 때문에, 수정이 발생하면 raw로 변환됩니다.

 

raw는 44바이트를 초과하는 문자열에 사용됩니다. 객체 헤더와 SDS가 별도 메모리에 할당되므로 두 번의 메모리 할당이 필요합니다.

 

 

44바이트라는 기준은 Redis 버전이나 설정에 따라 달라질 수 있지만, 핵심 원리는 동일합니다.

 

Key Naming

보통 콜론(`:`)을 구분자로 사용해서 계층 구조로 표현합니다. `user:1000:name`, `product:500:stock` 같은 형태입니다. Redis Insight 같은 모니터링 도구에서도 계층 구조를 따라 키를 탐색할 수 있어서 디버깅에도 도움이 됩니다. 다만 키 이름이 지나치게 길어지면 그 자체가 메모리 낭비이므로, 짧되 의미가 드러나는 정도가 적절합니다.

 

주요 명령어

SET / GET

SET user:1000:name "김철수"
GET user:1000:name              # "김철수"
GET user:9999:name              # (nil)

SET은 기존 값이 있어도 덮어씁니다. 이전에 어떤 타입의 값이 있었는지와 무관하게 새 값으로 교체됩니다. GET은 String 타입에 대해서만 동작하기 때문에, 같은 키 이름을 다른 자료구조와 혼용하면 에러가 발생합니다.

 

INCR / DECR

INCR page:views                 # (integer) 1
INCR page:views                 # (integer) 2
DECR page:views                 # (integer) 1

핵심은 원자성(Atomicity)입니다. 애플리케이션에서 GET → +1 → SET 순서로 직접 처리하면, 동시에 여러 요청이 들어올 때 값이 꼬일 수 있습니다. 예를 들어 두 사용자가 동시에 조회수를 증가시키면, 둘 다 100을 읽고 각각 101로 SET하게 됩니다. 원래는 102가 되어야 하지만 101이 되는 거죠. INCR은 읽기+증가+저장을 하나의 명령으로 처리하기 때문에 이런 경쟁 조건이 발생하지 않습니다.

 

키가 없으면 0에서 시작합니다. 정수가 아닌 문자열에 INCR을 시도하면 에러가 발생합니다. DECR도 원자적이며 Redis Long Type으로 음수 저장도 가능합니다.

 

INCRBY / DECRBY

INCRBY score 5
DECRBY score 3

지정한 값만큼 증가 또는 감소시킵니다.

 

MSET / MGET

MSET user:1000:name "김철수" user:1000:age "30" user:1000:city "서울"
MGET user:1000:name user:1000:age user:1000:city
# 1) "김철수"  2) "30"  3) "서울"

핵심은 RTT(Round Trip Time) 절약입니다. SET을 10번 실행하면 RTT가 1ms일 때 최소 10ms가 걸리지만, MSET으로 한 번에 보내면 1ms면 됩니다. 10배 차이입니다.

 

다만 Redis는 싱글 스레드이므로, MSET 실행 중에는 다른 명령어들이 대기합니다. 한 번에 10~100개 정도가 적절하고, 수천 개를 저장해야 한다면 나눠서 보내야 합니다. MGET의 반환 순서는 키 입력 순서가 유지되며, 존재하지 않는 키 위치에는 nil이 반환됩니다.

 

파이프라이닝과 비슷해 보이지만 차이가 있습니다. 파이프라이닝은 서로 다른 명령어(SET, GET, INCR 등)를 묶어서 보낼 수 있지만 원자성을 보장하지 않습니다. MSET/MGET은 단일 명령이므로 비교적 원자적 실행이 가능합니다.

 

SET 옵션: EX, PX, NX

SET weather:seoul "맑음" EX 600       # 600초(10분) 후 자동 삭제
SET session:abc "data" PX 30000       # 30초(밀리초) 후 삭제
SET lock:order:5001 "server-1" NX EX 30

EX, PX는 만료 시간을 함께 설정할 때 사용합니다. Redis 메모리는 제한적이기 때문에 캐시 데이터에 TTL을 거는 건 기본입니다. 최신 Redis에서는 SET 명령어 자체에 만료시간 옵션이 포함되어 있어서, 별도의 SETEX는 사실상 잘 사용하지 않습니다.

 

NX는 키가 존재하지 않을 때만 저장합니다. 분산 락 구현에서 핵심적으로 쓰입니다. 여러 서버가 동시에 같은 작업을 시작하려 할 때, 각 서버가 `SET key value NX`를 실행하면 첫 번째로 성공한 서버만 작업을 수행하고 나머지는 대기하는 구조를 만들 수 있습니다.


List

특징

여러 값을 저장하는 자료구조입니다. 다만 핵심은 값이 여러 개라는 사실보다, 그 값들이 순서를 가진 채 관리된다는 점입니다. 작업 대기열이나 메시지 소비 순서처럼, 먼저 들어온 값과 나중에 들어온 값을 구분해야 하는 상황에서 의미를 가집니다.

 

내부적으로 연결 리스트(Linked List) 기반입니다. 각 요소가 다른 요소의 메모리 주소를 가리키는 방식이기 때문에, Head와 Tail 양 끝에서 요소를 추가/제거하는 작업이 O(1)입니다. 1천만 개의 요소가 있어도 양 끝 작업의 속도는 동일합니다.

 

 

중복을 허용하고, 최대 약 42억 개(2^32-1)의 요소를 저장할 수 있습니다.

 

내부 구조: Quicklist

Redis 7.0 이전에는 요소가 적을 때 ziplist, 많을 때 linked list로 인코딩을 나누었습니다. 7.0 이후부터는 리스트팩(Listpack) 기반의 퀵 리스트(Quicklist) 하이브리드 구조를 사용합니다.

 

각 블록 내부에서는 리스트팩의 압축 인코딩을 사용하고, 블록과 블록 사이는 링크드 리스트로 연결됩니다. 메모리 효율성과 성능을 동시에 달성하는 구조입니다.

 

양 끝에서 값을 추가/제거하는 작업에는 강하지만, 중간 요소를 자주 조회하거나 임의 접근하는 용도에는 적합하지 않습니다.

 

주요 명령어

LPUSH / RPUSH

LPUSH mylist "first" "second" "third"
# 결과: third → second → first (왼쪽에 하나씩 순차 추가)

RPUSH mylist "value1"
# value1이 오른쪽 끝에 추가

반환값은 추가 후 전체 리스트의 길이입니다. 키가 없으면 자동으로 빈 리스트를 생성합니다. LPUSH는 왼쪽에 순차적으로 넣기 때문에 입력 순서와 최종 저장 순서가 달라질 수 있습니다.

 

LPOP / RPOP

LPOP mylist                     # Head에서 제거 후 반환
RPOP mylist                     # Tail에서 제거 후 반환
LPOP mylist 5                   # 5개 한 번에 제거 (Redis 6.2+)

Redis 6.2 이후부터는 count 인자로 여러 개를 한 번에 제거할 수 있습니다. count가 실제 요소 수보다 크면 남아있는 요소만 반환하고 리스트는 빈 상태가 됩니다.

 

LRANGE

LRANGE mylist 0 -1              # 전체 리스트 조회
LRANGE mylist 0 2               # 인덱스 0, 1, 2 조회 (stop 포함)

stop은 포함(inclusive)이고, 음수 인덱스도 지원됩니다. 범위를 벗어나도 에러가 아니라 자동으로 조절됩니다. 시간복잡도는 O(S+N)입니다. S는 start 인덱스까지의 이동 비용이고, N은 반환할 요소의 개수입니다. Head나 Tail에 가까운 소수의 요소를 조회하는 건 빠르지만, 중간 지점은 성능 이슈가 있을 수 있습니다.

 

LLEN / LINDEX

LLEN은 리스트의 길이를 반환하는데, 시간복잡도가 O(1)입니다. Redis가 메타데이터를 별도로 저장하기 때문에, 요소를 하나씩 세는 것이 아니라 메타데이터에서 바로 반환합니다.

 

LINDEX는 특정 인덱스의 요소를 반환합니다. 다만 시간복잡도가 O(N)입니다. Head부터 포인터를 따라가야 하기 때문입니다. 인덱스 0이나 -1처럼 양 끝은 빠르지만, 중간으로 갈수록 느려집니다.

 

중간 부분을 자주 조회해야 하는 상황이라면, List가 아닌 다른 자료구조를 고려하는 편이 맞습니다.

 

Queue와 Stack

Queue(FIFO)는 `RPUSH + LPOP` 조합으로 만들 수 있습니다. 먼저 들어간 값이 먼저 나옵니다. Stack(LIFO)은 `LPUSH + LPOP` 조합입니다. 나중에 들어간 값이 먼저 나옵니다.

 

 

BLPOP

리스트가 비어 있으면 값이 들어올 때까지 기다렸다가 꺼내는 블로킹 명령어입니다. Task Queue처럼 데이터가 들어오면 바로 처리하고, 없으면 기다리는 흐름에 맞습니다.

BLPOP mylist 5                  # 5초 대기 후 타임아웃
BLPOP mylist 0                  # 무한 대기

BLPOP이 실행되면 해당 커넥션을 점유합니다. 같은 커넥션에서 다른 명령을 함께 보내면 처리되지 않습니다. 반드시 별도 커넥션으로 분리해서 사용해야 합니다.

BLPOP은 같은 커넥션에서 다른 명령과 함께 사용하면 이벤트 루프가 정상 동작하지 않습니다. 별도 커넥션 필수입니다.

Set

특징

여러 값을 저장할 수 있지만, List와는 성격이 다릅니다. Set의 핵심은 순서가 아니라 중복을 허용하지 않는다는 점입니다.

 

같은 값을 여러 번 넣어도 실제로는 하나만 저장됩니다. 순서도 보장되지 않습니다. A, B, C 순서로 추가해도 조회 시 같은 순서로 반환된다는 보장이 없습니다. 내부적으로 해싱 처리를 하기 때문입니다.

 

 

Set의 가장 큰 장점은 특정 값의 포함 여부 확인이 O(1)이라는 점입니다. List에서 같은 작업을 하려면 전체를 순회해야 하므로 O(N)이 됩니다. 데이터가 많아질수록 이 차이는 극명해집니다.

 

내부 구조: intset과 hash table

모든 요소가 정수이고 개수가 적으면 intset이라는 메모리 효율적인 구조를 사용합니다. 이진 탐색을 통해 로그 시간으로 검색이 가능합니다.

 

요소가 많아지거나 문자열이 포함되면 hash table로 전환됩니다. hash 함수로 각 요소를 bucket에 mapping하기 때문에 O(1) 검색이 가능하지만, 메모리는 더 사용합니다.

 

주요 명령어

SADD / SISMEMBER / SREM / SCARD / SMEMBERS

SADD myset "Apple" "Banana" "Orange"    # (integer) 3
SADD myset "Apple"                       # (integer) 0 — 이미 존재

SISMEMBER myset "Apple"                  # 1 (존재) / 0 (없음), O(1)

SREM myset "Apple"                       # 삭제된 개수 반환
SCARD myset                              # 요소 개수
SMEMBERS myset                           # 전체 반환 (순서 보장 안 됨)

SADD의 반환값은 실제로 추가된 개수입니다. 이미 존재하는 값은 0으로 반환됩니다. 키가 없으면 자동으로 빈 Set을 생성합니다.

 

SMEMBERS는 전체를 한 번에 가져오기 때문에, 데이터가 큰 경우 주의가 필요합니다. Redis는 싱글 스레드이므로 응답이 커지면 그동안 다른 요청도 대기하게 됩니다.

 

SRANDOMMEMBER / SPOP

SRANDOMMEMBER myset 2           # 2개 랜덤 반환 (제거 안 함)
SPOP myset                      # 1개 랜덤 제거 후 반환

 

집합 연산

# Set1: {A, B, C}   Set2: {B, C, D}
SINTER set1 set2                # {B, C} — 교집합
SUNION set1 set2                # {A, B, C, D} — 합집합
SDIFF set1 set2                 # {A} — 차집합

3개 이상의 Set에 대해서도 가능합니다. SINTER의 시간복잡도는 O(N×M)입니다. N은 가장 작은 Set의 크기이고, M은 Set의 개수입니다.

 

다만 집합 크기가 커질수록 연산 비용도 커집니다. Redis가 싱글 스레드라는 점을 고려하면, 큰 Set에 대한 집합 연산은 데이터 규모와 호출 빈도를 함께 따져야 합니다.

SMEMBERS나 집합 연산을 큰 Set에서 실행하면, 싱글 스레드 특성상 다른 요청이 블로킹될 수 있습니다.

 

활용 예: 좋아요 시스템

`SADD post:likes "user1"`로 좋아요 추가, `SCARD post:likes`로 좋아요 수 확인, `SISMEMBER`로 특정 사용자의 좋아요 여부를 O(1)로 확인할 수 있습니다. 중복 좋아요도 Set의 특성으로 자동 방지됩니다.


Hash

특징

하나의 키 아래에 여러 개의 필드-값(Field-Value) 쌍을 저장하는 자료구조입니다. 프로그래밍 언어의 HashMap이나 Dictionary와 비슷하고, 관계형 데이터베이스의 한 행(row)에 비유할 수도 있습니다.

 

 

`user:1000`이라는 키 하나 아래에 `name`, `email`, `age` 같은 필드를 묶어서 저장할 수 있습니다. 각 필드 이름도 값도 모두 문자열이고 Binary Safe합니다.

 

String과의 차이

같은 데이터를 String 여러 개로 나눠 저장할 수도 있고, 객체 전체를 JSON으로 직렬화해서 String에 넣을 수도 있습니다. 실무에서는 이런 방식도 많이 쓰입니다.

 

다만 메모리 관점에서 차이가 있습니다. String으로 `user:1000:name`, `user:1000:email`, `user:1000:age`를 각각 저장하면 키 3개에 메타데이터 3개가 발생합니다. Hash로 `user:1000` 하나의 키에 세 필드를 담으면 키 1개, 메타데이터 1개로 끝납니다.

 

 

특정 필드 하나만 자주 수정하거나, 일부 필드만 조회해야 하는 경우에는 Hash가 더 적합합니다. 반대로 객체 전체를 통째로 읽고 쓰는 경우가 많다면 String 직렬화 방식이 더 단순할 수 있습니다. 선택 기준은 자료구조 자체보다 접근 패턴입니다.

 

내부 구조: ListPack과 HashTable

ListPack은 필드와 값을 연속된 메모리 공간에 순차적으로 저장하는 압축 인코딩입니다. 두 가지 조건을 모두 만족할 때 사용됩니다. 첫째, 필드 수가 512개 이하. 둘째, 모든 필드 이름과 값의 크기가 64바이트 이하.

 

필드를 찾으려면 처음부터 탐색해야 하지만, ListPack 자체가 매우 작은 구조이기 때문에 실질적으로 빠릅니다.

 

이 두 조건 중 하나라도 벗어나면 HashTable로 전환됩니다. 필드 이름을 해시 함수로 계산해 버킷에 저장하는 구조입니다. 필드 접근이 O(1)로 빠르지만 메모리를 더 사용합니다. Redis는 이 전환을 자동으로 처리합니다.

 

주요 명령어

HSET

HSET user:1000 name "alice" email "alice@example.com" age "25"

Redis 4.0 이상부터는 여러 필드를 한 번에 설정할 수 있습니다. 반환값은 새로 추가된 필드의 개수만 반환합니다. 기존 필드를 업데이트한 경우에는 포함되지 않습니다.

 

HGET / HMGET

HGET user:1000 name             # "alice"
HMGET user:1000 name email age  # ["alice", "alice@example.com", "25"]

HMGET은 여러 필드를 한 번에 가져옵니다. HGET을 여러 번 호출하는 것보다 RTT 관점에서 효율적입니다. MGET과 동일한 원리입니다. 존재하지 않는 필드는 nil로 반환됩니다.

 

HGETALL / HKEYS / HVALS

HGETALL user:1000               # 모든 필드와 값
HKEYS user:1000                 # 필드명만
HVALS user:1000                 # 값만

HGETALL은 모든 필드와 값을 가져오기 때문에, Hash가 커질수록 응답 크기와 처리 비용도 같이 커집니다.

 

HEXISTS는 특정 필드의 존재 여부를 0 또는 1로 반환합니다. 전체 값을 가져오는 것보다 네트워크 비용이 적습니다.

 

HINCRBY

HINCRBY user:1000 age 1         # age: 25 → 26 (원자적 증가)

Hash 내부의 숫자 필드를 원자적으로 증가시킵니다. 존재하지 않는 필드면 0에서 시작합니다.

 

원자성

Hash에서 중요한 부분은 여러 필드를 설정할 때의 원자적 처리입니다. Redis 사용 시 원자성을 최대한 활용해서 데이터 일관성을 유지하는 것이 핵심입니다. 하나의 명령어로 처리하고, 필요하면 분산락을 함께 활용하는 방식이 좋습니다.


Sorted Set

특징

Set처럼 멤버의 중복은 허용하지 않지만, 각 멤버마다 score라는 숫자 값을 함께 저장하고 그 값을 기준으로 자동 정렬된다는 점이 다릅니다.

 

 

리더보드, 랭킹, 점수 기반 추천, 우선순위 처리처럼 값 자체보다 점수와 순위가 중요한 문제에서 사용됩니다. 각 요소는 멤버(문자열)와 스코어(부동 소수점 값) 두 부분으로 구성됩니다. 동일한 Score를 가진 멤버는 사전순(lexicographical)으로 정렬됩니다.

 

내부 구조: 스킵 리스트 + 해시 테이블

스킵 리스트(Skip List)해시 테이블을 결합한 이중 구조를 사용합니다.

 

스킵 리스트는 여러 레벨의 포인터를 유지해서 O(log N) 검색을 지원합니다. 스코어 기반의 범위 조회나 순위 조회에 쓰입니다. 해시 테이블은 멤버 기반 조회에 쓰여서 O(1)로 특정 멤버를 찾을 수 있습니다.

 

 

이중 구조이기 때문에 단순 Set보다 2~3배 정도 메모리를 더 사용합니다. 메모리를 더 쓰는 대신 정렬과 검색을 모두 빠르게 처리할 수 있는 트레이드오프입니다.

 

주요 명령어

ZADD

ZADD leaderboard 100 "Alice" 95 "Bob" 85 "Charlie" 85 "Dave"

키가 없으면 자동 생성됩니다. 반환값은 새로 추가된 요소의 개수입니다. 이미 존재하는 멤버에 ZADD를 하면 기본적으로 스코어가 업데이트됩니다.

 

ZADD leaderboard NX 100 "NewPlayer"     # 존재하지 않을 때만 추가
ZADD leaderboard XX 150 "Alice"         # 존재할 때만 갱신
ZADD leaderboard GT 200 "Alice"         # 기존 점수보다 클 때만
ZADD leaderboard LT 50 "Alice"          # 기존 점수보다 작을 때만

NX는 새 멤버만, XX는 기존 멤버만, GT는 더 큰 값만, LT는 더 작은 값만 적용됩니다. CH 옵션을 붙이면 새로 추가된 것뿐 아니라 변경된 요소 수도 반환에 포함됩니다.

 

ZRANGE / ZREVRANGE

ZRANGE leaderboard 0 -1 WITHSCORES     # 전체 (낮은 점수부터)
ZREVRANGE leaderboard 0 2 WITHSCORES   # 상위 3명 (높은 점수부터)

순위는 0부터 시작합니다. ZRANGE 기준으로 스코어가 가장 낮은 것이 순위 0이고, ZREVRANGE 기준으로는 가장 높은 것이 순위 0입니다. 음수 인덱스도 지원됩니다. Redis 6.2 이상에서는 ZRANGE에 REV 옵션을 붙여서 ZREVRANGE와 동일하게 사용할 수 있습니다.

 

ZRANK / ZREVRANK

ZRANK leaderboard "Alice"               # 낮은 점수 기준 순위
ZREVRANK leaderboard "Alice"            # 높은 점수 기준 순위 (0이면 1등)

리더보드에서 특정 사용자의 순위를 확인할 때 ZREVRANK를 사용합니다.

 

ZINCRBY

ZINCRBY leaderboard 30 "Alice"          # Alice 스코어 30 원자적 증가

특정 멤버의 점수를 원자적으로 증가시킵니다. 누적 점수 구조에서 많이 사용됩니다. ZINCRBY는 한 개의 멤버만 증가시킬 수 있습니다.

 

활용 예: 리더보드

점수를 저장하고(ZADD), 상위 N명을 조회하고(ZREVRANGE), 특정 사용자의 순위를 확인하고(ZREVRANK), 점수를 증가시키는(ZINCRBY) 흐름이 모두 Sorted Set 명령어와 직접 대응됩니다. 값의 존재 여부보다 점수와 순위가 중요한 문제라면 Sorted Set이 가장 먼저 후보에 올라옵니다.


마무리

Redis의 기본 자료구조 5가지는 각각 서로 다른 종류의 데이터를 처리하기 위해 존재합니다. 값 하나를 저장하는 문제인지, 순서가 중요한지, 중복 제거가 필요한지, 필드 단위 접근이 필요한지, 순위가 핵심인지에 따라 자료구조가 나뉘어 있습니다.

 

한 가지 같이 봐야 할 점은, Redis가 싱글 스레드라는 점입니다. 모든 명령어가 순차 처리되기 때문에, 큰 데이터에 대한 연산이 오래 걸리면 다른 요청이 블로킹됩니다. MSET에 수천 개를 넣거나, 큰 Set에서 SMEMBERS를 호출하거나, BLPOP을 같은 커넥션에서 다른 명령과 함께 쓰는 것 모두 이 특성에서 비롯된 주의사항입니다.

 

Redis를 잘 사용한다는 것은 명령어를 많이 아는 것보다, 지금 다루는 데이터의 성격을 먼저 구분할 수 있는 데 더 가깝습니다. 그 위에서 내부 구조와 시간복잡도를 함께 이해할 때, 실무에서도 안정적으로 사용할 수 있습니다.