sayu.day

Redis 실전 패턴: 분산 락, Streams 파이프라인, 클러스터 Failover

Redis는 캐시로 시작해서 어느새 락 매니저, 이벤트 로그, 클러스터 스토리지 역할까지 떠맡게 되는 도구입니다. 문제는 세 역할 모두 "잘 될 때"가 아니라 "실패할 때" 어떻게 동작하는지가 설계의 전부라는 점입니다. 이 글은 분산 환경에서 Redis를 쓸 때 반복해서 마주치는 세 가지 주제, 즉 분산 락과 Streams 파이프라인, 클러스터 Failover를 실패 관점에서 패턴으로 정리합니다.

패턴 1: 분산 락 — 획득보다 해제가 어렵습니다

Redis 락은 간단하지만 오용하기도 쉽습니다. 핵심은 락 획득이 아니라 락 해제의 안전성입니다.

안전한 기본 패턴

락 획득은 원자적 명령 하나로 끝냅니다. value에는 요청마다 고유한 토큰(UUID)을 넣습니다.

SET lock:report-batch <uuid> NX PX 30000
  • NX: 키가 없을 때만 설정 (이미 잠겨 있으면 실패)
  • PX: TTL을 밀리초 단위로 지정 (프로세스가 죽어도 락이 풀리도록)

락 해제는 "내가 잡은 락이 맞는지" 확인한 뒤에만 지워야 합니다. GET과 DEL 사이에 락이 만료되고 다른 프로세스가 새로 획득할 수 있으므로, 반드시 Lua 스크립트로 원자적으로 처리합니다.

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

SET NX PX + 고유 토큰 + Lua 해제, 이 조합이 최소 안전선입니다.

꼭 지켜야 할 원칙

  1. TTL 없는 락은 금지합니다. 락 홀더가 죽으면 영원히 잠깁니다.
  2. 소유자 확인 없는 해제는 금지합니다. 남의 락을 지우는 사고로 이어집니다.
  3. 임계 구역 실행 시간이 TTL보다 길어질 수 있다면 TTL 갱신(연장) 전략이 필요합니다.
  4. 락 획득 실패 시에는 백오프를 적용해 재시도합니다.

Redlock에 대한 현실적 관점

여러 Redis 노드에 걸쳐 락을 잡는 Redlock은 고가용성 락을 향한 시도이지만, 분산 시스템의 모든 문제를 해결해 주지는 않습니다. 매우 강한 정합성이 필요한 금융·원장 핵심 경로라면 DB 트랜잭션이나 합의 기반 락을 우선 검토해야 합니다.

Redis 락이 잘 맞는 곳은 따로 있습니다.

  • 배치 작업의 중복 실행 방지
  • 완전한 엄격성이 필요하지 않은 스케줄러 리더 선출
  • 캐시 재생성(cache stampede) 중복 방지

정리하면, Redis 락은 "정확히 한 번"을 보장하는 도구가 아니라 실용적인 경합 제어 도구입니다. 락이 깨졌을 때의 보상 로직까지 포함해서 설계해야 안전합니다.

패턴 2: Streams 감사 파이프라인 — 유실 없이 재처리 가능하게

감사(audit) 이벤트 파이프라인의 목표는 처리량이 아니라 **"유실 없이 재처리 가능한 구조"**입니다. Redis Streams와 Consumer Group으로 이 구조를 만들 수 있습니다.

권장 구조

  1. Producer: 이벤트 발행 (XADD)
  2. Stream + Consumer Group: 병렬 처리
  3. Worker: 핸들러 실행 후 ACK
  4. Dead Letter Stream: 반복 실패 이벤트 격리

구현 핵심 네 가지

1) >0을 분리 처리합니다. Consumer Group에서 읽기 대상은 두 종류입니다.

XREADGROUP GROUP audit-group worker-1 COUNT 100 STREAMS audit >   # 신규 메시지
XREADGROUP GROUP audit-group worker-1 COUNT 100 STREAMS audit 0   # 내 pending 재처리

>는 신규 메시지, 0은 ACK되지 않은 pending 메시지입니다. 워커 재시작 후 0 처리를 빼먹으면 처리 중이던 메시지가 "유령 pending"으로 남아 조용히 유실됩니다.

2) ACK는 핸들러 성공 후에만 보냅니다. 읽자마자 XACK을 보내면 처리 중 장애 시 메시지가 사라집니다. 성공 후 ACK이어야 at-least-once가 유지되고, 대신 핸들러는 중복 처리를 견디도록(멱등하게) 만들어야 합니다.

3) 재시도는 delivery count로 제한합니다. XPENDING으로 확인할 수 있는 delivery count 기준으로 최대 재시도 횟수를 정하고, 초과한 메시지는 Dead Letter Stream으로 옮겨 격리합니다. 실패 메시지 하나가 파이프라인 전체를 막는 것을 방지합니다.

4) 메모리를 관리합니다. 스트림은 지우지 않으면 계속 누적됩니다. XTRIM 정책(최대 길이 또는 보존 기준)을 운영 기준으로 반드시 설정해야 합니다.

운영 체크리스트

  1. pending 길이, 재시도율, DLQ 유입량 모니터링
  2. consumer 멈춤 감지와 자동 복구
  3. 메시지 스키마 버전 관리
  4. 특정 구간을 다시 흘릴 수 있는 재처리 도구(범위 replay) 준비

Redis Streams는 빠른 큐가 아니라 상태 있는 로그입니다. pending과 재시도를 제대로 관리할 때만 신뢰할 수 있는 감사 파이프라인이 됩니다.

패턴 3: 클러스터 샤딩과 Failover — 승격은 자동, 손실 가능성은 감수

단일 인스턴스를 넘어서면 클러스터 모드(예: AWS ElastiCache for Redis OSS)의 동작 방식을 이해해야 합니다.

해시 슬롯 기반 샤딩

  • Redis Cluster는 모든 키를 0~16383, 총 16384개의 해시 슬롯 중 하나에 매핑합니다.
  • 각 해시 슬롯은 하나의 샤드(마스터 노드)가 담당합니다.
  • 샤드가 늘어나면 슬롯이 재분배되고, 각 샤드는 자신이 맡은 슬롯 범위의 키만 관리합니다.
  • 하나의 키는 하나의 슬롯, 곧 하나의 마스터에만 속하므로 같은 키를 여러 마스터가 동시에 다루는 일은 없습니다.

Failover 시나리오

각 마스터에는 하나 이상의 리플리카를 붙일 수 있고, 리플리카는 마스터의 데이터를 비동기 복제합니다. 마스터 장애 시 흐름은 다음과 같습니다.

  1. 마스터 노드에 장애 발생
  2. 감시 시스템(ElastiCache)이 자동 감지
  3. 해당 샤드의 리플리카 중 하나를 마스터로 자동 승격(promote)
  4. 클러스터 메타데이터 갱신
  5. 클라이언트는 MOVED 리다이렉션 응답을 통해 새 마스터로 연결
샤드 1:  마스터 A ──(비동기 복제)── 리플리카 A'
샤드 2:  마스터 B ──(비동기 복제)── 리플리카 B'   ← B 장애 시 B'가 마스터로 승격
샤드 3:  마스터 C ──(비동기 복제)── 리플리카 C'

주의사항

  • 복제가 비동기이므로, 장애 시점에 마스터에만 반영되어 있던 쓰기는 유실될 수 있습니다. 강한 정합성이 필요한 데이터라면 Redis에 두는 것 자체를 다시 검토해야 합니다.
  • 리플리카가 없으면 Failover 자체가 불가능합니다. 샤드당 최소 1개의 리플리카 구성이 권장됩니다.

모범 구성

  • Multi-AZ 배포로 장애 도메인 분리
  • 샤드마다 1개 이상의 리플리카
  • MOVED 리다이렉션을 처리할 수 있는 cluster-aware 클라이언트 사용 (lettuce, Jedis, ioredis 등)

정리

패턴 핵심 장치 실패 시 동작
분산 락 SET NX PX + 고유 토큰 + Lua 해제 TTL 만료로 자동 해제, 보상 로직 필요
Streams 파이프라인 Consumer Group, 성공 후 ACK, DLQ, XTRIM pending 재처리로 at-least-once 유지
클러스터 Failover 16384 슬롯 샤딩, 리플리카 자동 승격, MOVED 비동기 복제 구간의 쓰기는 유실 가능

세 패턴의 공통점은 Redis가 기본으로 보장해 주는 범위가 생각보다 좁다는 것입니다. 락은 "정확히 한 번"을 주지 않고, Streams는 ACK 규율 없이는 유실되며, 클러스터는 Failover 과정에서 데이터를 잃을 수 있습니다. 그 간극을 TTL·토큰·멱등 핸들러·리플리카 구성 같은 설계로 메우는 것이 분산 환경에서 Redis를 쓰는 실전 요령입니다.

참고 자료