백엔드 데이터 인프라 49편 — Redis String 깊이 + 분산 락 패턴

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 49편. Redis String — 가장 기본 타입을 SET/GET/INCR부터 EX·NX·XX·MSET·APPEND·BITCOUNT까지, 그리고 SET NX EX 한 줄로 끝나는 분산 락 패턴까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 49편 — Redis String 깊이 + 분산 락 패턴

이 글은 백엔드 데이터 인프라 시리즈 130편 중 49편이에요. 48편 에서 Redis 데이터 타입 13종을 한 번에 매핑했다면, 49편부터 54편까지는 자주 쓰는 6종(String·Hash·List·Set·Sorted Set·Stream)을 하나씩 깊이 풀어 갑니다. 첫 타자는 가장 기본인 Redis String.

Redis String이 어렵게 느껴지는 이유

"문자열인데 뭐가 어렵나" 싶지만, 처음 만지면 세 가지에서 멈춰요.

첫째, String이라는 이름과 달리 숫자도 들어갑니다. INCR · INCRBY 같은 명령어가 String 그룹에 묶여 있어서 "이게 String 명령인가?" 가 헷갈리죠. 정답은 "Redis는 모든 값을 일단 String으로 저장하고, 숫자처럼 보이는 String 에만 산술 명령을 추가로 허용". 이름은 String 이지만 문자열·숫자·바이너리 가 다 들어가는 자루라고 생각하면 편해요.

둘째, SET 명령에 옵션이 너무 많아요. EX·PX·NX·XX·KEEPTTL·EXAT·PXAT·GET... 풀 시그니처가 한 줄로 길어 보이는데, 각각이 "언제 쓰는가" 가 안 보이면 옵션 이름만 외워서는 도무지 손이 안 나가요.

셋째, String 위에 비트 연산이 얹혀 있어요. SETBIT·GETBIT·BITCOUNT·BITOP. 문자열인데 왜 비트? 라는 의문이 드는데, 답은 "String은 사실 바이트의 시퀀스라 비트 단위 조작이 자연스럽다". 1억 명 사용자 출석 체크를 12.5MB 메모리로 끝내는 비결이 여기서 나옵니다.

이 글에서는 위 세 가지를 정리하면서, 마지막에 분산 락(distributed lock — 여러 서버가 같은 자원에 동시에 손대지 못하게 막는 도구) 한 줄 패턴 까지 풀어요. 분산 락은 백엔드 실무에서 "Redis 도입하길 잘했다" 를 가장 자주 느끼게 해 주는 자리예요.

SET · GET — 가장 기본 두 명령어

Redis String의 출발은 두 명령어. 각각 O(1) 시간 복잡도 — 키가 100만 개든 1억 개든 같은 속도.

> SET user:42:name "Alice"
OK
> GET user:42:name
"Alice"

SET 의 핵심 동작:

  • 키가 없으면 → 새로 만들고 값 저장
  • 키가 있으면 → 기존 값 덮어쓰기 (타입이 Hash·List라도 무시하고 String으로 덮어씀, 주의)
  • 반환값 = OK 또는 nil (조건부 옵션 실패 시)

GET 의 핵심 동작:

  • 키가 있으면 → 문자열 값 반환
  • 키가 없으면 → nil (null) 반환
  • "Hash·List 같은 다른 타입에 GET을 쓰면?" → 에러 (WRONGTYPE Operation against a key holding the wrong kind of value)

여기서 시험 함정이 하나 있어요 — SET은 타입을 무시하고 덮어씁니다. user:42 키에 Hash가 있는데 SET user:42 "hello" 를 쏘면 Hash가 통째로 날아가요. 반대로 HSET user:42 name Alice 같이 다른 타입 명령은 키 타입을 체크해서 에러 — SET 만 특별히 거침이 없습니다.

SET 옵션 — 5가지만 알면 90%

SET key value [EX seconds | PX milliseconds | KEEPTTL] [NX | XX] [GET] — 풀 시그니처가 길지만 실무에서 자주 쓰는 옵션은 5개로 압축돼요.

(1) EX seconds — TTL을 초 단위로

TTLTime To Live, 키가 살아 있는 시간이에요. 만료되면 Redis가 알아서 삭제합니다.

> SET session:abc123 "{\"uid\":42}" EX 3600
OK

1시간 후 자동 삭제. 세션·캐시·OTP(One-Time Password, 일회용 인증번호) 같은 수명이 정해진 데이터 의 표준 패턴이에요.

(2) PX milliseconds — TTL을 밀리초 단위로

> SET rate:user42 1 PX 1000
OK

1초 후 자동 삭제. Rate Limiting(요청 횟수 제한)처럼 정밀한 만료가 필요한 자리.

(3) NX — 키가 없을 때만 SET

> SET lock:order:99 "worker-3" NX
OK         # 처음 → 락 획득 성공
> SET lock:order:99 "worker-7" NX
(nil)      # 이미 있음 → 락 획득 실패

"분산 락" 의 핵심. 여러 서버가 동시 요청을 보내도 한 명만 락을 잡고 나머지는 실패. 뒤에서 깊게 풀어요.

(4) XX — 키가 있을 때만 SET

> SET cache:article:99 "..." XX

"기존 값이 있을 때만 갱신, 없으면 만들지 마" 패턴. cache warming(서비스 시작 전에 캐시를 미리 채우는 작업) 한 곳에서만 만드는 정책에 쓰여요.

(5) KEEPTTL — 기존 TTL 유지

> SET cache:foo "new value" KEEPTTL

값만 갈고 남은 만료 시간은 유지. "내용은 갱신했지만 만료는 유지" 가 필요할 때.

한 줄 암기 — EX(초 TTL) · PX(밀리초 TTL) · NX(없을 때만) · XX(있을 때만) · KEEPTTL(만료 유지). 분산 락은 NX + EX 조합.

INCR · INCRBY · DECR — String이 카운터가 되는 자리

INCR 그룹이 String 명령에 들어가 있는 이유 — "숫자처럼 보이는 String 에 산술을 허용". 카운터·통계·재고 차감 같은 곳에서 매번 등장해요.

> SET pv:2026-05-17 0
OK
> INCR pv:2026-05-17
(integer) 1
> INCR pv:2026-05-17
(integer) 2
> INCRBY pv:2026-05-17 10
(integer) 12
> DECR pv:2026-05-17
(integer) 11
> DECRBY pv:2026-05-17 5
(integer) 6
  • INCR — +1
  • INCRBY n — +n
  • DECR — -1
  • DECRBY n — -n
  • INCRBYFLOAT n — 소수점 단위 증감

여기서 시험 함정 — "INCR이 race condition(여러 작업이 동시에 같은 데이터를 건드릴 때 결과가 어긋나는 현상) 없나요?". 정답: 단일 스레드라 자동으로 atomic(중간에 끼어들 수 없는 한 덩어리 동작). 100개 클라이언트가 동시에 INCR pv 쏴도 절대 카운트 누락 없음. RDB(관계형 DB)·MySQL에서 읽고 → +1 → 쓰는 3단계로 처리할 때 늘 race를 걱정해야 하는데, Redis는 그게 공짜. "왜 Redis가 카운터의 표준인가" 의 답이 여기 있어요.

INCR 은 키가 없으면 0에서 시작. SET pv 0 미리 안 박아도 첫 INCR 이 1을 반환합니다.

MSET · MGET — 여러 키 한 번에

서버 왕복(round trip) 횟수를 줄이는 묶음 명령. 100개 키를 한 번에 처리하면 100번 왕복이 1번으로 줄어요. 네트워크 지연(latency) 이 전체 응답 시간의 80% 를 차지하는 환경(클라우드·다른 리전)에서 효과가 큽니다.

> MSET bike:1 Deimos bike:2 Ares bike:3 Vanth
OK
> MGET bike:1 bike:2 bike:3
1) "Deimos"
2) "Ares"
3) "Vanth"

복잡도 = O(N) (N = 키 개수). 100개 묶어 보내도 한 번에 처리되는 atomic 한 묶음.

잠깐, 이 부분이 헷갈리는데 — "MGET 묶음을 트랜잭션처럼 쓸 수 있나요?". 답은 "MSET 자체는 atomic 하지만, MSET → MGET 두 명령을 묶어 보내려면 별도 트랜잭션(MULTI/EXEC) 또는 파이프라인이 필요". 묶음 명령 하나 안에서만 atomic 이지, 명령 사이 의 atomic은 아니에요.

APPEND · STRLEN · GETRANGE — 문자열 가공

기본 가공 3종.

> SET note:42 "Hello"
> APPEND note:42 ", World!"
(integer) 13
> GET note:42
"Hello, World!"
> STRLEN note:42
(integer) 13
> GETRANGE note:42 0 4
"Hello"
  • APPEND — 문자열 끝에 이어 붙임 (O(1) amortized — 평균적으로 O(1))
  • STRLEN — 바이트 길이 반환 (O(1))
  • GETRANGE start end — 부분 문자열 (Python slice 와 동일, end inclusive)
  • SETRANGE offset value — 특정 위치부터 덮어쓰기

자주 쓰는 자리:

  • 로그 누적 (APPEND 로 한 키에 계속 이어 붙임)
  • 짧은 문자열 길이 측정 (STRLEN)
  • 큰 문자열의 일부만 읽기 (GETRANGE, 메모리 절약)

분산 락 패턴 — SET key val NX EX

여기서 정말 중요한 자리 — Redis가 백엔드에서 가장 빛나는 활용처 중 하나, 분산 락. 여러 인스턴스가 동시에 같은 자원에 손을 대지 못하게 막는 도구예요.

문제 — 왜 락이 필요한가

쇼핑몰 주문 처리 시스템. 같은 사용자가 결제 버튼 을 빠르게 두 번 누르면 두 인스턴스가 같은 주문에 동시에 결제를 시도할 수 있어요. DB unique 제약으로 막을 수 있지만 "결제 게이트웨이 호출 → 응답 대기 → 주문 상태 업데이트" 같은 외부 호출 포함 트랜잭션 은 DB만으로 막기 어려워요.

한 줄 패턴

# 락 획득
> SET lock:order:99 "worker-3-uuid-abc" NX EX 30
OK         # 성공
# 또는
(nil)      # 이미 다른 인스턴스가 잡음 → 실패

핵심 옵션 두 가지:

  • NX — 키가 없을 때만 SET → 두 번째 요청은 무조건 실패
  • EX 30 — 30초 후 자동 해제 → 워커가 죽어도 락이 영원히 안 풀리는 데드락 방지

워커 식별자(worker-3-uuid-abc) 를 값에 박는 이유 — "내가 잡은 락만 내가 풀게" 라는 정확한 해제를 위해서. UUID(Universally Unique Identifier, 전 세계에서 고유한 식별자)로 "이 락은 내 것" 임을 증명.

락 해제

단순히 DEL 하면 위험 — 내가 잡은 락이 이미 만료돼서 다른 워커가 새 락 잡았는데 내가 DEL하면 남의 락이 풀려요. 그래서 락 해제는 Lua 스크립트로 atomic 하게:

if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
else
  return 0
end

GET 으로 내가 박은 UUID 인지 확인 하고, 맞으면 DEL. check-and-delete 가 한 명령으로 묶여야 하니 Lua가 필요해요. (60편 Lua scripting 에서 깊이)

Redlock — 더 안전한 분산 락 알고리즘

단일 Redis 인스턴스의 SET NX EX서버 다운 시 락 정보 유실 위험이 있어요. Redlock 은 여러 Redis 인스턴스에 동시에 락을 거는 알고리즘으로 더 강한 보장을 제공. 63편 distributed-lock 에서 풀어 가요.

여기까지 따라오셨다면 한 가지 의문이 들 거예요 — "DB의 SELECT FOR UPDATE 도 비슷한 락 아닌가요?". 맞지만 속도 차이가 1,000배. DB 락은 ms 단위, Redis 락은 μs 단위. 트래픽 많은 자리에서는 Redis 락 한 줄이 시스템을 살립니다.

한 줄 정리 — 분산 락 = SET lock:key uuid NX EX 30. 해제는 Lua 스크립트로 check-and-delete. 더 강한 보장은 Redlock(63편).

String 위의 비트 연산 — SETBIT·BITCOUNT

String은 사실 바이트 시퀀스 라 비트 단위 조작이 자연스럽습니다.

> SETBIT active:2026-05-17 42 1     # user 42가 오늘 접속
(integer) 0
> SETBIT active:2026-05-17 99 1
(integer) 0
> GETBIT active:2026-05-17 42       # user 42 접속했나?
(integer) 1
> BITCOUNT active:2026-05-17        # 오늘 접속자 수
(integer) 2

자주 쓰는 자리:

  • 출석 체크 / DAU(Daily Active Users) — 사용자 ID를 비트 위치로 매핑
  • boolean 플래그 집합 — 100가지 옵션 on/off 를 100비트 String 한 개로
  • A/B 테스트 그룹 추적 — 어느 사용자가 어느 그룹인지

메모리 효율 — 1억 명 사용자 × 365일 출석 데이터를 365 × 12.5MB = 약 4.5GB 로 끝낼 수 있어요. JSON Set 으로 표현하면 수십 GB 이상.

BITOP AND/OR/XOR비트맵 간 연산 도 가능 — "오늘 + 어제 둘 다 접속한 사용자" 같은 쿼리가 한 줄.

이 기능은 사실 Bitmap 데이터 타입이라는 별도 이름이 붙어 있지만, 내부 구조는 String 위의 비트 단위 명령. "Bitmap = String의 비트 뷰" 라고 외워도 됩니다.

시험 직전 한 번 더 — Redis String 함정 압축 노트

  • String = Redis 가장 기본 타입, 모든 값의 기본 자루
  • 한 String 최대 크기 = 512 MB
  • String 에 들어가는 것 = 문자열·숫자·바이너리 다 가능
  • SET 핵심 = O(1), 타입 무시하고 덮어쓰기 (다른 타입까지 날아감, 주의)
  • GET 으로 다른 타입 키 접근 = WRONGTYPE 에러
  • SET 옵션 5종 = EX(초 TTL)·PX(밀리초 TTL)·NX(없을 때만)·XX(있을 때만)·KEEPTTL(만료 유지)
  • INCR·INCRBY·DECR = 자동 atomic (단일 스레드 덕분)
  • INCR 첫 호출 = 키 없으면 0에서 시작 → 1 반환
  • INCRBYFLOAT = 소수점 증감 (재고 차감 등)
  • MSET·MGET = 묶음 명령, 네트워크 왕복 절약 (O(N))
  • 묶음 명령 하나 안에서만 atomic, 명령 사이 는 별도 트랜잭션 필요
  • APPEND = 문자열 끝에 이어 붙임 (로그 누적)
  • STRLEN = 바이트 길이 (O(1))
  • GETRANGE = Python slice, end inclusive
  • 분산 락 한 줄 = SET lock:key uuid NX EX 30
  • 락 해제 = Lua 스크립트로 check-and-delete (DEL 단독은 위험)
  • Redlock = 여러 Redis 인스턴스 분산 락 알고리즘 (63편)
  • DB SELECT FOR UPDATE vs Redis 락 = 속도 1,000배 차이
  • SETBIT·GETBIT·BITCOUNT = String 위의 비트 연산
  • 1억 명 출석 = 12.5MB 메모리
  • Bitmap = String 의 비트 뷰 (별도 데이터 타입 아님)
  • 모든 명령어 단일 스레드 = race condition 자동 차단

공식 문서: Redis Strings 에서 25개 String 명령어 전체 reference 를 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!