분산 락이란?

여러 서버(프로세스)가 동일한 공유 자원에 동시에 접근할 때, 오직 하나의 프로세스만 자원을 점유하도록 보장하는 메커니즘이다.

단일 서버에서는 synchronized, ReentrantLock 등으로 해결되지만, 멀티 인스턴스 환경에서는 JVM 밖의 외부 저장소가 필요하다. Redis가 가장 널리 쓰인다.

왜 Redis인가?

특성 설명
싱글 스레드 명령어가 순차 실행되므로 race condition 없음
원자적 명령어 SET NX, EVAL(Lua) 등으로 atomic한 락 획득 가능
TTL 지원 락에 만료 시간을 설정하여 데드락 방지
고성능 인메모리 기반으로 지연시간이 매우 낮음

기본 구현: SET NX EX

SET resource_lock <unique_value> NX EX 30
  • NX : 키가 존재하지 않을 때만 설정 (Not eXists)
  • EX 30 : 30초 후 자동 만료
  • unique_value : UUID 등 고유값 — 본인이 건 락만 해제하기 위함

락 획득

String lockKey = "order:lock:" + orderId;
String lockValue = UUID.randomUUID().toString();

Boolean acquired = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);

if (Boolean.TRUE.equals(acquired)) {
    try {
        // 임계 영역 로직
        processOrder(orderId);
    } finally {
        // 락 해제
        releaseLock(lockKey, lockValue);
    }
}

락 해제 — 반드시 Lua 스크립트로

private void releaseLock(String key, String value) {
    String script =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "  return redis.call('del', KEYS[1]) " +
        "else " +
        "  return 0 " +
        "end";
    redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        List.of(key), value
    );
}

왜 Lua인가? GET → 비교 → DEL을 별도로 수행하면, GET과 DEL 사이에 다른 프로세스가 끼어들 수 있다. Lua 스크립트는 Redis에서 원자적으로 실행된다.


Redisson 기반 구현

실무에서는 직접 구현보다 Redisson 라이브러리를 쓰는 것이 안전하다.

RLock lock = redissonClient.getLock("order:lock:" + orderId);

try {
    // 10초 대기, 30초 후 자동 해제
    boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
    if (acquired) {
        processOrder(orderId);
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

Redisson의 장점

  • Pub/Sub 기반 대기: 스핀락이 아닌 이벤트 기반으로 CPU 낭비 없음
  • Watchdog: 락 보유 중 자동으로 TTL 연장 (기본 30초마다)
  • 재진입 가능: 같은 스레드가 동일 락을 여러 번 획득 가능

Redlock 알고리즘

단일 Redis 인스턴스에 의존하면, 그 노드가 죽으면 락도 사라진다. Redlock은 Redis 창시자 Antirez가 제안한 분산 환경 알고리즘이다.

동작 방식

  1. N개(보통 5개)의 독립 Redis 마스터를 준비한다
  2. 클라이언트가 모든 노드에 동시에 락 획득을 시도한다
  3. 과반수(N/2 + 1) 이상 성공하고, 총 소요 시간이 TTL보다 짧으면 락 획득 성공
  4. 실패 시 모든 노드에서 락을 해제한다
Client → [Redis1: OK] [Redis2: OK] [Redis3: OK] [Redis4: FAIL] [Redis5: OK]
         → 4/5 성공, 과반수 충족 → 락 획득 성공

Redlock 논쟁

Martin Kleppmann(DDIA 저자)은 Redlock의 안전성에 의문을 제기했다:

  • GC pause: 락 획득 후 긴 GC가 발생하면, TTL이 만료되어 다른 프로세스가 락을 획득할 수 있다
  • 시계 점프: NTP 동기화로 시스템 시계가 갑자기 뛰면 TTL 계산이 틀어진다

이에 대해 Antirez는 반박했지만, 완벽한 합의에는 이르지 못했다.


극한 시나리오

시나리오 1: 락 보유 중 프로세스 죽음

시간 →
[Process A] 락 획득 ──── 크래시 💀
[Process B]                 대기... 대기... TTL 만료 → 락 획득

방어: TTL이 자동으로 만료시킨다. TTL이 없으면 영원히 데드락이다. TTL은 반드시 설정해야 한다.


시나리오 2: 작업이 TTL보다 오래 걸림

시간 →  0s          30s(TTL만료)        45s
[A] 락 획득 ──── 작업 진행중... ──── DEL 실행 (하지만 이미 B의 락!)
[B]                   락 획득 ──── 작업 진행중...  ← 락이 풀려버림!

문제: A가 자기 락이 아닌 B의 락을 삭제한다.

방어 1: Lua 스크립트로 value 비교 후 삭제 (위에서 설명)

방어 2: Watchdog으로 TTL 자동 연장 (Redisson)

방어 3: 작업 시간을 측정하여 TTL을 충분히 크게 설정


시나리오 3: Redis 마스터 장애 + Failover

시간 →
[A] 마스터에 락 획득 → 마스터 크래시 💀
[레플리카] 승격 (아직 락 데이터 복제 안됨)
[B] 새 마스터에 락 획득 → 성공! (A도 락을 가지고 있다고 생각)
→ 두 프로세스가 동시에 임계 영역 진입!

원인: Redis 복제는 비동기이므로, 마스터가 죽기 전에 복제되지 않은 데이터는 유실된다.

방어:

  • Redlock 사용 (과반수 기반)
  • WAIT 명령어로 동기 복제 강제 (성능 저하 감수)
  • 또는 Zookeeper/etcd 같은 CP 시스템 사용

시나리오 4: 네트워크 파티션

[Process A] ←──× 네트워크 단절 ×──→ [Redis]
[Process B] ←─────── 정상 ────────→ [Redis]

A는 락을 보유 중이지만, Redis와 통신이 끊겨 Watchdog이 TTL을 연장하지 못한다. TTL 만료 후 B가 락을 획득한다.

방어:

  • A는 작업 시작 전 fencing token(단조 증가 번호)을 발급받는다
  • 공유 자원(DB 등)은 fencing token이 현재보다 큰 경우에만 쓰기를 허용한다
[A] fencing token = 33 → DB에 쓰기 시도 (token 33)
[B] fencing token = 34 → DB에 쓰기 시도 (token 34) ✅
[A]                     → DB에 뒤늦게 도착 (token 33 < 34) ❌ 거부

시나리오 5: GC Stop-the-World

시간 →  0s     10s              40s
[A] 락 획득 → GC 발생(STW 30초) → GC 종료, 작업 재개 (락은 이미 만료!)
[B]              TTL만료 → 락 획득 → 작업 중...
→ A와 B 동시 진입!

방어:

  • 락 획득 후 남은 TTL을 확인하고, 충분하지 않으면 작업을 포기
  • Fencing token 패턴 적용
  • G1GC/ZGC 등 STW가 짧은 GC 사용

분산 락 설계 체크리스트

항목 필수 여부 설명
TTL 설정 필수 데드락 방지
unique value + Lua 해제 필수 남의 락 삭제 방지
재시도 + 백오프 권장 일시적 실패 대응
Watchdog (TTL 연장) 권장 장시간 작업 대응
Fencing token 강력 권장 최종 방어선
Redlock (다중 노드) 선택 단일 장애점 제거
타임아웃 필수 락 대기 무한 차단 방지
멱등성 보장 필수 락 실패 시에도 안전한 로직

정리

분산 락은 간단해 보이지만 극한 상황에서 깨지기 쉽다. 핵심 원칙:

  1. TTL은 반드시 설정하되, 작업 시간보다 충분히 길게
  2. 해제는 반드시 Lua로 — GET/DEL 분리 금지
  3. 비동기 복제 환경에서는 Redlock 또는 fencing token 필수
  4. 완벽한 분산 락은 없다 — 최종 방어선은 항상 DB 레벨 멱등성

카테고리:

업데이트: