database
분산 시스템을 운영하다 보면 여러 개의 노드가 동일한 자원에 접근하는 상황이 발생합니다. 예를 들어, 쇼핑몰에서 한정 수량의 상품을 주문할 때, 여러 서버에서 동시에 재고를 차감하면 데이터 불일치가 발생할 수 있습니다.
이를 방지하기 위해 **분산 락(Distributed Lock)**이 필요합니다. Redis는 기본적으로 SET NX(Not Exists)와 EX(Expire) 옵션을 이용하여 단일 노드 환경에서는 락을 쉽게 구현할 수 있지만, 다중 노드 환경에서는 단순한 락으로는 여러 장애 상황(네트워크 분할, 노드 장애 등)에 취약할 수 있습니다.
이 문제를 해결하기 위해 등장한 알고리즘이 RedLock입니다.
RedLock은 Redis의 창시자인 Salvatore Sanfilippo가 제안한 분산 락 알고리즘으로, 고가용성과 일관성을 보장해야 하는 분산 시스템 환경에서 신뢰할 수 있는 락 기능을 제공하기 위해 설계되었습니다. 단일 Redis 인스턴스에서의 락은 단순하지만, 장애나 네트워크 분할(Network Partition) 등으로 인해 신뢰성을 담보하기 어렵기 때문에, RedLock은 여러 개의 독립된 Redis 노드(보통 3~5개)를 동시에 활용하여 보다 견고한 락을 구현합니다.
RedLock의 동작 방식은 간단하면서도 강력합니다. 클라이언트는 동일한 락 키를 여러 Redis 인스턴스에 동시에 요청하고, 전체 노드의 과반수 이상(예: 5개 중 3개)에서 락을 성공적으로 획득한 경우에만 해당 락을 유효하다고 판단합니다. 또한 전체 과정에서 소요된 시간이 락의 만료 시간보다 짧아야만 유효한 락으로 간주되어, 시간 기준의 안정성 또한 확보할 수 있습니다.
이러한 구조 덕분에, 일부 Redis 노드가 일시적으로 다운되거나 통신 장애가 발생하더라도 전체 시스템은 여전히 락의 일관성과 유효성을 유지할 수 있는 내결함성(fault-tolerance)을 가지게 됩니다. 이 점은 특히 멀티 인스턴스 환경에서의 데이터 정합성, 동시에 접근하는 작업의 동기화, 크리티컬 섹션 보호 등의 상황에서 RedLock이 매우 유용하게 활용되는 이유입니다.
RedLock은 여러 개(일반적으로 3~5개)의 Redis 노드에 동시에 락을 걸어 분산 환경에서도 안정적인 동기화를 보장하는 알고리즘입니다. 단일 Redis 인스턴스를 사용하는 경우, 해당 서버가 다운되거나 네트워크가 분리되면 락의 신뢰성이 무너질 수 있습니다. 이를 방지하기 위해, RedLock은 다중 노드에 동시에 락을 시도하고, 일정 기준을 만족해야만 락이 유효하다고 판단하는 구조로 설계되어 있습니다.
먼저 클라이언트는 모든 Redis 노드에 동시에 동일한 락 요청을 보냅니다. 이때 사용하는 명령어는
SET my_lock_key random_token NX PX 10000
이 과정을 통해, 하나의 클라이언트만 특정 자원에 접근할 수 있도록 제어합니다.
락 요청을 보낸 후, 클라이언트(락을 걸고자 하는 응용 프로그램)는 가능한 빠르게 응답을 받은 노드 수를 계산합니다. 중요한 기준은 전체 Redis 노드의 과반수 이상(예: 5개 중 3개)에서 락을 성공적으로 획득했는지 여부입니다. 이 조건을 만족해야만 해당 락이 “유효한 락”으로 인정됩니다.
예를 들어 5개 노드 중 2개만 락이 설정되었다면, 이는 과반수에 미치지 못하기 때문에 락이 실패한 것으로 간주하고 작업을 중단합니다. 이때 클라이언트는 락 요청 및 응답까지의 총 소요 시간이 락 만료 시간(TTL)의 절반을 넘지 않도록 주의해야 합니다. 예를 들어 TTL이 10초라면, 락 요청부터 응답 확인까지는 5초 이내로 완료되어야 합니다. 이 제한을 두는 이유는, 네트워크 지연이 길어질수록 락 만료 전에 작업을 완료하지 못할 위험이 커지기 때문입니다.
과반수 이상의 Redis 노드에서 락을 획득하고, TTL 절반 이내에 응답을 받았다면 클라이언트는 해당 자원에 대한 작업(DB 쓰기, 파일 처리 등)을 수행할 수 있습니다. 이 시점부터 다른 클라이언트는 해당 자원에 대한 락을 걸 수 없기 때문에, 경쟁 조건(race condition)을 방지하고 안정적인 상태에서 작업이 가능해집니다.
작업이 완료되면, 클라이언트는 락을 해제해야 합니다. 여기서 중요한 것은 락을 설정한 클라이언트만이 해당 락을 해제할 수 있어야 한다는 점입니다. 이를 위해 락 설정 시 사용했던
if redis.call("GET", my_lock_key) == random_token then
redis.call("DEL", my_lock_key)
end
이 코드는 Redis의 Lua 스크립트를 사용한 예시이며, 락 소유권을 가진 클라이언트만 안전하게 락을 해제할 수 있도록 보장합니다. 만약 이 검증 절차 없이 락을 해제한다면, 다른 클라이언트가 실수로 락을 지우는 일이 발생할 수 있고, 이는 동기화 오류로 이어질 수 있습니다. 이러한 일련의 과정을 통해 RedLock은 분산 환경에서도 락의 정합성과 안정성, 복구 가능성까지 고려한 신뢰성 높은 락 메커니즘을 구현할 수 있게 합니다.
RedLock은 락을 자동으로 큐에 넣거나 대기 상태로 만들어주는 기능이 없습니다. 즉, 어떤 클라이언트가 이미 락을 보유 중이라면, 다른 클라이언트가 락을 요청해도 대부분의 Redis 노드에서
따라서 RedLock을 사용할 때는, 재시도 로직(retry with backoff)을 애플리케이션 레벨에서 직접 구현해야 합니다. 일정 시간 대기 후 다시 락을 시도하거나, 최대 재시도 횟수를 설정해 과도한 충돌을 방지하는 방식이 일반적입니다. 이와 같은 전략을 통해 경쟁 조건에서도 시스템이 안정적으로 동작할 수 있도록 제어해야 합니다.
import { createClient } from "redis";
import Redlock from "redlock";
// 1. 분산 Redis 노드에 연결하는 클라이언트 생성 (각기 다른 Redis 서버 주소여야 함)
const redisClients = [
createClient({ url: "redis://localhost:6379" }),
createClient({ url: "redis://localhost:6380" }),
createClient({ url: "redis://localhost:6381" }),
];
// 2. 클라이언트 연결
await Promise.all(redisClients.map(client => client.connect()));
// 3. Redlock 인스턴스 생성
const redlock = new Redlock({
clients: redisClients, // 연결된 Redis 인스턴스 배열
driftFactor: 0.01, // 시간 오차 보정 (기본값 사용 가능)
retryCount: 5, // 락 획득 재시도 횟수
retryDelay: 200, // 재시도 간격 (ms)
});
// 4. 락 획득 및 해제 예제
async function criticalSection() {
try {
// 락 획득 시도 (10초 TTL)
const lock = await redlock.acquire(["resource_key"], 10000);
console.log("락 획득 성공!");
// 중요한 작업 수행 (예: DB 업데이트, 파일 처리 등)
await new Promise(resolve => setTimeout(resolve, 5000));
// 락 해제
await lock.release();
console.log("락 해제 완료!");
} catch (err) {
console.error("락 획득 실패:", err);
}
}
criticalSection();
이 예제는 Node.js 환경에서 redis와 redlock 라이브러리를 이용해 RedLock 알고리즘을 구현한 기본 구조입니다. 핵심은 서로 다른 3개의 Redis 인스턴스에 동시에 락을 요청하고, 과반수(2개 이상)에서 락을 획득했을 때만 유효한 락으로 간주합니다. 락을 성공적으로 획득하면, 그 시간(TTL) 동안 **critical section(중요한 작업)**을 안전하게 실행할 수 있으며, 작업이 끝난 후에는 반드시 lock.release()를 호출하여 락을 해제해야 합니다. 이렇게 함으로써 다음 요청자에게 리소스 접근 권한이 넘어갈 수 있도록 보장됩니다.
단, 주의할 점은 redisClients 배열에 동일한 Redis 주소를 중복해서 넣으면 RedLock의 분산 락 요건을 충족하지 못한다는 것입니다. 각 클라이언트는 물리적으로 분리된 Redis 인스턴스에 연결되어야 하며, 단일 Redis 클러스터나 replica 구조만으로는 RedLock의 목적을 달성할 수 없습니다. 따라서 위 코드를 실환경에서 사용하려면, AWS ElastiCache, GCP Memorystore 등에서 독립된 Redis 인스턴스를 최소 3개 구성하거나, 여러 서버에 각각 Redis를 설치하여 사용해야 RedLock의 강점을 제대로 활용할 수 있습니다.
락의 키는 "공통된 자원을 식별하는 역할"을 하고, 락의 토큰은 "락의 소유권을 식별하는 역할"을 합니다.
하지만 위의 예시에서는 token이 없습니다. redlock 라이브러리는 내부적으로 토큰(random value)을 자동으로 생성하고, 락 해제 시에도 그 토큰이 일치할 때만 삭제하도록 검증하는 로직을 자동으로 처리합니다.
const lock = await redlock.acquire(["resource_key"], 10000);
await lock.release();
토큰 없이 위와 같이 사용을 해도 내부적으로는 락 설정 시
# 생성
SET my_lock_key random_token NX PX 10000
# 락 해제
if redis.call("GET", my_lock_key) == random_token then
redis.call("DEL", my_lock_key)
end
이렇게 해야, 다른 클라이언트가 실수로 남의 락을 해제하는 사고를 막을 수 있습니다.
RedLock은 단순한 분산 락이 아닌, 네트워크 분산 환경에서도 높은 안정성과 일관성을 보장할 수 있도록 설계된 알고리즘입니다. 핵심은 독립된 Redis 노드들로부터 과반수 이상에서 락을 획득하는 합의 기반 구조에 있으며, 이를 통해 일부 노드 장애나 네트워크 분리 상황에서도 동시성 제어를 안정적으로 수행할 수 있습니다.
하지만 실무에서는 RedLock 자체를 과신하기보다는, TTL 설정의 적절성, 락 재확인 로직, 장애 발생 시의 복구 전략까지 포함한 전체 설계가 중요합니다. 특히 락이 풀리지 않는 경우에 대한 대비책과, 락 만료 타이밍을 감지하는 별도의 헬스체크 로직도 필요합니다.
즉, RedLock은 분산 시스템에서 신뢰할 수 있는 락 메커니즘을 제공하지만, 그 활용은 결국 개발자의 책임 있는 설계와 검증에 달려 있습니다. 올바르게 적용된다면, RedLock은 높은 가용성과 정합성이 요구되는 환경에서도 안전한 동시성 제어와 효율적인 자원 보호를 동시에 실현할 수 있는 강력한 도구가 됩니다.