본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 요약
오늘은 Redis의 핵심 컬렉션 데이터 타입 중 List, Set, Hash에 대해 학습했다. 주로 연산자 종류와 간단한 실습 내용만 존재하여 각 타입의 개념과 내부 구조, 주요 연산자의 시간 복잡도, 그리고 실무에서 주의해야 할 점들을 추가적으로 학습 및 정리해보았다.
List 타입
List는 양방향 삽입/삭제가 가능한 순서형 컬렉션이다. FIFO(Queue) 또는 LIFO(Stack) 구조로 활용할 수 있으며, 동일한 키에 여러 값을 순차적으로 저장한다.
내부 구조
- List는 quickList라는 자료구조로 구현되어 있다.
- quickList는 이중 연결 리스트(Doubly Linked List)의 각 노드가 작은 압축 리스트를 담고 있는 하이브리드 구조다.
- Redis 7.0부터 ziplist가 listpack으로 대체되었다.
- listpack은 각 노드의 길이를 노드 끝에 배치하여 ziplist의 연쇄 업데이트 문제를 완전히 해결했다.
- 연속된 메모리 블록을 사용하기 때문에 CPU 캐시 효율성이 높고, 작은 데이터에서 메모리 사용량이 적다.
| Redis ≤ 6.2 | ziplist | 연속된 메모리 블록, 중간 삽입/삭제 시 cascading update 발생 가능 |
| Redis ≥ 7.0 | listpack | ziplist 개선 버전, cascading update 문제 해결, 더 컴팩트한 구조 |
주요 연산 및 시간 복잡도
| 연산 | 시간 복잡도 | 설명 |
| LPUSH / RPUSH | O(1) | 양 끝 삽입 |
| LPOP / RPOP | O(1) | 양 끝 제거 |
| LLEN | O(1) | 길이 조회 |
| LINDEX | O(N) | 인덱스로 조회 |
| LRANGE | O(S+N) | 범위 조회 (S: 시작 오프셋, N: 범위 내 요소 수) |
| LREM | O(N+M) | 값으로 요소 제거 (N: 리스트 길이, M: 제거된 요소 수) |
| LSET | O(N) | 인덱스로 값 수정 |
| LINSERT | O(N) | 특정 값 앞/뒤에 삽입 |
- 리스트의 head or tail에 접근하는 연산은 O(1) 복잡도를 가지며, 매우 효율적
- 그러나 목록 내 요소를 조작하는 명령어는 일반적으로 O(n) 복잡도를 가진다.
- LINDEX, LINSERT, LSET 등이 이에 해당하며, 특히 대규모 목록을 처리할 때 이러한 명령어를 실행할 때는 주의가 필요하다.
활용 정리
큐, 로그처럼 순서 있는 데이터 처리에 적합하며 고정 길이 유지도 가능
- 작업 큐: LPUSH + BRPOP(블로킹 대기)
- 최근 로그: LPUSH + LTRIM
- 키 분할: 너무 큰 리스트는 키에 샤딩 값 붙여서 분산 처리
- 양방향 처리: LPUSH, RPUSH, LPOP, RPOP 모두 O(1)로 매우 효율적
Set 타입
중복을 허용하지 않는 문자열 집합이다. 순서를 보장하지 않으며 수학적 집합과 유사하다.
삽입, 삭제, 존재 여부 확인이 빠르고 집삽 연산 명령어도 제공된다.
내부 구조
| 모든 버전 | hashtable | 일반적인 경우 |
| 모든 버전 | intset | 모든 요소가 정수이고 개수가 적을 때 |
| Redis ≥ 7.2 | listpack | 작은 Set에 대한 공간 효율적 인코딩 |
주요 연산 및 시간 복잡도
| 연산 | 시간 복잡도 | 설명 |
| SADD | O(1) per element | 요소 추가 |
| SREM | O(1) per element | 요소 제거 |
| SISMEMBER | O(1) | 멤버십 확인 |
| SCARD | O(1) | 요소 개수 |
| SMEMBERS | O(N) | 전체 요소 조회 |
| SRANDMEMBER | O(N) | N개 랜덤 요소 (count 지정 시) |
| SPOP | O(N) | N개 랜덤 제거 (count 지정 시) |
| SINTER | O(N*M) | 교집합 (N: 가장 작은 Set 크기, M: Set 개수) |
| SUNION | O(N) | 합집합 (N: 모든 Set 요소 합) |
| SDIFF | O(N) | 차집합 (N: 모든 Set 요소 합) |
| SSCAN | 각 호출마다 O(1), 완전한 반복에는 O(N) | 멤버들을 반복하여 조회 |
- 대부분의 집합 연산(추가, 제거, 집합 원소 여부 확인 등)은 O(1) 시간 복잡도를 가지며, 매우 효율적
- 그러나 수십만 개 이상의 원소를 가진 대규모 집합의 경우 SMEMBERS 명령어 실행 시 주의가 필요하다.
- O(n) 시간 복잡도를 가지며 단일 응답으로 전체 집합을 반환
- 대신 SSCAN 사용 혹은 SCAN + SSCAN 조합을 활용하는 것이 좋다.
활용 정리
중복없는 집합 표현과 집합 연산 기능을 활용해 간단한 필터링과 샘플링 가능하다.
- 중복 제거: 자동으로 동일 값 제거
- 태그 필터링: 교집합으로 조건 필터 구현
- 랜덤 추출: SRANDMEMBER 활용
- 키사이즈 주의 필요
Hash 타입
한 키안에 여러개의 필드-값 쌍을 저장하는 자료구조다. 일반적인 해시맵과 유사하지만 레디스에서는 복수 필드를 하나의 키 아래에 묶는 용도로 사용한다.
내부 구조
| 모든 버전 | hashtable | 일반적인 경우 |
| Redis ≤ 6.2 | ziplist | 공간 효율성이 좋으며 작은 해시에 적용 |
| Redis ≥ 7.0 | listpack | 공간 효율성이 좋으며 작은 해시에 적용 |
주요 연산 및 시간 복잡도
| 연산 | 시간 복잡도 | 설명 |
| HSET | O(1) | 필드 설정 |
| HGET | O(1) | 필드 조회 |
| HMSET / HMGET | O(N) | 다중 필드 설정/조회 (N: 필드 수) |
| HDEL | O(N) | 필드 삭제 (N: 삭제할 필드 수) |
| HEXISTS | O(1) | 필드 존재 확인 |
| HLEN | O(1) | 필드 개수 |
| HGETALL | O(N) | 전체 필드-값 조회 |
| HKEYS / HVALS | O(N) | 전체 키/값 조회 |
| HINCRBY | O(1) | 정수 필드 증가 |
- 대부분의 해시 명령어는 O(1)
- HKEYS, HVALS, HGETALL 및 대부분의 expiration-related 명령어와 같은 일부 명령어는 O(n)이며 주의가 필요하다.
- 마찬가지로 HGETALL 대신 HSACAN 권장
활용 정리
하나의 키에 여러 필드를 저장할 수 있어 객체 저장이나 설정 캐시에 적합
- 유저/세션 프로필 저장: HSET, HGET
- 소형 객체 최적화: ziplist or listpack 인코딩으로 메모리 절약
- 필드 수 주의: 필드 수가 많아지면 hashtable로 전환되며 메모리 증가
- 대량 순회: HSACAN 활용해 점진적 순회 가능
참고 출처
- https://redis.io/docs/latest/develop/data-types/lists/
- https://redis.io/docs/latest/develop/data-types/sets/
- https://redis.io/docs/latest/develop/data-types/hashes/
- https://redis.io/docs/latest/commands/object-encoding/



