리액티브 레디스 — 자료구조 5종 (String·Hash·List·Set·ZSet)

2026-05-03확률과 통계 마스터 노트

리액티브 레디스 마스터 노트 시리즈 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편)

시리즈 다른 편

공식 문서: Redis Data Types 에서 더 깊이.

다음 글(4편)에서는 WebFlux 캐싱 — Spring Cache 추상화·@Cacheable·CacheManager·TTL 정책까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!