[DB] 분산 환경에서의 데이터 정합성

HenryHong·2026년 1월 12일

DB

목록 보기
15/18
post-thumbnail

동시성 제어는 분산 환경의 개발자를 가장 지치게 만드는 문제다. 이 글에서는 “Redis 락/Redlock/Redisson을 써라” 같은 처방보다, 왜 그런 선택을 하게 되는지를 단계별로 짚어본다.


1. 왜 Redis로 락을 거는가

단일 서버 환경이라면 synchronized, ReentrantLock 같은 JVM 수준 락만으로도 충분하다. 하지만 서버가 여러 대로 늘어나면, 각 인스턴스의 메모리 락은 서로를 볼 수 없어서 더 이상 유효하지 않다.

이때 필요한 것은 모든 서버가 공통으로 바라볼 수 있는 외부 저장소다.

  • DB(RDB) 락
    • SELECT ... FOR UPDATE 같은 행 단위 락은 여전히 가장 강한 정합성을 보장한다.
    • 다만 트랜잭션 범위를 벗어난 작업(외부 API 호출, 여러 서비스에 걸친 작업)을 제어하기에는 한계가 있고, 락을 남발하면 DB가 병목이 되기 쉽다.
  • 파일 시스템 락
    • 공유 파일 시스템(NFS 등)을 전제로 해야 하고, 레이턴시·운영 복잡도가 크다.
  • Redis 락
    • In‑Memory 기반이라 외부 공용 저장소 중에서는 매우 빠른 편이고,
    • SETNX + TTL(만료시간)을 이용하면 비교적 간단하게 분산 락을 구현할 수 있다.
    • 단, 네트워크를 거치므로 로컬 메모리 락보다는 당연히 느리다는 점은 전제해야 한다.

결국 Redis는 “여러 서버가 동시에 바라보는 빠른 외부 상태 저장소”이기 때문에 분산 락의 대표적인 선택지가 된다.


2. 단일 Redis 락의 한계: SPOF와 복제 지연

Redis 한 대에만 락을 거는 방식은 구현이 쉽지만, 그 Redis 노드가 락 시스템의 단일 장애 지점(SPOF) 이 된다.

문제 시나리오를 몇 가지 보자.

  1. 단일 노드 장애
    • 락 정보를 가지고 있던 Redis가 죽으면, 락 상태가 순식간에 사라진다.
    • 클라이언트 입장에서는 “락이 풀렸다”고 오해하고 동일 자원에 대해 중복 작업을 수행할 수 있다.
  2. Replication 지연
    • 마스터에 락을 쓰고, 슬레이브로 복제되기 전에 마스터가 죽으면,
    • 새로운 마스터는 그 락을 모른 채 다른 클라이언트에게 다시 락을 내줄 수 있다.
    • 결과적으로 서로 다른 두 클라이언트가 같은 리소스를 동시에 점유하게 된다.

즉, “단일 Redis + 복제” 조합만으로는 강한 정합성을 보장하기 어렵고, 여기서 등장하는 것이 Redlock 알고리즘이다.


3. Redlock: 다수결로 락을 인정하는 방식

Redlock은 “Redis 한 대는 믿지 말자”는 전제에서 출발한다. 여러 개의 독립된 Redis 노드에 락을 분산해서 걸고, 다수결(과반수) 로 락의 유효성을 판단한다.

핵심 아이디어는 다음과 같다.

  1. 독립된 Redis 노드 여러 대 구성 (보통 5대)
    • 서로 다른 서버/가용 영역에 배치된 5개의 Redis 인스턴스를 준비한다.
  2. 각 노드에 동일한 키로 락 시도
    • 클라이언트는 5개의 노드에 SETNX key value PX ttl을 순차적으로 시도한다.
    • 각 시도에는 짧은 타임아웃을 두어, 한 노드 장애로 전체가 멈추지 않게 한다.
  3. 과반수 성공 + 유효 시간 검증
    • 5개 중 3개 이상에서 락 획득에 성공하면 락을 얻은 것으로 간주한다.
    • 이때 락을 얻기까지 걸린 전체 시간을 TTL에서 뺀 “실제 유효 시간”이 충분히 남아 있는지 확인하고, 그 시간 안에 비즈니스 로직을 수행한다.
  4. 락 해제도 모든 노드에 대해 수행
    • 락을 얻을 때 사용한 동일한 value를 가진 노드들에 대해 DEL을 호출해 락을 해제한다.

이렇게 하면 Redis 노드 한두 대가 죽더라도, 나머지 노드들이 과반수를 유지하는 한 락 시스템은 계속 동작할 수 있다.

Redlock에 대한 논쟁도 있다

다만 여기서 끝이 아니다.

  • 분산 시스템 연구자인 Martin Kleppmann은, 시계 드리프트 / 긴 GC / 네트워크 파티션 상황에서는 Redlock이 강한 분산 락으로 간주되기 어렵다고 비판했다.
  • Redis 저자(antirez)는 “현실적인 가정 하에서는 안전하게 쓸 수 있다”고 반박했지만,
    이 논쟁 때문에 “금융급 강한 정합성”이 필요한 곳에서는 ZooKeeper, etcd 같은 합의 시스템을 쓰자는 의견도 여전히 강하다.

즉, Redlock은 “실용적인 분산 락 패턴”이지만 절대 무오류의 만능 설계는 아니다라는 점을 글에 명시해 두는 것이 좋다.


4. Redisson: Java에서 분산 락을 안전하게 쓰기 위한 표준 도구

Redlock이든 단일 Redis 락이든, 직접 구현하면 두 가지 지옥이 기다린다.

  1. 락 만료 시간(lease)을 어떻게 관리할 것인가
  2. 락 대기 중 CPU를 어떻게 아끼면서 기다릴 것인가

Java 진영에서 가장 널리 쓰이는 해답이 Redisson이다.

4‑1. Watchdog: 락 TTL 자동 연장

  • Redisson에서 RLock.lock() (leaseTime 미지정) 을 사용하면, 내부적으로 Watchdog(감시견) 이 활성화된다.
  • 락의 TTL은 기본적으로 lockWatchdogTimeout(기본 30초)으로 설정되고, 락을 잡고 있는 스레드가 살아있는 동안 주기적으로 TTL을 갱신한다.
  • 프로세스가 정상적으로 돌아가는 한 “락이 중간에 풀려서 정합성이 깨지는” 상황을 많이 줄여 준다. 반대로 프로세스가 죽으면 더 이상 연장되지 않고 TTL이 지나면서 자동 해제된다.

4‑2. Pub/Sub 기반 대기: Spin Lock을 피하기

  • Redis에 계속 SETNX를 때리면서 락을 기다리는 방식(Spin Lock)은 CPU, 네트워크 낭비가 심하다.
  • Redisson은 Pub/Sub를 활용해,
    • 락이 이미 점유된 상태라면 “락이 풀렸다는 이벤트”를 구독하고,
    • 이벤트를 받기 전까지 스레드를 대기 상태로 둔다.
  • waitTime, leaseTime 등을 옵션으로 주어, “얼마나 기다릴지 / Watchdog 없이 얼마 동안만 점유할지”를 세밀하게 조정할 수 있다.

5. 실무에서 반드시 고려해야 할 엣지 케이스

분산 락을 도입한다고 해서 모든 동시성 문제가 마법처럼 사라지지는 않는다. 오히려 새로운 복잡성이 생긴다.

  1. 시계 불일치(Clock Drift)
    • 노드 간 시스템 시간이 어긋나면 TTL 계산에 오차가 생길 수 있다.
    • 모든 서버와 Redis 노드는 NTP로 주기적인 시계 동기화가 필수다.
  2. GC로 인한 Stop‑The‑World
    • JVM이 긴 GC를 수행하는 동안 락을 연장하지 못해 TTL이 만료될 수 있다.
    • 락을 잡은 후에는 임계 구역을 짧게 유지하고, 락 안에서 대형 객체 생성이나 긴 블로킹 호출을 피하는 것이 좋다.
  3. 네트워크 파티션
    • 일부 노드만 서로 보이는 상황에서는 “과반수 확보” 자체가 어려워질 수 있다.
    • 이 경우 재시도 전략, 타임아웃, 실패 시 폴백(사용자에게 에러 반환 등)을 명확히 설계해야 한다.

추가로, 락을 비즈니스 로직의 만능 제어 장치로 쓰지 않는 것도 중요하다.

  • 가능하면
    • DB 트랜잭션
    • 낙관적 락
    • Idempotency 키
    • 메시지 큐를 통한 직렬화

같은 패턴으로 먼저 해결하고 정말 어쩔 수 없는 경우에만 분산 락을 마지막 안전장치로 사용하자.


6. 어떤 도메인에서 무엇을 쓸 것인가

도메인 특성에 따라 선택은 달라져야 한다.

  • 일반적인 웹 API, 관리 화면, 통계 배치
    • 단일 Redis 노드 + Redisson 기본 락으로도 충분한 경우가 많다.
    • 장애 시 “잠깐 재시도하면 된다” 수준의 업무라면 굳이 Redlock까지 가지 않아도 된다.
  • 재고, 주문, 포인트, MES 등 정합성이 최우선인 도메인
    • 먼저, 가능하면 RDB 트랜잭션 + 낙관적 락 으로 해결할 수 없는지 검토한다.
    • 그래도 분산 락이 필요하다면
      • Redisson + Redlock 구성을 쓰거나
      • ZooKeeper, etcd 같은 합의 시스템(Quorum 기반 락)도 함께 후보에 올려 본다.
    • 어떤 방식을 선택하든, 네트워크 장애·시계 드리프트·프로세스 장애 시의 동작을 사전에 시뮬레이션해 두는 것이 필수다.

7. 정리

  • 분산 환경의 동시성 제어는 “락을 하나 더 건다”가 아니라,
    어느 수준에서 어떤 실패를 감수할 것인가를 설계하는 문제다.
  • Redis 락 → Redlock → Redisson으로 이어지는 단계는,
    • 단일 노드 락의 한계
    • 다중 노드 환경에서의 정합성
    • 개발자가 직접 처리하기 어려운 만료·대기 로직을 라이브러리에게 넘기기 위한 자연스러운 진화 과정이다.

이 흐름을 이해하고 나면, “우리 서비스에 정말 Redlock이 필요한가?”, “이 부분은 DB 트랜잭션으로 충분한가?” 같은 질문에 훨씬 더 분명하게 답할 수 있게 된다.

[참고]
https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/
https://liashchynskyi.net/posts/mastering-distributed-locks-redlock-mutexes-in-multi-server-setup
https://www.postgresql.org/docs/current/sql-update.html
https://dev.to/lazypro/explain-redlock-in-depth-31jj
https://hojongs.github.io/posts/redis-redlock-algorithm-and-java-implementation-and-criticism/

profile
주니어 백엔드 개발자

0개의 댓글