리액티브 레디스 마스터 노트 시리즈 3편. 5대 자료구조 String·Hash·List·Set·ZSet의 핵심 명령과 ReactiveRedisTemplate 사용 패턴, 각 자료구조의 시간 복잡도, 실무 사용처(String=캐시·카운터, Hash=객체, List=큐·스택, Set=중복 제거·태그, ZSet=실시간 랭킹), Pipeline으로 명령 묶기까지.
이 글은 리액티브 레디스 마스터 노트 시리즈의 세 번째 편입니다. 2편(Template)에서 5 Operations를 봤다면, 이번엔 각 자료구조의 핵심 명령 — 5대 자료구조 깊이 풀기.
ZSet 하나로 실시간 랭킹을 구현하면 Redis의 진가를 봅니다. 단순 캐시가 아닌 강력한 데이터 구조 서버. 각 자료구조가 어디에 쓰이는지가 핵심.
처음 자료구조 5종이 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, 명령어가 너무 많습니다. 각 자료구조마다 10개 넘는 명령. 외워야 하나? 둘째, 어디에 어떤 자료구조를 쓸지 막연합니다.
해결법은 한 가지예요. "각 자료구조 → 한 줄 사용처" 매핑. String=캐시·카운터, Hash=객체, List=큐, Set=태그, ZSet=랭킹. 이 한 줄만 잡으면 명령은 자연스럽게 따라옵니다.
자료구조 한 줄 정리
| 자료구조 | 한 줄 사용처 | 시간 복잡도 |
|---|---|---|
| String | 단순 키-값, 카운터 | O(1) |
| Hash | 객체 (필드-값 쌍) | O(1) |
| List | 큐·스택·타임라인 | 양 끝 O(1) / 중간 O(N) |
| Set | 중복 없는 태그·집합 | O(1) |
| Sorted Set (ZSet) | 점수 기반 랭킹 | O(log N) |
1. String — 가장 단순
명령
SET key value [EX seconds] [NX|XX] # 저장
GET key # 조회
DEL key # 삭제
INCR key # +1
DECR key # -1
INCRBY key 10 # +10
APPEND key "더" # 기존 값에 추가
STRLEN key # 길이
MSET k1 v1 k2 v2 # 여러 SET
MGET k1 k2 # 여러 GET
Reactive 사용
ReactiveValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("user:1", "Alice").subscribe();
ops.get("user:1").subscribe(System.out::println);
// 카운터 (원자적)
ops.increment("page:home:visits").subscribe();
사용처
- 단순 캐시 — DB 조회 결과 저장
- 카운터 — 방문수·좋아요·뷰
- 세션 — 로그인 토큰
- Rate Limit — IP/사용자별 요청 횟수
- 분산 락 (간단) —
SET NX EX
여기서 시험 함정이 하나 있어요. INCR은 원자적. 동시성 환경에서 race 없이 안전. DB의 UPDATE counter = counter + 1보다 빠름.
2. Hash — 객체 표현
명령
HSET key field value # 필드 저장
HGET key field # 필드 조회
HMSET key f1 v1 f2 v2 # 여러 필드 저장 (deprecated → HSET)
HMGET key f1 f2 # 여러 필드 조회
HGETALL key # 모든 필드-값
HDEL key field # 필드 삭제
HEXISTS key field # 존재 확인
HKEYS key # 모든 필드
HVALS key # 모든 값
HLEN key # 필드 수
HINCRBY key field 5 # 필드 +5
Reactive 사용
ReactiveHashOperations<String, String, String> ops = redisTemplate.opsForHash();
// 사용자 정보 저장
ops.put("user:1", "name", "Alice").subscribe();
ops.put("user:1", "age", "30").subscribe();
// 한 번에 여러
ops.putAll("user:1", Map.of("name", "Alice", "age", "30")).subscribe();
// 한 번에 조회
ops.entries("user:1")
.collectList()
.subscribe(System.out::println);
사용처
- 객체 표현 — 사용자·상품 (필드별 부분 업데이트 가능)
- 세션 데이터 — 여러 필드 한 번에
- 카운터 그룹 — 사용자별 여러 카운터
여기서 정말 중요한 시험 함정 — String JSON vs Hash. 작은 객체(필드 < 100) = Hash가 메모리 효율 ↑ + 부분 업데이트 가능. 큰 객체·복잡 구조 = JSON String. 둘의 trade-off 인식.
3. List — 양방향 큐
명령
LPUSH key value # 왼쪽 추가
RPUSH key value # 오른쪽 추가
LPOP key # 왼쪽 제거+반환
RPOP key # 오른쪽 제거+반환
LRANGE key 0 -1 # 범위 조회 (-1 = 끝)
LLEN key # 길이
LINDEX key 0 # 인덱스 접근
LREM key 0 value # 값으로 제거
BLPOP key timeout # 블로킹 LPOP (없으면 timeout 초 대기)
BRPOP key timeout # 블로킹 RPOP
Reactive 사용
ReactiveListOperations<String, String> ops = redisTemplate.opsForList();
// 큐로 사용 (FIFO)
ops.rightPush("queue:tasks", "task-1").subscribe();
ops.leftPop("queue:tasks").subscribe(System.out::println);
// 스택으로 (LIFO)
ops.leftPush("stack:items", "item-1").subscribe();
ops.leftPop("stack:items").subscribe();
// 타임라인 (최근 100개만)
ops.leftPush("timeline:user-1", postId)
.then(ops.trim("timeline:user-1", 0, 99))
.subscribe();
사용처
- 메시지 큐 — Worker가 처리할 작업
- 타임라인 — SNS 게시물 시간순
- 로그 스트림 — 최근 N개만 보존
- Undo 스택 — 작업 되돌리기
여기서 시험 함정이 하나 있어요. List 길이 제한 안 두면 무한 증가. LTRIM으로 일정 길이 유지. 또는 ZSet 사용 (시간 점수).
4. Set — 중복 없는 집합
명령
SADD key value # 추가
SREM key value # 제거
SMEMBERS key # 모든 원소
SISMEMBER key value # 포함?
SCARD key # 원소 수
SUNION k1 k2 # 합집합
SINTER k1 k2 # 교집합
SDIFF k1 k2 # 차집합
SRANDMEMBER key # 랜덤 1개
SPOP key # 랜덤 1개 + 제거
Reactive 사용
ReactiveSetOperations<String, String> ops = redisTemplate.opsForSet();
// 태그
ops.add("post:1:tags", "redis", "spring", "java").subscribe();
// 친구 목록
ops.add("user:alice:friends", "bob", "charlie").subscribe();
ops.add("user:bob:friends", "alice", "dave").subscribe();
// 공통 친구 (교집합)
ops.intersect("user:alice:friends", "user:bob:friends").subscribe();
사용처
- 태그·카테고리 — 중복 없는 분류
- 친구·팔로워 — 관계 표현
- 방문 사용자 — 일별 unique users
- 추천 — 교집합·차집합으로 추천 알고리즘
- A/B 테스트 그룹
여기서 정말 중요한 시험 함정 — Set 연산은 강력하지만 비용 큼. SUNION·SINTER 큰 Set끼리는 O(N+M). 자주 호출되면 결과 별도 Set에 캐시.
5. Sorted Set (ZSet) — 점수 기반 정렬
명령
ZADD key score member # 점수와 함께 추가
ZSCORE key member # 점수 조회
ZRANGE key 0 -1 # 점수 오름차순 범위
ZRANGE key 0 -1 WITHSCORES # 점수 포함
ZREVRANGE key 0 -1 # 점수 내림차순
ZRANGEBYSCORE key min max # 점수 범위
ZRANK key member # 순위 (오름차순)
ZREVRANK key member # 순위 (내림차순)
ZINCRBY key 10 member # 점수 +10
ZREM key member # 제거
ZCARD key # 원소 수
Reactive 사용
ReactiveZSetOperations<String, String> ops = redisTemplate.opsForZSet();
// 실시간 랭킹 (점수 = 좋아요 수)
ops.add("rank:posts", "post-1", 42).subscribe();
ops.add("rank:posts", "post-2", 17).subscribe();
ops.incrementScore("rank:posts", "post-1", 1).subscribe();
// 상위 10위
ops.reverseRangeWithScores("rank:posts", Range.closed(0L, 9L))
.subscribe(t -> System.out.println(t.getValue() + " " + t.getScore()));
// post-1 순위
ops.reverseRank("rank:posts", "post-1").subscribe();
사용처
- 실시간 랭킹 — 게임 점수·트렌딩 게시물
- 타임라인 (정확한 시간순) — 점수 = timestamp
- 우선순위 큐 — 점수 = 우선순위
- Rate Limit (sliding window) — 시간 점수 + 카운트
- 세션 만료 관리 — 점수 = 만료 시간
여기서 정말 중요한 시험 함정 — ZSet은 Redis의 결정적 차별화. 다른 캐시(Memcached)에 없음. 실시간 랭킹·타임라인이 필요한 모든 시스템에 ZSet.
Sorted Set 활용 — 슬라이딩 윈도우 Rate Limit
public Mono<Boolean> allowRequest(String userId, int limit, Duration window) {
long now = System.currentTimeMillis();
long windowStart = now - window.toMillis();
String key = "rate:" + userId;
return redisTemplate.opsForZSet()
// 1. 윈도우 밖 옛 요청 제거
.removeRangeByScore(key, Range.leftUnbounded(Range.Bound.exclusive((double) windowStart)))
// 2. 현재 시각 추가
.then(redisTemplate.opsForZSet().add(key, UUID.randomUUID().toString(), now))
// 3. 윈도우 안 요청 수
.then(redisTemplate.opsForZSet().count(key, Range.closed((double) windowStart, (double) now)))
// 4. limit 이하면 허용
.map(count -> count <= limit)
// 5. TTL 갱신
.doOnNext(allowed -> redisTemplate.expire(key, window).subscribe());
}
ZSet 한 자료구조로 정밀한 Rate Limit.
Pipeline — 여러 명령 묶기
redisTemplate.execute(connection -> {
return Flux.merge(
connection.stringCommands().set(...),
connection.stringCommands().set(...),
connection.stringCommands().set(...)
);
});
여러 명령을 한 번 네트워크 왕복으로. 처리량 ↑.
여기서 시험 함정이 하나 있어요. Pipeline ≠ Transaction. Pipeline은 단순 묶음 (각 명령 독립), Transaction은 원자성 (MULTI/EXEC). 7편에서 자세히.
자료구조 선택 표
| 패턴 | 선택 |
|---|---|
| 단일 키-값 | String |
| 카운터 | String + INCR |
| 사용자 객체 (작음) | Hash |
| 사용자 객체 (큰 JSON) | String |
| FIFO 큐 | List + LPUSH·RPOP |
| 최근 N개 | List + LTRIM |
| 태그·카테고리 | Set |
| 공통 관심사 | Set + SINTER |
| 실시간 랭킹 | ZSet |
| 타임라인 | List 또는 ZSet (시간 점수) |
| 우선순위 큐 | ZSet |
| Rate Limit (sliding) | ZSet |
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 3편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- 5 자료구조 — String / Hash / List / Set / ZSet
- 한 줄 사용처 — String=캐시·카운터, Hash=객체, List=큐, Set=태그, ZSet=랭킹
- String — SET·GET·DEL·INCR(원자)·MSET·MGET
- INCR = 원자적, 동시성 안전
- Hash — HSET·HGET·HGETALL·HMGET·HINCRBY
- 작은 객체 = Hash (메모리 효율·부분 업데이트)
- 큰 객체 = JSON String
- List — LPUSH·RPUSH·LPOP·RPOP·LRANGE·LTRIM·BLPOP
- 양 끝 O(1)·중간 O(N)
- 큐(LPUSH+RPOP) / 스택(LPUSH+LPOP)
- BLPOP·BRPOP = 블로킹 (대기열용)
- List 무한 증가 방지 = LTRIM
- Set — SADD·SREM·SMEMBERS·SUNION·SINTER·SDIFF
- 중복 없음, 집합 연산 강력
- 태그·친구·추천
- SUNION/SINTER 큰 Set은 비용 큼 → 결과 캐시
- Sorted Set (ZSet) = Redis의 결정적 차별화
- ZADD·ZSCORE·ZRANGE·ZREVRANGE·ZRANK·ZINCRBY
- 시간 복잡도 O(log N)
- 실시간 랭킹·정확한 타임라인·우선순위 큐·Rate Limit
- Sliding Window Rate Limit = ZSet 표준 패턴
- Pipeline ≠ Transaction
- Pipeline = 단순 묶음·처리량 ↑
- Transaction = 원자성 (MULTI/EXEC, 7편)
시리즈 다른 편
- 1편 — Redis 기본·Spring Boot 연동
- 2편 — Template·Redisson·Serializer
- 3편 — 자료구조 5종 (현재 글)
- 4편 — WebFlux 캐싱
- 5편 — 성능
- 6편 — Pub/Sub·WebSocket
- 7편 — 고급 (Transaction·Persistence·GeoSpatial·ACL)
공식 문서: Redis Data Types 에서 더 깊이.
다음 글(4편)에서는 WebFlux 캐싱 — Spring Cache 추상화·@Cacheable·CacheManager·TTL 정책까지 풀어 갑니다.