Redis 분산락 구현하기: 원리부터 적용까지 단계별 가이드

한정 수량 100개를 두고 동시에 요청이 몰립니다. 여러 서버 인스턴스가 각자 “재고 1개 남음”을 읽고 모두 차감해 재고가 음수가 됩니다. 단일 서버라면 자바의 synchronized로 막겠지만, 무중단 배포나 오토스케일링으로 인스턴스가 여러 대 떠 있으면 이런 JVM 내부 락은 전혀 통하지 않습니다. 이럴 때 필요한 것이 Redis 분산락입니다.

아래 단계를 따라가며 원리부터 실전 적용까지 정리합니다.

1단계 — 왜 분산 환경에서 락이 깨지는지 이해한다

동시성 문제는 여러 실행 흐름이 공유 자원을 동시에 읽고 쓸 때, 연산이 원자적이지 않아 발생합니다. “재고 조회 → 1 차감 → 저장”이라는 세 단계가 하나의 묶음으로 처리되어야 하는데, 두 인스턴스가 거의 동시에 같은 재고 값을 읽으면 둘 다 같은 값을 기준으로 차감해 한 번의 차감이 사라지는 갱신 분실(Lost Update)이 일어납니다.

synchronized는 같은 프로세스 안에서만 유효하므로 인스턴스가 2대 이상이면 무력화됩니다. DB 비관적 락(SELECT ... FOR UPDATE)도 가능하지만 락 경합이 심해지면 커넥션이 묶이고 DB 부하가 커집니다. 그래서 모든 인스턴스가 공유하는 Redis에 락을 두어 같은 락을 바라보게 만드는 방식이 표준이 됩니다.

2단계 — 가장 단순한 락: SET NX EX

Redis 분산락의 기본은 락 키를 선점하는 것입니다.

SET lock:stock:1001 {uuid} NX EX 3

NX는 키가 없을 때만 설정하므로 먼저 성공한 요청만 락을 획득합니다. EX로 만료 시간을 주어, 락을 잡은 인스턴스가 장애로 죽어도 일정 시간 뒤 자동 해제되게 합니다.

다만 이 단순 방식은 직접 짜야 할 디테일이 많습니다. 획득 실패 시 재시도(스핀락), 만료보다 작업이 길어졌을 때의 처리, 자신이 잡은 락만 안전하게 해제하는 검증을 모두 손으로 구현해야 합니다. 그래서 실무에서는 이 복잡성을 추상화한 Redisson으로 넘어갑니다.

3단계 — Redisson으로 안전하게 구현한다

Redisson은 분산락을 자바 표준 Lock처럼 다루게 해줍니다. 내부적으로 pub/sub 기반 락을 써서 Redis를 계속 폴링하지 않고 해제 신호를 구독하므로 부하가 적습니다.

public void decrease(Long itemId, int quantity) {
    RLock lock = redissonClient.getLock("lock:stock:" + itemId);
    try {
        boolean acquired = lock.tryLock(5, 3, TimeUnit.SECONDS); // 5초 대기, 3초 뒤 자동 해제
        if (!acquired) throw new IllegalStateException("락 획득에 실패했습니다.");

        Stock stock = stockRepository.findByItemId(itemId);
        stock.decrease(quantity);
        stockRepository.save(stock);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) lock.unlock();
    }
}

두 가지가 핵심입니다. 락 해제는 반드시 finally에서 자신이 보유한 락인지 확인 후 수행해야 합니다. isHeldByCurrentThread() 검증 없이 풀면 다른 인스턴스가 잡은 락을 풀어버릴 수 있습니다. 또 트랜잭션 커밋이 락 해제보다 먼저 끝나야 다음 요청이 변경된 데이터를 읽으므로, 락 안에서 트랜잭션이 완전히 끝나도록 메서드 구조를 설계해야 합니다.

4단계 — 타임아웃 전략을 정한다

분산락에서 가장 신경 쓸 부분은 락을 얻지 못했을 때의 처리입니다. 무한정 기다리면 스레드가 고갈되고 장애가 전파됩니다.

파라미터의미설정 기준
waitTime락 대기 최대 시간사용자 응답 허용 시간 내 (예: 3~5초)
leaseTime락 자동 해제 시간임계 영역 최대 수행 시간보다 약간 길게
실패 처리획득 실패 시 동작즉시 예외 또는 재시도 후 안내

leaseTime이 너무 짧으면 작업이 끝나기 전에 락이 풀려 동시성 문제가 다시 생기고, 너무 길면 장애 시 자원이 오래 묶입니다. 선착순 이벤트처럼 트래픽이 폭증하면, 대기 없이 즉시 실패시키고 “잠시 후 다시 시도”로 부하를 분산하는 편이 낫습니다.

응용 — 캐시 스탬피드도 같은 도구로 막는다

Redis는 캐싱 저장소로도 핵심인데, 여기에도 동시성 함정이 있습니다. 인기 데이터의 캐시가 만료되는 순간 수많은 요청이 동시에 캐시 미스를 겪고 한꺼번에 DB로 몰려가는 캐시 스탬피드(Cache Stampede)입니다. 캐시 재생성 구간에 분산락을 걸어 한 요청만 DB를 조회하게 하거나, 만료 시간에 약간의 무작위 값(jitter)을 더해 동시 만료를 분산시키는 방식으로 막습니다.

마지막으로 실무 체크포인트. 락 키는 lock:stock:{itemId}처럼 자원 단위로 잘게 나눠 불필요한 경합을 피하고, 락 획득 → 트랜잭션 → 락 해제 순서를 보장하며, Redis가 단일 장애점이 되지 않도록 가용성을 고려합니다. 무엇보다 꼭 분산락이어야 하는지 먼저 검토하세요. 단순 카운터 증감은 Redis의 원자적 INCR이나 DB의 원자적 UPDATE로 더 간단히 풀리는 경우가 많습니다. 분산락은 강력하지만 비용이 있는 도구라, 정말 임계 영역 보호가 필요한 곳에만 정교하게 적용하는 것이 좋습니다.

참고: Redisson 공식 위키, Redis 분산락 문서

댓글 남기기