Post

How can be Redis be used

How can be Redis be used

String

String은 단순한 key-value 저장과 조회(O(1))에 최적화되어 있고, SETNX(존재하지 않을 때만 설정)와 EX(만료 시간) 옵션을 함께 줄 수 있어 원자적인 생성과 자동 만료를 동시에 처리할 수 있다.

명령어시간복잡도비고
SET / GETO(1)값 크기와 무관하게 단일 키 접근
SET ... NX EXO(1)조건부 쓰기 + TTL 설정도 여전히 O(1)
DEL (단일 키)O(1)여러 키를 한 번에 지우면 O(N), N=키 개수
  • Session: 세션 ID를 키로, 세션 데이터(직렬화된 JSON 등)를 값으로 저장한다. SET session:{id} {data} EX {ttl}처럼 만료 시간을 함께 지정하면 별도의 정리 작업 없이도 세션이 자동으로 사라지게 만들 수 있다.
  • Cache: DB 조회나 API 응답 결과를 String으로 캐싱한다. GET/SET이 단순하고 빠르기 때문에 캐시 조회 경로의 지연을 최소화할 수 있고, TTL로 캐시 무효화를 자동화할 수 있다.
  • Distributed Lock: SET lock:{resource} {owner} NX EX {ttl}로 락 획득을 시도한다. “값이 없을 때만 설정”이 원자적으로 보장되기 때문에, 여러 서버가 동시에 요청해도 오직 하나만 락을 획득하게 되어 분산 환경에서의 상호 배제(mutual exclusion)를 구현할 수 있다.
    • 락이라는 개념 자체는 “하나의 키에 값이 있는지 없는지, 있다면 누가 소유했는지”만 표현하면 되는 단일 값이라 여러 필드나 정렬 같은 구조가 필요 없다. 필요한 건 원자적인 check-and-set 하나뿐이며, 이는 String의 SET ... NX EX로 정확히 표현된다.
    • NX는 “키가 없을 때만 설정”을 단일 명령으로 원자적으로 수행한다. Redis가 싱글 스레드로 명령을 순차 처리하기 때문에 여러 클라이언트가 동시에 요청해도 정확히 하나만 성공하며, 이것이 상호 배제의 핵심이다.
    • EX(TTL)는 락을 획득한 프로세스가 죽어도 시간이 지나면 키가 자동으로 사라지게 해 영구 데드락을 방지한다.
    • 이 두 옵션을 하나의 원자적 명령으로 묶을 수 있는 건 대상이 String이기 때문이다. Hash나 List처럼 여러 필드/요소로 락을 표현하려 하면 “존재 확인 + 값 설정”을 원자적으로 처리하기 위해 Lua 스크립트나 트랜잭션이 추가로 필요해져 불필요하게 복잡해진다.
    • 값(owner)에는 락을 요청한 클라이언트의 고유 식별자를 넣어서, 해제 시 GET으로 값을 확인 후 자기 것일 때만 DEL(Lua로 GET+DEL을 원자화)하도록 만들면 다른 프로세스의 락을 실수로 해제하는 것을 막을 수 있다.
    • 즉, String이 제공하는 “단일 값에 대한 원자적 조건부 쓰기 + TTL”이라는 최소 기능 조합이 분산 락에 필요한 것과 정확히 일치한다.

Int

Redis의 String은 정수 값에 대해 INCR/DECR 같은 원자적 증감 연산을 지원한다. 이 연산은 read-modify-write를 하나의 명령으로 처리하므로 동시에 여러 클라이언트가 접근해도 race condition 없이 정확한 카운팅이 가능하다.

명령어시간복잡도비고
INCR / DECR / INCRBYO(1)값을 읽고 더하고 쓰는 과정이 하나의 원자적 연산
EXPIREO(1)카운터/윈도우에 TTL 설정
  • Counter: 게시글 조회수, 좋아요 수처럼 계속 증가만 하는 값을 INCR post:{id}:views로 관리한다. 애플리케이션 레벨에서 락을 걸지 않아도 Redis가 원자성을 보장해준다.
  • Rate Limiter: 사용자별 요청 횟수를 INCR rate:{user_id}로 세고, 최초 요청 시 EXPIRE로 시간 윈도우(예: 60초)를 설정한다. 카운트가 임계값을 넘으면 요청을 거부하는 방식으로 fixed window rate limiting을 간단히 구현할 수 있다.
  • Global ID: INCR global:id:seq로 매번 1씩 증가하는 유일한 ID를 발급한다. 여러 서버가 동시에 호출해도 원자적 연산이라 ID 중복이나 유실이 발생하지 않아 분산 환경에서의 ID 생성기로 활용할 수 있다.

Hash

Hash는 하나의 키 아래 여러 필드-값 쌍을 저장할 수 있어, 객체의 여러 속성을 하나의 엔티티로 묶어 관리하기에 적합하다. 전체 데이터를 매번 읽고 쓰는 대신 특정 필드만 조회(HGET)하거나 갱신(HSET)할 수 있어 네트워크/직렬화 비용을 줄일 수 있다.

명령어시간복잡도비고
HSET / HGET / HDEL / HEXISTSO(1)필드 단위 접근 (평균)
HINCRBYO(1)특정 필드 값에 대한 원자적 증감
HLENO(1)필드 개수만 셀 때는 저장된 카운트를 바로 반환
HGETALL / HKEYS / HVALSO(N)N = 해당 Hash의 전체 필드 수
  • Shopping Cart: cart:{user_id}를 키로, 필드는 상품 ID, 값은 수량으로 저장한다(HSET cart:{user_id} {product_id} {qty}). 상품을 하나 추가하거나 수량을 바꿀 때 장바구니 전체를 다시 쓰지 않고 해당 필드만 갱신할 수 있고, HGETALL로 장바구니 전체를 한 번에 조회할 수도 있다.
    • cart:{user_id}는 Redis 키스페이스 전체에서 단 하나의 키다. 이 키가 가리키는 값이 “여러 필드-값 쌍을 담은 작은 해시테이블” 자체이며, String처럼 키 하나에 값 하나가 1:1로 매칭되는 게 아니라 키 하나가 작은 객체 전체를 담는 구조다.
      1
      2
      3
      4
      
      cart:user123
      ├── product:1001 → 2   (수량 2개)
      ├── product:2002 → 1   (수량 1개)
      └── product:3003 → 5   (수량 5개)
      
    • 상품을 담거나 수량을 바꿀 때마다 HSET cart:user123 product:1001 2처럼 필드 하나만 지정해서 쓰면 된다. 장바구니 전체를 JSON으로 직렬화해 String에 저장했다면 상품 하나 수량만 바꿔도 전체를 파싱→수정→직렬화→덮어쓰기 해야 하지만, Hash는 HINCRBY cart:user123 product:1001 1처럼 필드 하나만 원자적으로 증감시킬 수 있다.
    • HGET cart:user123 product:1001로 특정 상품 수량만 조회하거나, HDEL cart:user123 product:2002로 특정 상품만 제거하거나, HLEN cart:user123으로 담긴 상품 종류 수를 확인하는 등 필드 단위 접근이 가능하다.
    • 필드 수가 적고 값이 작을 때(기본 128개 필드, 64바이트 이하) Redis는 Hash를 listpack이라는 압축된 연속 메모리 구조로 저장해 메모리 효율이 좋고, 필드 수가 많아지면 자동으로 해시테이블 구조로 전환되어 O(1) 접근을 유지한다. 장바구니처럼 상품 종류가 보통 몇~수십 개 수준인 경우 대부분 listpack으로 저장되어 메모리도 절약되고 빠르다.

Bitmap

Bitmap은 String 위에서 비트 단위로 값을 다루는 방식으로, 사용자 ID 같은 정수를 비트의 offset으로 매핑하면 극히 적은 메모리로 대규모 사용자 집합의 on/off 상태를 표현할 수 있다. BITCOUNT, BITOP 같은 비트 연산으로 집계·교집합 계산도 빠르게 처리된다.

명령어시간복잡도비고
SETBIT / GETBITO(1)특정 offset 하나에 대한 읽기/쓰기
BITCOUNTO(N)N = 비트맵 전체(또는 지정 범위) 바이트 수를 스캔
BITOP (AND/OR/XOR)O(N)N = 연산에 참여하는 문자열 중 가장 긴 것의 길이
  • User Retention: 하루 단위 키(login:2026-08-30)에 접속한 사용자의 ID를 offset으로 SETBIT을 1로 찍는다. 특정 날짜의 접속자 수는 BITCOUNT로, “어제도 오늘도 접속한 사용자”처럼 여러 날의 연속 접속(리텐션)은 BITOP AND로 날짜별 비트맵을 겹쳐 계산할 수 있다.
    • 2026-08-30에 1, 3, 4, 7, 8번 유저가 로그인했다면:
      1
      2
      3
      4
      5
      
      SETBIT login:2026-08-30 1 1
      SETBIT login:2026-08-30 3 1
      SETBIT login:2026-08-30 4 1
      SETBIT login:2026-08-30 7 1
      SETBIT login:2026-08-30 8 1
      

      login:2026-08-30 키 하나에 비트가 이렇게 채워진다:

      1
      2
      3
      
      offset:  0 1 2 3 4 5 6 7 8
      value:   0 1 0 1 1 0 0 1 1
               └──────byte0──────┘└byte1
      

      offset은 곧 유저 ID이고, 그 위치의 비트 값(0/1)이 그날의 로그인 여부다. byte0(offset 0~7) = 01011001(0x59), byte1(offset 8) = 10000000처럼 사람이 읽는 값이 아니라 압축된 이진 문자열이 저장된다.

    • 조회는 GETBIT login:2026-08-30 41(4번 유저는 로그인함), BITCOUNT login:2026-08-305(그날 로그인한 유저 총 수)처럼 할 수 있다.
    • 리텐션은 두 날짜의 비트맵을 겹쳐서 계산한다: BITOP AND retention:0829-0830 login:2026-08-29 login:2026-08-30BITCOUNT retention:0829-0830을 하면 두 날 모두 로그인한 유저 수(연속 접속 리텐션)가 바로 나온다.
    • 메모리 관점에서, 유저 ID가 최대 100만이라면 비트맵은 100만 비트 = 125,000바이트(≈122KB)면 충분하다. Set에 유저 ID를 문자열로 하나씩 저장했다면 멤버당 수십 바이트씩 들어 수 MB가 필요했을 것이므로, 이 메모리 절약이 Bitmap이 대규모 사용자 리텐션 추적에 적합한 이유다.

List

List는 양쪽 끝(head/tail)에서의 삽입·삭제가 O(1)이고 입력 순서를 유지하기 때문에 큐(Queue)나 스택(Stack) 구조를 구현하기에 적합하다. 또한 BRPOP/BLPOP 같은 블로킹 연산을 지원해 컨슈머가 새 메시지를 폴링 없이 대기할 수 있다.

명령어시간복잡도비고
LPUSH / RPUSHO(1)원소 1개 기준. M개를 한 번에 넣으면 O(M)
LPOP / RPOPO(1)원소 1개 기준. count 지정 시 O(N)
BRPOP / BLPOPO(1)블로킹 대기 시간은 복잡도에 포함되지 않음
LRANGEO(S+N)S = 시작 offset, N = 반환할 범위 길이
LLENO(1)리스트 길이는 별도로 유지되어 즉시 반환
  • Message Queue: 프로듀서는 LPUSH queue:task {payload}로 작업을 넣고, 컨슈머는 BRPOP queue:task 0으로 순서대로 꺼내 처리한다. FIFO 순서가 보장되면서도 별도의 폴링 없이 새 작업이 들어오는 즉시 소비할 수 있어 가벼운 작업 큐로 활용할 수 있다.

ZSet

ZSet(Sorted Set)은 각 멤버에 score를 부여해 내부적으로 skip list와 hash table을 함께 사용하므로, 삽입·갱신·순위 조회가 모두 O(log N)이면서 항상 score 순으로 정렬된 상태를 유지한다. 이 특성 덕분에 실시간으로 값이 바뀌는 순위 데이터를 다루기에 적합하다.

명령어시간복잡도비고
ZADD / ZINCRBY / ZREMO(log N)N = ZSet에 담긴 전체 멤버 수, skip list 재조정 비용
ZSCOREO(1)멤버→score는 별도 hash table로 관리되어 즉시 조회
ZRANK / ZREVRANKO(log N)skip list를 타고 내려가며 순위 계산
ZRANGE / ZREVRANGEO(log N + M)M = 실제로 반환하는 원소 수
  • Rank/Leaderboard: ZADD leaderboard {score} {user_id}로 유저의 점수를 갱신하고, ZREVRANGE leaderboard 0 9로 상위 10명을 조회하거나 ZRANK/ZREVRANK로 특정 유저의 현재 순위를 즉시 알 수 있다. 점수가 실시간으로 바뀌는 게임 랭킹, 실시간 인기글 순위 등에 활용된다.
    • 게임 랭킹에 user_a, user_b, user_c, user_d가 각각 850, 1200, 1200, 400점을 얻었다면:
      1
      2
      3
      4
      
      ZADD leaderboard 850 user_a
      ZADD leaderboard 1200 user_b
      ZADD leaderboard 1200 user_c
      ZADD leaderboard 400 user_d
      

      leaderboard 키 하나에 (score, member) 쌍들이 score 순으로 정렬된 채로 저장된다:

      1
      2
      3
      4
      5
      6
      
      score  member
      ─────  ──────
        400  user_d
        850  user_a
       1200  user_b   (동점이면 member 이름 사전순으로 정렬)
       1200  user_c
      

      List처럼 입력 순서를 유지하는 게 아니라, 삽입/갱신 즉시 score 기준으로 재정렬된 상태가 항상 유지된다는 점이 핵심이다.

    • 순위 조회는 ZREVRANGE leaderboard 0 1 WITHSCORESuser_b 1200, user_c 1200(점수 높은 순 상위 2명), ZREVRANK leaderboard user_a2(0부터 세어 3등)처럼 할 수 있다.
    • 점수를 갱신할 때는 ZINCRBY leaderboard 100 user_a처럼 기존 점수에 더하기만 하면 되고, Redis가 내부적으로 skip list 상의 위치를 O(log N)에 재조정해 정렬 상태를 계속 유지해준다. 만약 List나 Set으로 이걸 구현했다면 점수가 바뀔 때마다 전체를 다시 정렬해야 해 O(N log N)이 걸렸을 것이다.
This post is licensed under CC BY 4.0 by the author.

Trending Tags