분산 락은 언제 써야 할까 — DB 락으로 충분한 경우와 Redis 락이 필요한 경우

스터디·11분 읽기

분산 락, 왜 알아야 하나요?

데이터베이스 락, SELECT ... FOR UPDATE, 낙관적 락 vs 비관적 락 글까지 읽고 나면 다음 질문이 자연스럽게 나옵니다.

  • 재고 차감 같은 문제도 Redis 분산 락으로 풀어야 할까요?
  • 여러 서버 인스턴스에서 같은 스케줄러가 동시에 돌면 DB 락으로 막을 수 있을까요?
  • FOR UPDATE, GET_LOCK(), Redis 락은 무엇이 다른가요?
  • 사실 락보다 UNIQUE 제약이나 조건부 UPDATE가 더 나은 경우가 있지 않을까요?

핵심은 분산 락이 더 강한 락이냐가 아닙니다. 먼저 구분해야 할 것은 무엇을 보호하려는가 입니다.

  • 같은 DB 행을 수정하는 경쟁인가?
  • 같은 작업이 여러 인스턴스에서 중복 실행되는 문제인가?
  • 외부 API 호출, 캐시 재생성, 배치 작업처럼 DB 바깥 자원을 조율해야 하는가?

이 글은 MySQL + Redis를 기준으로, DB 락으로 충분한 경우와 Redis 락이 필요한 경우의 경계를 정리합니다.

먼저 선택 기준부터 보면

실무에서는 보통 아래 순서로 판단하면 크게 틀리지 않습니다.

상황 먼저 볼 선택지 이유
같은 DB 행의 상태를 읽고 판단한 뒤 수정 FOR UPDATE 또는 조건부 UPDATE 문제 범위가 이미 DB 트랜잭션 안에 있습니다
중복 삽입 방지 UNIQUE 제약 조건 락보다 더 직접적으로 중복을 금지할 수 있습니다
여러 앱 인스턴스가 같은 작업을 중복 실행 MySQL GET_LOCK() 또는 Redis 락 보호 대상이 특정 행이 아니라 작업 자체입니다
보호 대상이 DB 밖의 자원 Redis 락 같은 외부 조율 수단 DB 행 락만으로는 외부 자원을 잠글 수 없습니다
간헐적 캐시 재생성, 배치 단일 실행 named lock / distributed lock 검토 같은 "행"이 아니라 "작업 키"를 기준으로 조율해야 합니다

가장 짧게 줄이면 이렇습니다.

  • 문제가 DB 행 경쟁이라면 DB 락부터 봅니다
  • 문제가 여러 프로세스의 중복 실행이라면 named lock이나 distributed lock을 봅니다
  • 락 없이 제약 조건이나 원자적 SQL로 끝나면 그쪽이 더 낫습니다

Phase 1. DB 락과 분산 락은 애초에 푸는 문제가 다릅니다

이 둘을 같은 선상에서 비교하면 자꾸 헷갈립니다.

DB 행 락은 "데이터 변경"을 보호합니다

예를 들어 재고 차감은 이런 문제입니다.

START TRANSACTION;

SELECT stock
FROM products
WHERE id = 1
FOR UPDATE;

-- stock > 0 인지 확인

UPDATE products
SET stock = stock - 1
WHERE id = 1;

COMMIT;

여기서 보호 대상은 products.id = 1이라는 행의 현재 상태입니다. 읽기-판단-쓰기 전체가 DB 트랜잭션 안에 있으므로, DB 락이 가장 자연스럽습니다.

분산 락은 "여러 프로세스의 실행"을 조율합니다

반면 이런 문제는 성격이 다릅니다.

  • 서버 4대가 같은 스케줄러를 동시에 실행
  • 같은 캐시 키 재생성이 여러 인스턴스에서 한꺼번에 시작
  • 같은 외부 API 요청을 여러 워커가 동시에 발송

여기서 핵심은 특정 행 하나가 아닙니다. 같은 작업을 누가 지금 수행 중인가를 프로세스 간에 합의해야 합니다.

즉:

  • DB 락은 보통 데이터 중심
  • 분산 락은 보통 작업 중심

으로 생각하는 편이 정확합니다.

Phase 2. 많은 경우 DB 락이나 SQL만으로 충분합니다

분산 락을 붙이기 전에 먼저 확인해야 할 것은, 정말로 DB 밖으로 나가야 하는 문제인지입니다.

1. 재고 차감은 보통 Redis 락보다 SQL이 먼저입니다

재고 차감은 자주 이렇게 끝낼 수 있습니다.

UPDATE products
SET stock = stock - 1
WHERE id = 1
  AND stock > 0;

이 쿼리는:

  • 재고가 있을 때만 차감되고
  • 품절이면 아무 행도 수정하지 않으며
  • 영향받은 행 수로 성공/실패를 판단할 수 있습니다

이 문제를 굳이 Redis 락으로 바꾸면:

  • 락 획득
  • DB 조회
  • DB 갱신
  • 락 해제

처럼 단계만 늘어날 수 있습니다. 핵심 규칙이 이미 DB 한 문장으로 표현된다면, 분산 락은 대개 과합니다.

2. 상태 전이도 조건부 UPDATE가 더 직접적일 수 있습니다

UPDATE orders
SET status = 'PAID'
WHERE id = 1
  AND status = 'READY';

이 경우도:

  • 1행 수정이면 성공
  • 0행 수정이면 이미 다른 상태이거나 선행 조건 불만족

즉, 상태 전이의 핵심이 특정 행의 현재 값이라면 먼저 조건부 UPDATE 를 검토하는 편이 맞습니다.

3. 중복 삽입 방지는 락보다 UNIQUE 제약이 더 강합니다

예를 들어 같은 사용자가 같은 쿠폰을 두 번 발급받으면 안 된다면:

ALTER TABLE issued_coupon
ADD CONSTRAINT uk_coupon_user UNIQUE (coupon_id, user_id);

그 후 그냥 INSERT하고 중복 키 예외를 처리하면 됩니다.

이 문제의 핵심은 "동시에 실행되지 않게 하자"가 아니라 중복 결과를 물리적으로 허용하지 말자이기 때문입니다.

Phase 3. MySQL GET_LOCK()이면 충분한 경우도 있습니다

분산 락 이야기를 할 때 자주 빠지는 중간 단계가 있습니다. named lock 또는 advisory lock 입니다.

MySQL의 GET_LOCK()은 이런 형태입니다.

SELECT GET_LOCK('myapp:daily-settlement', 10);
-- 성공하면 1, timeout이면 0

-- 작업 수행

SELECT RELEASE_LOCK('myapp:daily-settlement');

이 락은 특정 행이 아니라 이름(string) 을 잠급니다. MySQL 문서 기준으로:

  • 같은 이름은 다른 세션이 동시에 획득할 수 없고
  • 명시적으로 RELEASE_LOCK() 하거나 세션이 종료될 때 풀리며
  • COMMIT이나 ROLLBACK으로는 풀리지 않습니다

즉, GET_LOCK()row lock이 아니라 협조적 advisory lock 입니다.

언제 유용할까?

  • 같은 MySQL primary 하나를 모든 앱 인스턴스가 공유하고 있고
  • 보호 대상이 특정 행이 아니라 "작업 이름"일 때
  • 예를 들어 daily-settlement, rebuild-home-cache, send-batch-report 같은 작업을 한 번만 실행하고 싶을 때

이런 경우에는 Redis를 추가로 쓰지 않고도 named lock으로 충분할 수 있습니다.

하지만 한계도 분명합니다

MySQL 문서 기준으로 GET_LOCK()단일 mysqld 기준입니다. 즉:

  • 여러 MySQL 서버 전체에 걸친 범용 분산 락이라고 보기는 어렵고
  • 트랜잭션 커밋/롤백과 자동으로 묶이지도 않으며
  • 락 이름 충돌을 피하려면 애플리케이션별 네임스페이스를 잘 잡아야 합니다

그래서 문제의 범위가 "우리 앱 인스턴스들 + 하나의 MySQL primary"라면 후보가 될 수 있지만, 더 넓은 분산 조율 문제라면 Redis 같은 외부 락 저장소가 더 자연스럽습니다.

참고: PostgreSQL의 advisory lock도 비슷한 목적의 도구입니다. 다만 세부 동작은 DBMS마다 다르므로, 이 글에서는 MySQL GET_LOCK() 기준으로만 설명합니다.

Phase 4. Redis 분산 락이 필요한 순간

Redis 락은 보통 보호 대상이 DB 행이 아닐 때 의미가 커집니다.

1. 여러 인스턴스에서 같은 작업이 중복 실행되면 안 된다

예를 들어 애플리케이션 서버가 4대이고, 각 서버에서 같은 배치 스케줄러가 기동된다고 가정해 보겠습니다.

server-a: 00:00에 정산 배치 시작
server-b: 00:00에 정산 배치 시작
server-c: 00:00에 정산 배치 시작
server-d: 00:00에 정산 배치 시작

여기서는 특정 행 하나를 잠그는 것이 아니라 정산 배치 작업 전체를 한 번만 실행하고 싶습니다. 이런 경우는 작업 이름 기준의 락이 더 잘 맞습니다.

2. 보호 대상이 외부 자원이다

예를 들어:

  • 외부 결제사 정산 API 호출
  • 외부 시스템 동기화 작업
  • 파일 생성/업로드
  • 캐시 재생성

이런 작업은 DB 행 락만으로는 직접 보호할 수 없습니다. DB 트랜잭션 안에서 외부 자원을 잠근다는 개념이 없기 때문입니다.

3. 캐시 재생성처럼 "같은 키 작업"을 한 번만 수행하고 싶다

캐시 스탬피드 글과도 연결되는 부분입니다. 예를 들어 같은 캐시 키가 만료되었을 때 여러 인스턴스가 동시에 재생성을 시작하면 원본 저장소로 요청이 몰릴 수 있습니다.

이때는:

  • cache:rebuild:home-feed
  • cache:rebuild:item:123

같은 작업 키를 기준으로 한 프로세스만 재생성하게 만들고 싶을 수 있습니다. 이런 경우는 distributed lock이 잘 맞는 대표 사례입니다.

Phase 5. Redis 락은 "트랜잭션 락"이 아니라 "lease"에 가깝습니다

Redis 공식 문서에서 가장 중요한 포인트 중 하나는 TTL이 락의 유효 시간이라는 점입니다.

가장 단순한 획득 패턴은 다음과 같습니다.

SET lock:daily-settlement random-token NX PX 30000

이 의미는:

  • 키가 없을 때만 락을 잡고
  • 30초 후에는 자동 만료되며
  • 그 30초 안에 작업이 끝난다고 가정한다

는 뜻입니다.

즉, Redis 락은 "작업이 끝날 때까지 영원히 잠근다"가 아니라, 정해진 유효 시간 동안만 배타성을 기대하는 lease 에 가깝습니다.

그래서 TTL을 잘못 잡으면 문제가 생깁니다

예를 들어 작업이 10초 걸릴 줄 알고 TTL을 10초로 잡았는데, 실제로는 GC pause, 네트워크 지연, 외부 API 지연 때문에 25초가 걸릴 수 있습니다. 그러면:

  1. 클라이언트 A가 락 획득
  2. TTL 만료
  3. 클라이언트 B가 같은 락 획득
  4. A와 B가 겹쳐서 작업 수행

즉, Redis 락은 TTL 안에서만 상호 배제를 기대할 수 있다는 점을 잊으면 안 됩니다.

해제도 DEL만 하면 안 됩니다

Redis 문서는 락 값을 랜덤 토큰으로 두고, 해제 시에는 "내가 잡은 락이 아직 맞는지"를 확인한 뒤 지우라고 설명합니다.

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

이 검사가 필요한 이유는 간단합니다.

  • A가 락 획득
  • TTL 만료
  • B가 새 락 획득
  • A가 늦게 도착해서 그냥 DEL 실행

이렇게 되면 A가 B의 락을 지워 버릴 수 있기 때문입니다.

단일 master + replica failover는 완전한 상호 배제를 보장하지 않습니다

Redis 공식 문서는, 단순히 master에 락을 쓰고 replica failover에 기대는 방식은 비동기 복제 때문에 상호 배제가 깨질 수 있다고 설명합니다.

예를 들어:

  1. 클라이언트 A가 master에 락 획득
  2. 복제되기 전에 master 장애
  3. replica 승격
  4. 클라이언트 B가 같은 락 획득

이 상황에서는 A와 B가 동시에 락을 가진 것처럼 보일 수 있습니다.

Redis 문서는:

  • 가끔 이런 race가 나도 괜찮다면 단일 인스턴스 방식이 현실적인 선택일 수 있고
  • 그렇지 않다면 독립 master 다수에 기반한 Redlock 같은 더 강한 알고리즘을 검토하라고 설명합니다

참고: 여기서 중요한 것은 "무조건 Redlock을 써야 한다"가 아니라, 내가 허용할 수 있는 실패 모델이 무엇인가를 먼저 정하는 것입니다.

Phase 6. 결국 어떻게 고르면 될까?

질문을 바꾸면 판단이 빨라집니다.

질문 1. 보호 대상이 특정 DB 행인가, 작업 이름인가?

  • 특정 행이다 → row lock, 조건부 UPDATE, UNIQUE를 먼저 봅니다
  • 작업 이름이다 → advisory lock이나 distributed lock 후보입니다

질문 2. 모든 인스턴스가 공유하는 단일 DB가 이미 있는가?

  • 있다 → GET_LOCK() 같은 named lock도 후보가 됩니다
  • 없다 / 더 넓은 분산 조율이 필요하다 → Redis 락이 더 자연스럽습니다

질문 3. 락 없이 더 강한 정합성을 만들 수 있는가?

  • 중복 삽입 → UNIQUE
  • 조건부 상태 변경 → 조건부 UPDATE
  • 중복 API 요청 → idempotency key

이런 식으로 해결되면 락보다 더 단순하고 명확합니다.

질문 4. Redis 락의 실패 모델을 감당할 수 있는가?

  • TTL 만료 전 작업 완료를 reasonably 보장할 수 있는가?
  • owner token 검증으로 안전하게 해제하고 있는가?
  • failover 중 드문 중복 실행을 허용할 수 있는가?

이 질문에 답이 모호하면, "Redis 락을 붙이면 끝"이 아닐 가능성이 큽니다.

한눈에 보는 선택 기준

문제 유형 더 먼저 볼 수단 이유
재고 차감, 상태 전이, 같은 행 수정 경쟁 FOR UPDATE, 조건부 UPDATE 보호 대상이 DB 행입니다
중복 삽입 방지 UNIQUE 제약 락보다 더 직접적으로 막을 수 있습니다
단일 MySQL primary 기준의 작업 단일 실행 GET_LOCK() 작업 이름 기준 advisory lock이면 충분할 수 있습니다
여러 인스턴스의 외부 작업 조율 Redis 락 특정 행이 아니라 작업 키를 보호해야 합니다
캐시 재생성, 스케줄러 중복 실행 Redis 락 또는 named lock 같은 작업을 한 번만 수행하고 싶습니다
아주 강한 상호 배제가 필요한 분산 환경 단순 Redis lock 이상 검토 TTL, failover, 알고리즘 가정을 함께 봐야 합니다

정리

  1. DB 락과 분산 락은 푸는 문제가 다릅니다 — 하나는 데이터 변경 경쟁을, 다른 하나는 여러 프로세스의 실행 경쟁을 주로 다룹니다
  2. 많은 동시성 문제는 DB 안에서 끝납니다 — 조건부 UPDATE, UNIQUE, FOR UPDATE가 먼저인 경우가 많습니다
  3. MySQL GET_LOCK() 같은 advisory lock은 Redis 이전에 볼 수 있는 중간 선택지입니다 — 특히 단일 DB를 공유하는 다중 인스턴스 환경에서 유용할 수 있습니다
  4. Redis 락은 외부 자원이나 작업 키를 조율할 때 빛납니다 — 스케줄러 단일 실행, 캐시 재생성, 외부 작업 직렬화가 대표적입니다
  5. Redis 락은 lease 모델이라는 점을 잊으면 안 됩니다 — TTL, owner token 검증, failover 시나리오를 함께 봐야 합니다

핵심을 한 문장으로 줄이면 이렇습니다.

"같은 데이터를 누가 바꾸는가"의 문제는 DB에서, "같은 작업을 누가 실행하는가"의 문제는 named lock이나 distributed lock에서 보는 편이 보통 더 정확합니다