백엔드 데이터 인프라 63편 — Distributed Lock + Redlock 알고리즘

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

백엔드 데이터 인프라 63편. Redis 분산 락 — SET NX EX 한 줄의 한계와 Redlock 알고리즘의 5단계, master-replica 환경의 안전성 트레이드오프, Redisson Spring 통합 코드까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 63편 — Distributed Lock + Redlock 알고리즘

이 글은 백엔드 데이터 인프라 시리즈 130편 중 63편이에요. 62편 에서 9가지 패턴을 한눈에 매핑했다면, 그중 가장 까다로운 분산 락(Distributed Lock, 여러 서버가 같은 자원을 동시에 못 만지게 막는 잠금) 을 깊이 풀어요. 49편에서 본 SET NX EX 의 한계, Redlock 알고리즘, Redisson Spring 통합, 실무 함정까지.

Distributed Lock이 어렵게 느껴지는 이유

이름은 단순한데 정확하고 안전하게 구현하는 건 분산 시스템에서 가장 까다로운 주제 중 하나예요. 세 군데에서 막혀요.

첫째, 단일 Redis 인스턴스만으로는 100% 보장이 안 돼요. master 가 죽고 replica 가 promote(승격) 되는 몇 ms 사이에 락 정보가 사라질 위험이 있어요. "단일 인스턴스 락은 빠르지만 안전하지 않다" 가 핵심이에요.

둘째, Redlock 알고리즘이 처음 보면 복잡해요. 5개 Redis 인스턴스, quorum(과반수 합의), clock skew(서버 간 시계 어긋남), validity time(락 유효 시간) 같은 개념이 한꺼번에 나와요.

셋째, Martin Kleppmann 과 Antirez 의 논쟁이 있어요. 분산 시스템 권위자 Martin Kleppmann 은 "Redlock 도 안전하지 않다" 라고 비판했고, Redis 창시자 Antirez 가 반박했어요. 논쟁의 교훈은 알지만 실무에서 어느 쪽을 따라야 할지가 처음에는 헷갈려요.

이 글에서는 세 가지를 정확한 트레이드오프로 풀어요. 언제 단일 인스턴스 락으로 충분한지, 언제 Redlock 이 필요한지, 언제 그것도 부족해 다른 도구로 가야 하는지.

왜 분산 락이 필요한가

같은 자원에 여러 인스턴스가 동시에 작업하지 못하게 막기 위해서예요.

문제 상황

  • 결제 버튼을 빠르게 두 번 누르면 같은 주문에 결제가 동시 호출돼요
  • 재고 차감 요청이 동시에 들어오면 재고가 음수로 떨어져요
  • cron job 클러스터링을 안 한 환경이면 여러 인스턴스가 같은 batch 를 두 번 실행해요
  • 외부 API 호출이 중복되면 비용과 사이드 이펙트가 두 번 발생해요

DB 레벨로 막을 수 있는 자리

  • unique 제약으로 자연스럽게 차단 가능한 경우
  • DB 트랜잭션 SELECT FOR UPDATE 로 충분한 경우

Redis 분산 락이 필요한 자리

  • 외부 호출이 포함된 트랜잭션 (DB 트랜잭션 안에 외부 호출은 비추예요)
  • DB 부담 없이 빠른 락이 필요한 영역 (μs(마이크로초) vs ms)
  • DB 가 없는 작업 (예: 캐시 warming, 외부 API rate limit 조율)

1단계: Naive SET NX EX (49편 복습)

가장 단순한 형태예요.

> SET lock:order:99 my-uuid NX EX 30
OK
# 작업 ...
# 안전 해제 = Lua
> EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 lock:order:99 my-uuid

보장하는 것

  • Mutual exclusion(상호 배제) — 한 시점에 한 클라이언트만 락을 가져요 (단일 인스턴스 가정)
  • Deadlock-free(교착 없음) — TTL 로 워커가 죽어도 자동 해제돼요
  • Fault-tolerance (제한적) — Redis 가 살아 있는 한 락이 동작해요

보장 안 하는 것

  • Master crash 안전성 — master 가 락 정보를 replica 에 복제하기 전에 죽으면 replica promote 후 같은 락이 두 명한테 부여될 수 있어요
  • Clock skew 안전성 — 서버 시계가 크게 어긋나면 TTL 계산에 오차가 생겨요

여기까지가 단일 인스턴스 락의 현실이에요. 대부분의 비결정적 환경에서는 충분히 안전하지만, "한 번이라도 두 클라이언트가 동시에 갖는 사건이 일어나면 큰 문제" 인 자리에는 부족해요.

Redlock 알고리즘 — 5단계

여러 Redis 인스턴스에 동시에 락을 거는 알고리즘이에요. N = 5 (홀수 권장) 인스턴스가 표준이에요.

5단계 흐름

1. 현재 시각 t1 기록
2. 모든 N개 Redis 인스턴스에 SET NX EX 시도 (각각 작은 timeout, 예: 5ms)
3. 성공한 인스턴스 수 카운트
4. 락 유효 시간 계산:
   validity = TTL - (t_now - t1)
5. 성공 인스턴스 ≥ N/2+1 AND validity > 0 이면 → 락 획득 성공
   아니면 → 모든 인스턴스에서 락 제거 + 실패 처리

N=5 의 의미

  • 3개 이상 인스턴스 (과반수) 에 락을 획득하면 락 보유로 봐요
  • 3개 미만 이면 다시 해제하고 재시도해요
  • 인스턴스가 2개까지 죽어도 락 시스템이 동작해요 (3개가 살아 있으면 quorum 성립)

Validity Time 계산

validity = TTL - (시도 끝난 시각 - 시도 시작 시각)

여러 인스턴스에 락을 시도하는 경과 시간이 TTL 의 상당 부분을 차지할 수 있어요. validity 가 음수면 락 획득 자체는 됐지만 이미 만료가 임박한 상태라서 실패 처리하는 게 안전해요.

해제

# 모든 N개 인스턴스에 대해
for instance in all_instances:
    instance.eval(unlock_script, keys=['lock:order:99'], args=[uuid])

과반수가 아니어도 모든 인스턴스에 해제를 시도해요. 일부 인스턴스에 락이 부분적으로 잡혀 있을 수 있어서요.

장단점

Redlock 의 장점

  • Single point of failure(단일 장애점) 제거 — 일부 인스턴스가 죽어도 락 시스템이 동작해요
  • Master crash 안전 — quorum 모델 덕분이에요
  • Liveness(진행 보장) — TTL 로 deadlock 을 막아요

Redlock 의 단점·논쟁점

  • GC pause(가비지 컬렉션 멈춤), process freeze 시 위험 — 락을 잡고 있던 워커가 수십 초 멈췄다가 깨어나는 사이에 다른 클라이언트가 같은 락을 획득하면 두 명이 동시에 잡고 있는 사고가 나요
  • Clock skew 가 큰 환경 — TTL 계산이 시스템 시계 동기화에 의존하는데, NTP(시각 동기화 프로토콜) 가 크게 점프하면 TTL 오차가 생겨요
  • 운영 복잡도 — 독립된 Redis 인스턴스 5개 (Cluster X) 를 운영하는 부담이 있어요

Martin Kleppmann 의 비판 (요약)

Kleppmann 은 "Distributed lock 자체가 fencing token(락마다 단조 증가하는 ID) 없이는 안전하지 않다" 고 주장했어요. 보호된 자원이 오래된 token 을 거부하는 방식이에요. ZooKeeper(분산 코디네이션 서비스) 나 etcd(분산 키-값 저장소) 같은 consensus 시스템이 이런 보장을 제공해요.

Antirez (Redis 창시자) 의 반박

Antirez 는 "대부분의 실무 환경에서 Redlock 으로 충분하다" 고 봐요. Redis 가 시스템에서 가장 신뢰할 만한 부분이라고 가정하면 fencing 없이도 충분히 안전하다는 거예요.

실무 결론

  • 금융이나 법적 보장이 필요하고 한 번도 두 명한테 동시 부여되면 안 되는 자리 → ZooKeeper / etcd (fencing token)
  • 대부분의 비즈니스 로직 (멱등성 확보 가능) → Redlock 또는 단일 인스턴스 SET NX EX
  • 시간·비용 부담이 최소고 99.9% 안전이면 되는 자리 → 단일 인스턴스 + Lua 해제

Redisson — Java 환경 표준 구현

Java 환경에서는 Redlock 을 직접 구현하지 말고 Redisson(Java 용 Redis 클라이언트 라이브러리) 을 쓰는 게 표준이에요.

단일 인스턴스 락

RLock lock = redisson.getLock("lock:order:99");
try {
    boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
    if (acquired) {
        try {
            // 작업
        } finally {
            lock.unlock();
        }
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Redlock 형태 (RedissonRedLock)

RLock lock1 = redissonClient1.getLock("myLock");
RLock lock2 = redissonClient2.getLock("myLock");
RLock lock3 = redissonClient3.getLock("myLock");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
lock.tryLock(10, 30, TimeUnit.SECONDS);
// ...
lock.unlock();

Spring Boot 통합

# application.yml
redisson:
  single-server-config:
    address: "redis://localhost:6379"
@Autowired
private RedissonClient redisson;

public void processOrder(Long orderId) {
    RLock lock = redisson.getLock("lock:order:" + orderId);
    try {
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            try {
                // 비즈니스 로직
            } finally {
                lock.unlock();
            }
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

Redisson 의 추가 기능

  • Watchdog(자동 TTL 연장) — 락을 잡은 워커가 주기적으로 TTL 을 늘려서 작업이 TTL 보다 길어져도 락이 유지돼요
  • Reentrant Lock(재진입 락) — 같은 스레드가 여러 번 lock() 을 호출해도 OK (ReentrantLock 모델)
  • FairLock — 호출 순서대로 공정하게 락을 부여해요
  • MultiLock — 여러 락을 원자적으로 한 번에 잡아요

한계·실무 함정

1. GC pause 함정 (Kleppmann 의 핵심 비판)

T1: 워커 A가 락 획득 (TTL 30초)
T2: 워커 A가 GC pause 60초 (이 동안 워커 A는 멈춤)
T3: 30초 TTL 만료 → 다른 워커 B가 같은 락 획득
T4: GC pause 끝난 워커 A가 깨어나서 보호된 자원에 쓰기
    → 워커 A와 B가 동시에 자원 사용!

해법은 fencing token 을 쓰거나 작업 자체에 멱등성을 보장하는 거예요. Redis 락만으로 100% 완벽한 mutual exclusion 을 만들 수는 없어요.

2. Network partition

락 노드와 워커 노드 사이가 네트워크적으로 분리되면 락은 살아 있는데 갱신을 못 해요. 그러다 TTL 이 만료되면 다른 워커가 락을 가져가요.

3. 락 잡고 너무 긴 작업

TTL 이 30초인데 작업이 60초 걸리면, TTL 만료 후 내가 작업 중인 자원을 다른 워커가 가져가요. Watchdog (Redisson 자동) 을 쓰거나 작업을 짧게 쪼개야 해요.

4. 락 해제 못 함 (워커 강제 종료)

finally 블록이 빠지거나 kill -9 로 죽으면 락이 TTL 만료까지 그대로 남아요. TTL 을 너무 길게 잡으면 안 돼요.

시험 직전 한 번 더 — Distributed Lock 함정 압축 노트

  • 단일 인스턴스 락 = SET NX EX + Lua 해제
  • 단일 인스턴스 = 빠르지만 master crash 위험
  • Redlock = N=5 (홀수) 인스턴스 + quorum (N/2+1) 모델
  • Redlock 5단계 = t1 기록 → 모든 인스턴스 시도 → 성공 카운트 → validity 계산 → quorum 체크
  • Validity time = TTL - (시도 경과 시간)
  • validity ≤ 0 = 락 획득해도 만료 임박 → 실패 처리
  • 해제 = 모든 인스턴스에 해제 (부분 잡힘 대비)
  • Redlock 장점 = SPOF 제거, master crash 안전, liveness
  • Redlock 단점 = GC pause·process freeze 위험, clock skew, 운영 복잡
  • Kleppmann 비판 = fencing token 없이 분산 락은 완전 안전 X
  • Antirez 반박 = 실무 환경 대부분에 충분
  • 실무 결론 — 금융·법적 = ZooKeeper/etcd, 비즈니스 = Redlock/단일 + 멱등성
  • Redisson = Java 환경 표준 라이브러리
  • RLock = 단일 / RedissonRedLock = Redlock 형태
  • Watchdog = 락 자동 TTL 연장
  • Reentrant / Fair / MultiLock 등 풍부한 변형
  • GC pause 함정 — 워커 멈춘 사이 락 만료 → 다른 워커 + 깨어난 워커 동시
  • 해법 = fencing token 또는 작업 자체 멱등성
  • TTL 너무 길면 = kill 후 재시도까지 긴 대기
  • TTL 너무 짧으면 = 작업 완료 전 락 손실
  • 적정 TTL = 작업 평균 시간의 2~3배 + Watchdog
  • 락 잡고 너무 긴 작업 = 작업 쪼개기 또는 Watchdog

공식 문서: Redis Distributed Locks 에서 Redlock 알고리즘 자세한 사양을 확인할 수 있어요.

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

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!