분산 락은 언제 써야 할까 — DB 락으로 충분한 경우와 Redis 락이 필요한 경우
분산 락, 왜 알아야 하나요?
데이터베이스 락, 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마다 다르므로, 이 글에서는 MySQLGET_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-feedcache: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초가 걸릴 수 있습니다. 그러면:
- 클라이언트 A가 락 획득
- TTL 만료
- 클라이언트 B가 같은 락 획득
- 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에 기대는 방식은 비동기 복제 때문에 상호 배제가 깨질 수 있다고 설명합니다.
예를 들어:
- 클라이언트 A가 master에 락 획득
- 복제되기 전에 master 장애
- replica 승격
- 클라이언트 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, 알고리즘 가정을 함께 봐야 합니다 |
정리
- DB 락과 분산 락은 푸는 문제가 다릅니다 — 하나는 데이터 변경 경쟁을, 다른 하나는 여러 프로세스의 실행 경쟁을 주로 다룹니다
- 많은 동시성 문제는 DB 안에서 끝납니다 — 조건부
UPDATE,UNIQUE,FOR UPDATE가 먼저인 경우가 많습니다 - MySQL
GET_LOCK()같은advisory lock은 Redis 이전에 볼 수 있는 중간 선택지입니다 — 특히 단일 DB를 공유하는 다중 인스턴스 환경에서 유용할 수 있습니다 - Redis 락은 외부 자원이나 작업 키를 조율할 때 빛납니다 — 스케줄러 단일 실행, 캐시 재생성, 외부 작업 직렬화가 대표적입니다
- Redis 락은
lease모델이라는 점을 잊으면 안 됩니다 — TTL,owner token검증, failover 시나리오를 함께 봐야 합니다
핵심을 한 문장으로 줄이면 이렇습니다.
"같은 데이터를 누가 바꾸는가"의 문제는 DB에서, "같은 작업을 누가 실행하는가"의 문제는
named lock이나distributed lock에서 보는 편이 보통 더 정확합니다