- Published on
Redis 8.8의 Array 타입 — 스트림 이후 처음 늘어난 코어 자료구조, 그리고 Valkey에는 없는 것
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — 엿새 간격으로 나온 두 릴리스
- 스트림 이후 처음 늘어난 코어 타입
- Array가 실제로 무엇인가
- 내부 표현 — 슬라이스, 포인터 태깅, 슈퍼 디렉터리
- 명령어 18개
- ARLEN과 ARCOUNT는 다른 것을 센다
- 벤더 벤치마크가 말하는 것과 말하지 않는 것
- 문서는 아직 preview라고 적어 두었다
- Valkey에는 없다
- 8.8의 나머지 — INCREX가 더 실용적일 수 있다
- AI로 만든 자료구조라는 사실
- 그래서, 당신은 써야 하나
- 마치며
- 참고 자료
들어가며 — 엿새 간격으로 나온 두 릴리스
2026년 5월, 같은 뿌리에서 갈라진 두 프로젝트가 거의 동시에 릴리스를 냈습니다. Valkey 9.1.0이 5월 19일, Redis 8.8.0이 5월 25일. 엿새 차이입니다.
두 릴리스 노트를 나란히 놓고 읽으면 온도 차가 분명합니다. Valkey 9.1.0은 스스로 "Upgrade urgency LOW"라고 적은 첫 9.1 안정 릴리스이고, 새 기능은 클러스터 버스 트래픽 메트릭 추가와 리해싱 중 지연 스파이크 완화 두 건, 나머지는 CVE 세 건과 버그 픽스입니다. Redis 8.8.0의 목록은 다른 성격입니다 — 그 맨 위에 이렇게 적혀 있습니다: "New data structure: Array (@antirez)".
포크 드라마 얘기를 하려는 게 아닙니다. 소스 트리에서 확인되는 구체적인 차이를 보려는 것입니다. 그리고 이 차이는 실제로 src/server.h 안에 한 줄로 존재합니다.
스트림 이후 처음 늘어난 코어 타입
Redis의 코어 객체 타입은 server.h에 상수로 박혀 있습니다. Redis 8.8.0 태그의 해당 부분은 이렇습니다.
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
#define OBJ_MODULE 5 /* Module object. */
#define OBJ_STREAM 6 /* Stream object. */
#define OBJ_ARRAY 7 /* Array object. */
OBJ_ARRAY 7이 새로 생긴 줄입니다. 그 앞의 OBJ_STREAM 6은 스트림이고, XADD의 커맨드 정의를 보면 since가 5.0.0입니다. 즉 코어 객체 타입이 늘어난 것은 2018년 Redis 5.0의 스트림 이후 이번이 처음입니다.
여기서 흔한 오해를 하나 짚고 갑니다. "벡터 셋이 8.0에 들어갔잖아?" — 맞지만 벡터 셋은 코어 타입이 아닙니다. VADD의 정의는 modules/vector-sets/commands.json에 있고 그룹은 vector_set이며, 구현은 modules/vector-sets/hnsw.c입니다. 즉 Redis 트리 안에 함께 배포되는 모듈이지 t_*.c 코어 타입이 아닙니다. 반면 Array는 src/t_array.c(약 73KB)와 src/sparsearray.c(약 80KB)로 코어에 직접 들어갔습니다.
Array가 실제로 무엇인가
공식 문서의 정의는 이렇습니다 — Array는 0부터 2⁶⁴−1 범위의 정수 인덱스를 문자열 값에 매핑하는 희소(sparse) 인덱스 주소 자료구조입니다. 리스트처럼 시퀀스상의 위치로 접근하는 게 아니라 인덱스로 직접 접근하고, 사이의 빈 칸을 할당하지 않고도 아무 인덱스나 세팅할 수 있습니다.
왜 필요한지는 antirez가 PR #15162의 본문에 직접 적어 뒀습니다. 그의 표현을 그대로 옮기면, 기존 타입들은 "위치 자체가 비즈니스 의미를 갖는" 경우에 다 어딘가 어긋납니다 — 해시는 랜덤 조회를 주지만 인덱스를 키로 따로 저장해야 하고 범위 가시성이 없으며, 리스트는 추가와 트리밍을 주지만 중간에 있는 것에 접근하기 어렵고, 스트림은 append-only 이벤트라는 또 다른 물건입니다. 슬롯 37번, 스텝 4번, 행 18552번, 파일 11번째 줄 — 이런 것들이 그가 든 예입니다.
정리하면 Array가 겨냥하는 자리는 이렇습니다.
- 인덱스가 곧 도메인 키인 데이터 (시간 슬롯, 좌석 번호, 행 번호, 스텝 번호)
- 값이 듬성듬성 존재하는 희소 데이터 (센서 결측치, 일부 행에만 붙는 주석)
- 최근 N개만 유지하는 링 버퍼
내부 표현 — 슬라이스, 포인터 태깅, 슈퍼 디렉터리
antirez가 PR 본문에 적은 인코딩 설명은 네 단계입니다.
- 조밀(dense)할 때 Array는 사실상 조금 더 화려한 C 배열입니다. 인덱스를 저장하는 비용을 따로 내지 않습니다.
- 다만 완전히 평평하게 가는 대신 4096개 원소 단위의 슬라이스로 쪼개고, 각 슬라이스는 원소가 몇 개 없을 때 특수한 희소 인코딩을 씁니다. 슬라이스가 비면 디렉터리에
NULL만 남습니다. - 작은 정수, 부동소수, 짧은 문자열은 포인터 태깅되어 포인터 슬롯 자체를 넘는 추가 메모리를 쓰지 않습니다.
- 아주 희소해지면 윈도잉된 디렉터리들의 슈퍼 디렉터리를 씁니다. 이 표현은 원소가 800만 개를 넘거나 아주 높은 인덱스가 세팅됐을 때만 발동합니다.
4번이 왜 있는지는 antirez의 개발기에 나옵니다. 그는 사용자가 ARSET myarray 293842948324 foo를 해도 거대한 할당 없이 동작하기를 원했는데, 처음 잡은 2단(디렉터리 + 슬라이스) 구조로는 부족하다는 걸 구현 중에 깨달았다고 씁니다. 그래서 특정 조건에서 자료구조가 내부적으로 모양을 바꿔 슬라이스된 조밀 디렉터리들의 슈퍼 디렉터리가 되도록 다시 설계했고, 그 목적은 ARSCAN이 구간 폭이 아니라 존재하는 원소 수에 비례하는 시간에 돌게 하는 것이었습니다.
여기서 정직하게 짚을 게 있습니다. 그 목표는 "전형적인 경우"에 대한 것이지 보장이 아닙니다. ARSCAN과 AROP의 공식 복잡도 표기는 이렇습니다 — "O(P) where P is visited positions in touched slices (dense scanned slots + sparse entries), with worst-case O(|end-start|+1) and typical case close to O(N)". 최악의 경우는 구간 폭에 비례합니다. 문서가 이 캐비엇을 스스로 적어 뒀다는 점이 오히려 신뢰할 만한 신호입니다.
명령어 18개
Redis 8.8.0 태그의 src/commands/에 있는 AR* 명령어는 정확히 18개입니다.
ARSET ARGET ARMSET ARMGET ARGETRANGE ARSCAN
ARDEL ARDELRANGE ARLEN ARCOUNT ARINFO
ARINSERT ARNEXT ARSEEK ARRING ARLASTITEMS
AROP ARGREP
(antirez의 개발기에는 ARPOP이 언급되지만 8.8.0의 최종 명령어 목록에는 없습니다. 글에 나온 이름을 그대로 믿지 말고 태그된 소스를 보는 편이 안전합니다.)
성격별로 묶으면 이렇습니다.
기본 읽기/쓰기. ARSET은 주어진 인덱스부터 연속된 값 여러 개를 씁니다. ARGET은 한 인덱스를 읽고, 세팅되지 않은 인덱스는 nil을 돌려줍니다. ARMSET과 ARMGET은 인덱스-값 쌍 여러 개를 한 번에 다룹니다. 여기까지는 모두 @fast입니다.
커서 계열. Array는 대체로 무상태지만 마지막으로 추가된 항목의 인덱스 하나만 기억합니다. ARINSERT는 그 커서 자리에 넣고 커서를 전진시키고, ARNEXT는 다음에 쓰일 인덱스를 알려 주고, ARSEEK는 커서를 원하는 자리로 옮깁니다.
링 버퍼. ARRING은 크기를 지정해 넣으면 넘칠 때 알아서 감고 잘라 냅니다. ARLASTITEMS는 최근에 들어온 N개를 돌려주고 REV로 순서를 뒤집습니다.
서버 사이드 집계. AROP은 구간에 대해 SUM, MIN, MAX, AND, OR, XOR, 그리고 MATCH와 USED를 수행합니다.
검색. ARGREP은 구간의 원소를 텍스트 술어로 훑습니다. 술어는 EXACT, MATCH, GLOB, RE 네 가지이고, 여러 술어를 AND 또는 OR로 묶을 수 있으며 LIMIT, WITHVALUES, NOCASE 옵션이 붙습니다.
문서에 있는 실제 동작 예를 옮기면 이렇습니다.
ARSET events:1 0 "login" "click" "purchase" -> 3
ARGET events:1 0 -> "login"
ARGET events:1 999 -> nil
ARRING readings 3 "v0" -> 0
ARRING readings 3 "v1" -> 1
ARRING readings 3 "v2" -> 2
ARRING readings 3 "v3" -> 0 # 크기 3을 넘겨 0번으로 되감김
ARGET readings 0 -> "v3" # 가장 오래된 v0을 덮어씀
ARLASTITEMS readings 3 -> ["v1", "v2", "v3"]
ARLASTITEMS readings 3 REV -> ["v3", "v2", "v1"]
ARLEN과 ARCOUNT는 다른 것을 센다
실무에서 제일 먼저 밟을 지뢰는 이것입니다. 문서의 예를 그대로 옮깁니다.
ARSET sparse 0 "a" -> 1
ARSET sparse 1000000 "b" -> 1
ARLEN sparse -> 1000001
ARCOUNT sparse -> 2
ARLEN은 "최대 인덱스 + 1"이고 ARCOUNT는 "비어 있지 않은 원소 수"입니다. 둘 다 O(1)이지만 의미가 전혀 다릅니다. 리스트의 LLEN 감각으로 ARLEN을 쓰면 원소 두 개짜리 Array에서 백만이 나옵니다. 희소 자료구조에서는 당연한 정의지만, 당연한 정의가 새벽 세 시에 사람을 잡습니다.
ARGETRANGE와 ARSCAN의 차이도 같은 결입니다 — ARGETRANGE seq 0 3은 빈 칸을 nil로 채워 ['a', 'b', None, 'd']를 주고, ARSCAN seq 0 3은 존재하는 것만 인덱스와 함께 줍니다.
벤더 벤치마크가 말하는 것과 말하지 않는 것
Redis가 8.8 발표 글에 올린 수치입니다. 전부 벤더 자체 측정이고, 조건은 Redis 8.8 단일 인스턴스, Intel Sapphire Rapids m7i.metal-24xl, 원소 10만 개입니다.
| 연산 (10만 원소, 1KB 값) | Array | List | Hash |
|---|---|---|---|
| 랜덤 원소 읽기 | 675K ops/sec | 133K ops/sec | 626K ops/sec |
| 랜덤 원소 쓰기 | 757K ops/sec | 137K ops/sec | 689K ops/sec |
| 랜덤 원소 삭제 | 841K ops/sec | — | 730K ops/sec |
Redis의 요약은 "랜덤 원소 연산에서 Array는 해시보다 8~15% 나은 처리량을 주고 리스트보다 최소 5배 빠르다"입니다. 링 버퍼 비교는 이렇습니다.
| 링 크기 / 원소 크기 | Array (ARRING) | List (RPUSH+LTRIM) |
|---|---|---|
| 1K / 100B | 1.11M inserts/sec | 512K inserts/sec |
| 100K / 100B | 1.12M inserts/sec | 528K inserts/sec |
| 1K / 1KB | 840K inserts/sec | 424K inserts/sec |
| 100K / 1KB | 837K inserts/sec | 413K inserts/sec |
이 표를 읽는 법을 정직하게 말하면 이렇습니다.
해시 대비 이득은 크지 않습니다. 8~15%는 벤더가 자기 신기능을 홍보하며 자기 하드웨어에서 잰 수치입니다. 이미 해시를 잘 쓰고 있다면 이 숫자만으로 마이그레이션할 이유가 못 됩니다.
리스트 대비 5배는 비교 대상이 불리한 것입니다. 리스트에서 "랜덤 원소 읽기"는 원래 O(N) 연결 리스트 순회입니다. 리스트가 애초에 하면 안 되는 일을 시켜서 5배가 나온 것이지, 리스트가 느린 게 아닙니다. ARRING의 2배는 그보다 공정한 비교입니다 — RPUSH+LTRIM 두 왕복을 한 원자적 명령으로 줄인 것이니 실제로 쓰는 패턴이 맞습니다.
메모리는 Array가 더 씁니다. 같은 글의 표에 따르면 100바이트 원소에서 원소당 Array 122바이트, List 104바이트, Hash 151바이트이고, 1KB 원소에서는 각각 1290 / 1035 / 1337바이트입니다. Redis 자신의 표현으로 Array는 리스트보다 원소당 약 18% 더 많은 메모리를 씁니다. 링 버퍼를 LPUSH+LTRIM에서 ARRING으로 바꾸면 처리량 2배를 얻는 대신 메모리를 그만큼 더 냅니다.
그리고 이 표에 없는 것들이 있습니다 — 희소한 경우의 메모리 수치, 클러스터 환경, 복제/AOF 부하, ARM에서의 수치. 발표 글은 x86 단일 인스턴스만 보여 줍니다.
문서는 아직 preview라고 적어 두었다
이게 가장 중요한 캐비엇인데 발표 글에는 없고 문서에만 있습니다. 공식 Array 문서 첫 줄은 이렇습니다.
Array is a new data type that is currently in preview and may be subject to change.
GA 릴리스에 실려 나왔지만 문서는 여전히 "미리보기이며 변경될 수 있음"이라고 못 박고 있습니다. 명령어 정의의 since는 전부 8.8.0이지만, 그것이 API 안정성 약속은 아니라는 뜻입니다. 프로덕션 데이터 모델을 여기에 얹기 전에 이 한 줄을 반드시 계산에 넣어야 합니다.
캐비엇이 하나 더 있습니다. ARGREP의 정규식을 위해 TRE 라이브러리가 deps/tre로 벤더링됐습니다(8.8.0의 deps/ 목록에서 확인됩니다). antirez가 TRE를 고른 이유는 병리적 패턴에서 시간/공간이 폭발하지 않는다는 보장 때문이고, 그는 foo|bar|zap 형태의 교대(alternation)가 비효율적이던 것을 직접 최적화하고 잠재적 보안 이슈 몇 개를 고쳤다고 적었습니다. 그래도 순수한 사실은 남습니다 — 서버에 새 네이티브 C 정규식 엔진이 들어왔고, 사용자 입력 패턴이 그 위에서 돕니다. PR에 붙은 자동 리뷰 봇도 이 지점을 "새로운 네이티브 코드 표면적"으로 지목했습니다.
Valkey에는 없다
이 글의 출발점으로 돌아갑니다. Valkey 9.1.0 태그의 src/server.h를 보면 객체 타입은 OBJ_STREAM 6에서 끝납니다. OBJ_ARRAY가 없습니다. src/에는 t_hash.c, t_list.c, t_set.c, t_stream.c, t_string.c, t_zset.c 여섯 개만 있고 t_array.c가 없습니다. src/commands/에 AR로 시작하는 명령어도 없습니다.
즉 2026년 5월 기준으로 Redis는 코어 타입 7개, Valkey는 6개입니다. 같은 코드베이스에서 갈라진 두 서버가 이제 다른 자료구조 집합을 갖습니다. 이것은 성능 튜닝이나 설정 차이가 아니라 데이터 모델의 차이이고, 방향이 한쪽입니다 — Array로 모델링한 데이터는 Valkey로 옮길 수 없습니다.
여기에 라이선스가 겹칩니다. Redis 8.8의 LICENSE.txt는 RSALv2 / SSPLv1 / AGPLv3 삼중 라이선스이고, Valkey의 COPYING은 SPDX-License-Identifier: BSD-3-Clause입니다. 이 배경은 오픈소스 라이선스 지형 변화 편에서 따로 다뤘습니다. 요점만 말하면, BSD로 남기 위해 Valkey를 골랐던 조직이라면 Array는 애초에 선택지에 없고, Array가 매력적이라면 그 라이선스를 받아들이는 결정을 같이 하는 것입니다. 기능 선택이 아니라 묶음 선택입니다.
8.8의 나머지 — INCREX가 더 실용적일 수 있다
Array가 헤드라인이지만, 많은 팀에게 8.8에서 당장 쓸모 있는 건 INCREX일 수 있습니다. 레이트 리미터는 지금까지 대부분 Lua 스크립트로 구현해 왔는데, 8.8이 이걸 명령어로 만들었습니다(구현자는 raffertyyu와 Redis 팀).
INCREX key
[<BYFLOAT|BYINT> increment]
[LBOUND lowerbound] [UBOUND upperbound] [SATURATE]
[EX sec | PX msec | EXAT unix-time-sec | PXAT unix-time-msec | PERSIST]
[ENX]
발표 글이 설명하는 핵심은 세 가지입니다. 첫째, 새 카운터 값과 실제 적용된 증가분을 함께 돌려주므로 호출자가 즉시 허용/거부를 판단할 수 있습니다. 둘째, ENX를 주면 키에 만료가 없을 때만 만료를 설정하므로 윈도의 TTL이 창 생성 시에만 잡히고 이후 요청에 밀려 연장되지 않습니다 — Lua 없이 하려다 다들 한 번씩 틀리는 그 부분입니다. 셋째, 경계를 넘기면 거부하고 SATURATE를 주면 경계에 붙여 부분 수용합니다. 레이트 리미팅 알고리즘 자체는 토큰 버킷과 슬라이딩 윈도 편에 정리해 뒀습니다.
이 밖에 8.8에는 스트림 컨슈머가 메시지를 명시적으로 반납하는 XNACK(SILENT/FAIL/FATAL 모드), 해시 필드 단위 알림, ZUNION/ZINTER의 COUNT 집계자, JSON.SET의 FPHA(부동소수 배열을 BF16/FP16/FP32/FP64 중 무엇으로 저장할지 지정)가 들어갔습니다.
AI로 만든 자료구조라는 사실
antirez는 개발기에서 이 타입을 어떻게 만들었는지 숨기지 않고 적었습니다. 1월 첫 주에 시작해 첫 달은 사양 문서만 손으로 썼고, 처음에는 Opus와 페어링하다가 GPT-5.3이 나온 뒤 설계와 개발을 Codex로 옮겼다고 합니다. 둘째 달부터 자동 코딩으로 구현하며 계속 리뷰했고, 앞서 말한 인디렉션 재설계가 이때 나왔습니다. 셋째 달은 스트레스 테스트였습니다.
그의 결론을 그대로 옮기면, 고품질 시스템 프로그래밍에서는 여전히 본인이 완전히 관여해야 하지만, AI 덕분에 평소라면 건너뛰었을 복잡도 수준까지 갈 수 있었다는 것입니다. 그리고 ARGREP은 계획에 없던 기능이었습니다 — 자료구조를 이리저리 써 보다가 마크다운 파일을 Array에 넣게 됐고, 에이전트용 스킬 지식 베이스가 필요하던 자기 상황과 맞아떨어져서 만들었다고 합니다.
PR 자체의 규모는 확인 가능한 숫자로 남아 있습니다 — 커밋 18개, 86개 파일, +22,258 / −39 라인. 5월 4일에 열려 5월 13일에 머지됐고, 12일 뒤 8.8.0 GA에 실렸습니다. 이걸 어떻게 평가할지는 각자의 몫이지만, 최소한 "AI로 만든 코드가 프로덕션 데이터베이스의 코어 자료구조로 들어간" 구체적이고 문서화된 사례라는 점은 기록해 둘 만합니다. 문서의 preview 딱지를 볼 때 이 맥락을 함께 놓고 보는 것도 합리적입니다.
그래서, 당신은 써야 하나
써서 값을 하는 경우
- 인덱스가 진짜 도메인 키다 — 시간 슬롯, 좌석/칸 번호, 행 번호, 워크플로 스텝. 지금 해시에 인덱스를 문자열 필드로 박아 쓰고 있다면 정확히 이 자리입니다.
- 링 버퍼를
RPUSH+LTRIM으로 돌리고 있고, 그 왕복과 비원자성이 실제로 아프다.ARRING이 한 명령으로 줄여 줍니다. - 구간 집계를 클라이언트로 다 끌어와서 하고 있다.
AROP이 서버에서 끝냅니다.
과잉이거나 위험한 경우
- push/pop이 필요하거나 원소 사이에 삽입해야 한다 → Redis 자신이 리스트를 쓰라고 말합니다.
- 숫자 인덱스가 아니라 필드 이름으로 접근한다 → 해시를 쓰라고 말합니다.
- 메모리가 빠듯하다 → 리스트보다 원소당 약 18% 더 냅니다.
- Valkey를 쓰거나 쓸 가능성이 있다 → 존재하지 않는 타입입니다.
- API 안정성이 필요하다 → 문서가 preview라고 적어 뒀습니다.
- 이미 잘 돌아가는 해시 기반 설계가 있다 → 벤더 벤치마크의 8~15%라는 숫자는 재작성 비용을 정당화하지 못합니다.
마치며
정리하면 이렇습니다. Redis 8.8은 스트림 이후 처음으로 코어 객체 타입을 하나 늘렸고, 그 타입은 인덱스가 의미를 갖는 데이터를 위한 희소 컨테이너입니다. 4096개 슬라이스와 포인터 태깅으로 조밀/희소 양쪽을 감당하고, 명령어 18개로 집계와 grep까지 서버에서 끝냅니다. 같은 달 나온 Valkey 9.1의 소스에는 이 타입이 없고, 앞으로도 데이터 모델 수준에서 두 서버는 갈라진 채로 갑니다.
동시에 이건 GA에 실렸지만 문서가 preview라고 적어 둔 타입이고, 벤더 벤치마크의 이득 대부분은 리스트에게 리스트가 못하는 일을 시켜서 나온 것이며, 해시 대비 실질 이득은 한 자릿수~십몇 퍼센트에 메모리를 더 내는 거래입니다.
그래서 결론은 "Redis에 좋은 게 생겼으니 쓰자"가 아닙니다. 지금 인덱스를 해시 키에 문자열로 박아 넣고 있거나 LPUSH+LTRIM 왕복을 감수하고 있다면, Array는 그 특정한 불편을 정확히 겨냥한 물건입니다. 그런 불편이 없다면 8.8에서 당신에게 중요한 건 Array가 아니라 INCREX이거나, 아무것도 아닐 수 있습니다. 새 자료구조는 새 망치이고, 못이 있는지부터 확인하는 게 순서입니다.
참고 자료
- Redis 8.8.0 릴리스 노트 (GitHub)
- Valkey 9.1.0 릴리스 노트 (GitHub)
- Implement the new Redis Array type — PR #15162 (antirez)
- Redis array type: short story of a long development — antirez 개발기
- Announcing Redis 8.8 — 벤더 발표 글, 벤치마크 수치 출처
- Redis arrays 공식 문서 — preview 표기와 명령어 18개
- TRE — ARGREP의 정규식 엔진
- 인메모리 캐싱 & KV 스토어 2026 비교 (관련 글)
- 오픈소스 라이선스 지형 변화 2026 (관련 글)
- 레이트 리미팅 알고리즘 — 토큰 버킷과 슬라이딩 윈도 (관련 글)