분산 락은 언제 써야 할까 — DB 락으로 충분한 경우와 Redis 락이 필요한 경우
전체 보기접기
- 01트랜잭션 격리 수준 완전 정복 — Read Uncommitted부터 Serializable까지
- 02MVCC 완전 정복 — Undo 로그부터 스냅샷 읽기까지
- 03데이터베이스 락 완전 정복 — 공유 락부터 데드락까지
- 04SELECT ... FOR UPDATE는 언제 써야 할까 — 비관적 락이 필요한 순간
- 05주문은 한 번 취소됐는데 환불 이력은 왜 두 번 쌓였을까 — MySQL `REPEATABLE READ` 실전 사례
- 06멱등성 완전 정복 — 중복 요청을 한 번처럼 처리하는 법
- 07분산 락은 언제 써야 할까 — DB 락으로 충분한 경우와 Redis 락이 필요한 경우읽는 중
- 08낙관적 락 vs 비관적 락 — `@Version`과 `FOR UPDATE`를 고르는 실무 기준
- 09Dirty Read와 Phantom Read는 실제로 언제 발생할까 — 교과서와 실무 사이의 간격
- 102단계 로킹 규약 완전 정복 — 2PL, Strict 2PL, 직렬 가능성까지
분산 락, 왜 알아야 하나요?
데이터베이스 락, 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은 논쟁이 있는 알고리즘입니다. Martin Kleppmann은 프로세스 일시 정지나 시계 점프 같은 비동기 환경의 가정 때문에 상호 배제가 깨질 수 있다고 지적했고, antirez(Redis 저자)는fencing token을 함께 쓰면 안전하다고 재반박했습니다. 즉 "무조건Redlock을 써야 한다"거나 "Redlock은 쓰면 안 된다"로 단정하기보다, 내가 허용할 수 있는 실패 모델이 무엇인가를 먼저 정하고, 상호 배제를 절대적으로 보장해야 하는 구간에는fencing token같은 추가 안전장치를 함께 설계하는 편이 안전합니다.
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에서 보는 편이 보통 더 정확합니다
다음으로 읽어볼 글
2단계 로킹 규약 완전 정복 — 2PL, Strict 2PL, 직렬 가능성까지
2PL의 Growing phase와 Shrinking phase, Strict 2PL과 Conservative 2PL의 차이, 그리고 MySQL InnoDB를 2PL로만 보면 안 되는 이유를 정리합니다.
낙관적 락 vs 비관적 락 — `@Version`과 `FOR UPDATE`를 고르는 실무 기준
낙관적 락과 비관적 락을 충돌률, 재시도 비용, 트랜잭션 길이 관점에서 비교하고, 조건부 `UPDATE`나 `UNIQUE` 제약이 더 나은 경우까지 정리합니다.
SELECT ... FOR UPDATE는 언제 써야 할까 — 비관적 락이 필요한 순간
SELECT ... FOR UPDATE가 정확히 무엇을 잠그는지, 어떤 상황에서 필요하고 어떤 상황에서는 과한지, 인덱스와 트랜잭션 범위까지 실무 기준으로 정리합니다.