TIL · Redis · MSA · Concurrency Control
Redis 분산 락, Redisson Watchdog, Pub/Sub vs MQ
오늘은 MSA 환경에서 반드시 마주치게 되는 동시성 문제를 Redis 중심으로 정리했다. 단순히 "락을 건다" 수준이 아니라, 왜 초과 판매가 발생하는지, SETNX가 왜 위험할 수 있는지, Redisson Watchdog은 언제 믿어도 되는지, Pub/Sub과 MQ는 어떻게 구분해야 하는지까지 한 번에 연결해서 정리해봤다.
한 줄 요약
분산 환경에서는 여러 서버가 동시에 같은 자원을 건드리기 때문에 단일 서버 시절의 방식만으로는 정합성을 지키기 어렵다. 그래서 Redis 분산 락, 원자적 명령어, Lua 스크립트, 메시지 큐 같은 도구를 문제 성격에 맞게 선택해야 한다.
1. 왜 MSA에서는 동시성 문제가 더 무서워질까?
단일 서버 환경에서는 하나의 프로세스 안에서 synchronized나 JVM lock으로 어느 정도 제어가 가능했다. 하지만 MSA 환경에서는 여러 서버 인스턴스가 동시에 같은 재고, 쿠폰 수량, 계좌 잔액 같은 공유 자원에 접근한다. 이때 각 서버가 "지금 재고가 있네?"라고 동시에 판단하면 초과 판매(Over-sell) 같은 문제가 발생한다.
결국 핵심은 읽기와 쓰기 사이의 틈이다. 여러 요청이 같은 값을 읽고, 그 사이에 서로의 변경을 모른 채 업데이트를 수행하면 데이터 정합성이 깨진다. 이 문제를 막기 위해 등장하는 대표적인 해법이 바로 분산 락(Distributed Lock)이다.
2. 분산 락은 무엇이고, 왜 Redis를 많이 쓸까?
분산 락은 쉽게 말해 "공용 화장실 열쇠" 같은 개념이다. 여러 서버가 공용 자원에 접근하려고 할 때, 먼저 열쇠를 가져간 서버만 임계 구역에 들어가고 나머지는 기다리거나 실패 처리하는 방식이다.
Redis는 싱글 스레드 기반으로 명령이 원자적으로 처리되고, 메모리 기반이라 매우 빠르다. 그래서 짧은 시간 동안 "오직 하나만 들어가게" 만드는 락 저장소로 자주 사용된다.
핵심 명령
SET lock:product:1 request-uuid NX EX 5
여기서 중요한 건 NX와 EX를 함께 쓰는 것이다. 예전처럼 SETNX로 먼저 락을 잡고, 다음 줄에서 EXPIRE를 따로 호출하면 그 사이 서버가 죽었을 때 락이 영원히 안 풀릴 수 있다. 즉, 락 획득과 TTL 설정은 반드시 한 번의 원자적 명령으로 처리해야 한다.
3. 문서 속 예시와 도표에서 배운 포인트
재고 차감 / 초과 판매 시나리오
여러 서버가 동시에 재고를 읽고 각각 "재고 있음"으로 판단한 뒤 차감을 수행하면 실제 수량보다 더 많이 판매되는 상황이 생긴다. 이 예시는 MSA에서 분산 락이 왜 필요한지를 가장 직관적으로 보여준다.
데드락 시나리오
락은 잡았지만 만료 시간 설정 전에 서버가 죽으면, 락 키가 영구히 남아 다른 서버가 영원히 진입하지 못한다. 그래서 SETNX와 EXPIRE를 분리 호출하는 구현은 실무에서 매우 위험하다.
동시성 제어 기술 비교표
DB 비관적 락, DB 낙관적 락, Redis 기본 락, Redisson은 각각 장단점이 다르다. 정합성이 최우선이면 DB 락이 더 맞을 수 있고, 고성능 선착순 처리에는 Redis 계열이 유리하다. 결국 기술 선택은 트래픽과 정합성 요구사항의 균형 문제다.
Pub/Sub vs MQ 비교표
Redis Pub/Sub은 빠르지만 휘발성이고, Kafka/RabbitMQ는 보관과 Ack를 통해 더 높은 처리 보장성을 제공한다. 채팅 알림처럼 실시간성이 중요한 경우와, 결제 이벤트처럼 절대 유실되면 안 되는 경우를 반드시 구분해야 한다.
4. Redisson을 쓰면 왜 편해질까?
직접 Redis 락을 구현하면 생각보다 신경 쓸 게 많다. 락 획득 재시도, TTL 관리, 락 해제 시 본인 검증, 예외 상황 처리, 스핀락 부하 문제까지 모두 개발자가 챙겨야 한다. 반면 Redisson은 이런 부분을 더 안전하고 편하게 추상화해준다.
Watchdog이 중요한 이유
비즈니스 로직이 예상보다 오래 걸리면 TTL이 먼저 끝나서 락이 풀릴 수 있다. 그러면 아직 작업 중인데도 다른 서버가 같은 자원에 들어와 버린다. Redisson의 Watchdog은 이런 상황을 막기 위해 락 만료 시간을 자동 연장해준다.
다만 함정도 있다. leaseTime을 직접 지정하면 Watchdog이 동작하지 않는다. 그래서 "무조건 leaseTime 넣는 게 더 안전하겠지?"라고 생각하면 오히려 긴 작업에서 락이 중간에 풀릴 수 있다. 이건 실무에서 정말 자주 헷갈리는 포인트다.
5. 락 해제는 왜 UUID 검증이 필요할까?
락 값(value)에 단순히 1 같은 값을 넣으면 안 된다. 내 요청이 락을 잡은 뒤 시간이 지나 TTL이 만료되고, 다른 요청이 같은 키로 새 락을 잡았다고 가정해보자. 그런데 이전 요청이 뒤늦게 DEL lock:key를 실행하면, 내가 잡은 락이 아니라 다른 요청이 새로 잡은 락을 지워버릴 수 있다.
그래서 락 value에는 반드시 UUID 같은 고유 식별자를 넣고, 해제할 때도 "현재 value가 내 UUID와 같을 때만 삭제"해야 한다. 이 검증과 삭제는 보통 Lua 스크립트로 원자적으로 처리한다.
예시 개념
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
6. Redis Pub/Sub과 MQ는 어떻게 구분해야 할까?
문서에서 가장 인상적이었던 비교는 Redis Pub/Sub을 "라디오 방송", Kafka를 "우체국"에 비유한 부분이다. Redis Pub/Sub은 발행 시점에 연결된 구독자에게만 메시지가 전달되고, 중간에 구독자가 내려가 있으면 메시지는 그대로 사라진다.
반면 Kafka나 RabbitMQ 같은 메시지 큐는 메시지를 저장하고, 소비자의 처리 여부를 추적할 수 있다. 그래서 이벤트 유실이 치명적인 주문, 결제, 정산, 포인트 적립 같은 기능에는 MQ가 더 적합하다. 반대로 실시간 알림, 일시적인 브로드캐스트, 워커 깨우기 같은 용도라면 Pub/Sub이 훨씬 가볍고 빠르다.
7. 락 없이 해결하는 방법도 있다
이 문서가 좋은 이유는 락만 강조하지 않는다는 점이다. 단순 수량 증가/감소라면 락보다 INCR, DECR 같은 원자적 명령이 훨씬 빠르고 단순하다. 또 여러 조건이 걸린 복합 로직도 Lua 스크립트로 묶어 서버 내부에서 원자 실행하면 네트워크 왕복을 줄이면서 안전하게 처리할 수 있다.
즉, 실무에서는 먼저 "이걸 진짜 락으로 풀어야 하나?"를 질문해야 한다. 카운터 문제인지, 상태 전이 문제인지, 메시지 전달 문제인지에 따라 정답이 달라진다.
8. 실무에서 추가로 꼭 알아야 할 포인트
Redis 분산 락은 매우 유용하지만, 완벽한 분산 합의 시스템은 아니다. 단일 마스터 복제 지연이나 장애 전환 상황에서는 상호 배제가 깨질 가능성이 있다. 그래서 금융처럼 정합성 요구 수준이 매우 높은 시스템에서는 Redis 락만 믿지 않고 DB 락이나 추가 검증 로직을 함께 사용하는 다층 방어 전략이 필요하다.
또한 락이 있어도 중복 요청은 언제든 발생할 수 있으므로 멱등성(idempotency)을 반드시 고려해야 한다. 결제 승인, 주문 생성, 쿠폰 발급 같은 기능은 같은 요청이 여러 번 들어와도 결과가 한 번만 반영되도록 설계해야 실제 운영에서 안전하다.
그리고 "Exactly-once"라는 표현은 매우 조심해서 써야 한다. 대부분의 시스템은 결국 중복 가능성을 전제로 설계하고, idempotency key, 상태 전이 검증, outbox/inbox 패턴 등으로 안정성을 확보한다.
9. 실무 사례로 이해한 선택 기준
물류나 재고 시스템에서는 DB 락만으로 버티기 어렵기 때문에 Redis 기반 분산 락이 성능상 유리할 수 있다. 하지만 금융 서비스처럼 오차가 허용되지 않는 영역에서는 Redis 락만으로 끝내지 않고 DB 레벨 검증까지 겹쳐 쓰는 방식이 더 현실적이다.
또 선착순 이벤트 시스템에서는 Pub/Sub을 "데이터 전달"이 아니라 "워커를 깨우는 트리거" 정도로만 쓰고, 실제 데이터는 Redis List나 MQ에 안전하게 적재하는 방식이 훨씬 안정적이다. 이 지점이 Pub/Sub과 MQ를 구분해서 써야 하는 이유를 가장 잘 보여준다.
10. 오늘의 TIL
오늘의 핵심 배움 1
분산 환경의 동시성 문제는 "여러 서버가 동시에 읽고 수정한다"는 사실에서 시작된다.
오늘의 핵심 배움 2
Redis 락은 빠르지만, 락 획득/TTL/해제 검증을 모두 제대로 설계해야 안전하다.
오늘의 핵심 배움 3
Redisson Watchdog은 편리하지만, leaseTime을 직접 주면 동작하지 않는다는 점을 반드시 기억해야 한다.
오늘의 핵심 배움 4
Redis Pub/Sub은 실시간 브로드캐스트에 적합하고, 유실되면 안 되는 비즈니스 이벤트는 MQ로 다뤄야 한다.
오늘의 핵심 배움 5
모든 동시성 문제를 락으로 풀 필요는 없고, 원자적 명령어와 Lua 스크립트가 더 좋은 해법일 때도 많다.
11. 면접 대비 한 줄 답변
Q. SETNX와 EXPIRE를 왜 분리하면 안 되나요?
락을 잡은 뒤 EXPIRE 전에 서버가 죽으면 락이 영원히 안 풀려 데드락이 발생할 수 있기 때문이다.
Q. Redisson Watchdog은 언제 동작하나요?
일반적으로 leaseTime을 직접 주지 않았을 때 동작하며, 락의 TTL을 자동 연장해준다.
Q. Pub/Sub과 MQ의 가장 큰 차이는 뭔가요?
Pub/Sub은 휘발성 브로드캐스트이고, MQ는 메시지 저장과 처리 보장을 제공한다.
Q. 락 없이 동시성을 제어할 수도 있나요?
가능하다. 단순 카운팅은 INCR/DECR, 복합 조건은 Lua 스크립트로 원자 처리할 수 있다.
