DB 락만 믿고 배포했다가 서비스가 멈췄습니다 — 분산 락이 진짜 필요한 순간

대표 이미지

DB 락만 믿고 배포했다가 서비스가 멈췄습니다 — 분산 락이 진짜 필요한 순간

단순한 행 잠금(Row-level Lock)의 한계를 넘어, Redis 분산 락을 도입해야 하는 아키텍처적 이유와 치명적인 함정을 분석합니다.

가끔 후배들이 “동시성 문제는 그냥 DB에서 SELECT FOR UPDATE로 잡으면 되는 거 아니에요?”라고 묻곤 합니다. 이론적으로는 맞죠. 하지만 실제 운영 환경, 특히 트래픽이 몰리는 분산 시스템에서 이걸 그대로 적용했다가 서비스 전체가 마비되는 광경을 저는 몇 번이나 봤습니다.

DB 락은 기본적으로 백엔드 트랜잭션이 살아있는 아주 짧은 시간 동안만 유지되도록 설계되었거든요. 그런데 여기에 ‘사람의 시간(Human-time)’, 즉 몇 초에서 몇 분 단위의 대기 시간이 개입되는 순간 DB 성능은 완전히 파괴됩니다 [1]. 예를 들어, 선착순 쿠폰 발급 이벤트에서 DB 락을 잡은 채로 외부 결제 게이트웨이(PG)의 응답을 기다린다고 생각해보세요. PG사 응답이 3초만 늦어져도 그동안 해당 행을 점유한 커넥션은 아무것도 못 하고 묶여 있게 됩니다.

결국 핵심은 이겁니다. 데이터베이스 락은 트랜잭션 단위의 짧은 작업에는 적합하지만, 사람이 개입하거나 여러 서비스 간의 조정이 필요한 분산 환경에서는 성능 붕괴와 데드락을 유발하기 쉽습니다. 이럴 때는 제어권을 DB 밖으로 꺼내 Redis 같은 분산 락으로 분리해야 시스템이 비로소 숨을 쉴 수 있습니다.

우리가 배운 ‘DB 락’이 분산 환경에서 무너지는 이유

우리가 흔히 쓰는 비관적 락이나 낙관적 락 같은 행 수준 잠금(Row-level Lock)은 기본적으로 단일 DB 인스턴스 내부에서만 유효합니다. 서비스가 하나일 때는 문제가 없지만, 여러 서비스 노드가 떠 있는 분산 환경에서는 서비스 간의 세밀한 조정이 불가능하죠 [1].

더 심각한 건 ‘커넥션 점유’ 문제입니다. DB 락은 트랜잭션에 종속되어 있어요. 즉, 락을 쥐고 있는 동안에는 DB 커넥션을 계속 붙잡고 있어야 한다는 뜻입니다. 만약 사용자 요청이 폭주하는데 락 유지 시간이 길어진다면 어떻게 될까요? 오픈 커넥션 수가 급증하면서 DB 서버의 메모리와 CPU가 고갈되고, 결국 새로운 요청을 처리하지 못해 시스템 전체가 뻗어버리는 ‘커넥션 풀 고갈(Connection Pool Exhaustion)’ 상황이 옵니다 [1].

실제로 한 커머스 서비스에서는 재고 차감 로직에 비관적 락을 걸었다가, 특정 상품에 트래픽이 몰리자 DB 커넥션이 모두 락 대기 상태로 전환되어 다른 상품 조회 API까지 모두 응답 불능 상태가 된 사례가 있습니다. 특히 트랜잭션 범위 안에 외부 API 호출이 포함되어 있거나, 사용자가 결제 버튼을 누르기까지 기다리는 시간이 포함된다면 이건 정말 치명적입니다.

“Database locks are not designed to be long-lived. They generally live for the life of a back-end transaction, not for human-time.” [1]

데이터베이스 락은 오래 유지되도록 설계되지 않았으며, 백엔드 트랜잭션의 수명 동안만 유지될 뿐 ‘사람의 시간’을 견디지 못한다는 의미입니다.

Redis 분산 락: 제어권을 DB 밖으로 꺼내야 하는 이유

그래서 우리는 락의 제어권을 DB 밖으로, 즉 Redis 같은 인메모리 저장소로 옮겨야 합니다. 이렇게 하면 DB 커넥션과 독립적으로 락 상태를 관리할 수 있어 DB 리소스 점유를 최소화할 수 있거든요. Redis는 기본적으로 저지연 읽기/쓰기를 제공하기 때문에 락을 획득하고 해제하는 오버헤드가 매우 적습니다 [2].

또한, Redis 분산 락의 가장 큰 장점 중 하나는 TTL(Time-To-Live) 설정입니다. 만약 락을 획득한 프로세스가 갑자기 네트워크 장애나 OOM(Out Of Memory)으로 다운되면 어떻게 될까요? DB 락이라면 트랜잭션 타임아웃이 발생하거나 DB 세션이 끊길 때까지 다른 요청들이 줄줄이 대기해야 하지만, Redis는 설정한 시간이 지나면 락이 자동으로 사라져 ‘좀비 락’으로 인한 시스템 마비를 막아줍니다.

여러 마이크로서비스가 하나의 공유 리소스를 두고 다툴 때, Redis는 모든 서비스가 바라보는 단일한 동기화 지점이 되어줍니다 [3]. 예를 들어, 주문 서비스와 재고 서비스가 서로 다른 DB를 쓰더라도 Redis라는 공통의 락 저장소를 통해 동일한 자원에 대한 동시 접근을 제어할 수 있습니다.

아래는 Redis를 이용해 간단하게 락을 구현하는 예시입니다.

# 락 획득: NX(Not Exists) 옵션으로 키가 없을 때만 생성, EX(Expire)로 10초 후 자동 삭제
# 'lock:order:123'이라는 키로 락을 걸고, 10초의 유효기간을 둡니다.
SET lock:order:123 "unique_request_id_1" NX EX 10

이 설정은 lock:order:123이라는 키가 없을 때만 값을 저장(NX)하고, 동시에 10초라는 만료 시간(EX)을 부여합니다. 이렇게 해야만 락을 쥔 서버가 죽더라도 10초 뒤에는 다른 서버가 작업을 이어받을 수 있습니다.

주의: Redis 락을 직접 구현할 때 빠지는 치명적 함정

그런데 주의할 점이 있어요. 많은 개발자가 실수하는 부분이 SETNX로 락을 잡고, 그 다음 줄에서 EXPIRE로 만료 시간을 설정하는 겁니다.

여기서 한 가지 짚고 넘어갈게요. 만약 SETNX는 성공했는데 EXPIRE를 호출하기 직전에 서버가 뻗어버리면 어떻게 될까요? 만료 시간이 설정되지 않은 락이 Redis에 영원히 남게 됩니다. 이게 바로 전형적인 데드락 상황이죠 [4]. 그래서 반드시 위 예시처럼 SET 명령의 옵션을 통해 원자적(Atomic)으로 처리해야 합니다.

또 다른 함정은 비즈니스 로직 수행 시간이 TTL보다 길어지는 경우입니다. 락은 5초 뒤에 풀리는데, 실제 작업은 7초가 걸린다면? 5초 시점에 다른 클라이언트가 락을 획득하게 되고, 결국 두 프로세스가 동시에 리소스에 접근하는 상호 배제 원칙이 깨지게 됩니다 [4]. 이를 해결하기 위해 ‘락 연장(Lock Renewal)’ 메커니즘이나 Redisson 라이브러리의 ‘Watchdog’ 같은 기능을 사용해 작업 완료 시까지 락 시간을 자동으로 늘려주는 전략이 필요합니다.

마지막으로, 내가 잡은 락이 아닌데 실수로 해제하는 레이스 컨디션도 조심해야 합니다. A가 락을 잡고 작업이 길어져 TTL이 만료되었는데, B가 그 사이 락을 잡았습니다. 이때 A가 작업을 마치고 DEL 명령을 보내면 B가 잡고 있는 락을 지워버리게 됩니다. 따라서 락 값에 고유한 UUID를 넣어, 해제할 때 “내가 잡은 락이 맞는지” 확인하는 Lua 스크립트 기반의 원자적 삭제 절차가 반드시 필요합니다.

Redlock의 환상과 현실: 정밀함과 복잡성의 트레이드오프

단일 Redis 노드의 장애가 걱정되어 Redlock 알고리즘을 고려하시는 분들도 계실 겁니다. Redlock은 여러 개의 독립적인 Redis 노드 중 과반수(Majority)의 동의를 얻어 락을 획득하는 방식이라 가용성이 높죠. 하지만 여기에는 무서운 함정이 있습니다.

Redlock은 기본적으로 시스템 클락(System Clock)에 의존합니다. 만약 특정 노드에서 ‘클락 점프(Clock Jump)’가 발생해 시간이 갑자기 미래로 튀어버리면, 락이 예상보다 빨리 만료되어 보안 문제가 생길 수 있습니다 [4]. 또한 비동기 복제 방식 때문에 락을 획득한 직후 마스터 노드가 장애가 나고 슬레이브가 승격되면, 락 정보가 아직 복제되지 않아 다른 클라이언트가 다시 락을 획득하는 상황이 발생할 수 있습니다.

“If you need locks for correctness, please don’t use Redlock. Instead, please use a proper consensus system such ZooKeeper.” [5]

정확성(Correctness)이 절대적으로 중요하다면 Redlock보다는 ZooKeeper나 etcd 같은 합의 알고리즘(Raft, Paxos) 기반의 시스템을 쓰는 게 맞다는 뜻입니다.

단순히 효율성을 위해 락을 쓰는 거라면 단일 Redis나 Redis Sentinel로도 충분하지만, 금융 거래처럼 데이터 무결성이 생명인 곳에서는 Redlock의 복잡성이 오히려 독이 될 수 있습니다.

짚고 넘어갈 한계와 안티패턴

물론 Redis 분산 락이 만능은 아닙니다. 외부 컴포넌트가 추가되는 만큼 네트워크 지연이 발생하고 시스템 복잡도가 올라갑니다 [1]. 또한 Redis의 데이터 휘발성 때문에 락 정보가 예고 없이 사라질 수 있다는 점도 신뢰성 측면에서 약점이 될 수 있죠 [6].

가장 안 좋은 안티패턴은 “일단 락부터 걸고 보자”는 식의 접근입니다. 락은 동시성을 제한하기 때문에 과도하게 사용하면 시스템 전체의 처리량(Throughput)이 급감합니다. 락을 걸기 전에, 다음과 같은 대안을 먼저 고민하는 게 시니어의 자세입니다 [6].

1. 이벤트 기반 설계: 메시지 큐(Kafka, RabbitMQ)를 이용해 요청을 순차적으로 처리하여 동시성 자체를 제거합니다. 2. 멱등성(Idempotency) 확보: 동일한 요청이 여러 번 들어와도 결과가 같도록 설계하여 락의 필요성을 줄입니다. 3. DB 원자적 연산: UPDATE stock SET count = count - 1 WHERE id = 1 AND count > 0 처럼 DB 자체의 원자적 쿼리를 활용합니다.

핵심 요약

  • DB 락은 트랜잭션 내 짧은 작업용이며, ‘사람의 시간’이나 외부 API 대기가 개입되는 순간 성능 재앙이 됩니다.
  • Redis 분산 락은 DB 커넥션 점유를 막아 시스템 확장성을 확보해 줍니다.
  • SETNXEXPIRE를 따로 호출하는 것은 원자성 부족으로 인한 데드락의 주범이 됩니다.
  • Redlock은 가용성을 높이지만 클락 점프와 복제 지연이라는 근본적 한계가 있습니다.
  • 무결성이 최우선이라면 ZooKeeper/etcd를, 성능과 편의성이 우선이라면 Redis를 선택하세요.

단순히 ‘동시성 해결’이라는 키워드에 매몰되어 툴을 선택하기보다, 우리 서비스가 감당해야 할 ‘정확성의 수준’과 ‘성능의 임계치’가 어디인지 정의하는 것이 중요합니다. 결국 도구보다 중요한 건 그 도구가 실패했을 때 우리 시스템이 어떻게 반응할지를 설계하는 능력이니까요.


참고 자료 (References)

1. [stackoverflow.com] database – How is Redis distributed lock better than using pessimistic or optimistic lock in Ticketmaster design? — https://stackoverflow.com/questions/78286862/how-is-redis-distributed-lock-better-than-using-pessimistic-or-optimistic-lock-i 2. [medium.com] Database Locking Is Not Enough: Why Redis Locks Are Still Required — https://medium.com/@dipankarsethi3012/database-locking-is-not-enough-why-redis-locks-are-still-required-b89803b38b84 3. [gist.github.com] Compare Redis‑based distributed locks with traditional DB row locks — https://gist.github.com/ChinmayKuJena/7c0c883ca5b0bf99f70e2b7e4c2ca019 4. [www.alibabacloud.com] Implementation Principles and Best Practices of Distributed Lock — https://www.alibabacloud.com/blog/implementation-principles-and-best-practices-of-distributed-lock_600811 5. [martin.kleppmann.com] How to do distributed locking — https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html 6. [dev.to] Explain Redlock in Depth — https://dev.to/lazypro/explain-redlock-in-depth-31jj

관련 글 추천

  • https://infobuza.com/2026/06/16/20260616-j39a35/
  • https://infobuza.com/2026/06/16/20260616-aaduwr/

FAQ

분산 환경에서 DB 락(Row-level Lock)을 사용할 때 발생하는 주요 문제는 무엇인가요?

DB 락은 트랜잭션에 종속되어 있어 락을 유지하는 동안 DB 커넥션을 계속 점유해야 합니다. 특히 외부 API 호출이나 사용자의 대기 시간처럼 '사람의 시간'이 개입되어 락 유지 시간이 길어지면, 커넥션 풀 고갈(Connection Pool Exhaustion)로 인해 시스템 전체가 마비될 수 있습니다.

Redis 분산 락이 DB 락보다 유리한 점은 무엇인가요?

락의 제어권을 DB 밖으로 분리하여 DB 커넥션 점유를 최소화할 수 있으며, 저지연 읽기/쓰기를 통해 오버헤드가 적습니다. 또한 TTL(Time-To-Live) 설정을 통해 프로세스 장애 시에도 락이 자동으로 해제되어 '좀비 락'으로 인한 시스템 마비를 방지할 수 있습니다.

Redis로 락을 구현할 때 SETNX와 EXPIRE를 따로 호출하면 안 되는 이유는 무엇인가요?

두 명령을 따로 호출할 경우, SETNX로 락을 획득한 직후 EXPIRE를 호출하기 전에 서버가 다운되면 만료 시간이 설정되지 않은 락이 영원히 남게 되어 데드락 상황이 발생할 수 있습니다. 따라서 SET 명령의 옵션을 통해 원자적(Atomic)으로 처리해야 합니다.

Redis 분산 락 사용 시 비즈니스 로직 수행 시간이 TTL보다 길어지면 어떻게 되나요?

상호 배제 원칙이 깨지게 됩니다. 락이 먼저 만료되어 다른 클라이언트가 락을 획득할 수 있기 때문입니다. 이를 해결하기 위해 락 연장(Lock Renewal) 메커니즘이나 Redisson 라이브러리의 Watchdog 같은 기능을 사용하여 락 시간을 자동으로 늘려주어야 합니다.

데이터 무결성과 정확성이 절대적으로 중요한 시스템에서는 어떤 도구를 사용하는 것이 좋나요?

Redlock은 클락 점프나 복제 지연으로 인한 한계가 있으므로, 정확성이 최우선인 금융 거래 등의 시스템에서는 ZooKeeper나 etcd 같은 합의 알고리즘(Raft, Paxos) 기반의 시스템을 사용하는 것이 권장됩니다.

보조 이미지 1

보조 이미지 2

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다