이번 글에서는 분산 락 시스템에 대해 다룬다. 특히, 분산 환경에서 데이터의 일관성을 지키는 핵심 시스템인 분산 락을 제공하는 미들웨어인 Google의 Chubby와 Apache의 ZooKeeper를 중심으로 작성한다.
다음과 같은 순서로 진행된다.
먼저, 분산 락의 개념과 필요성을 운영체제 시간에 배웠던 세마포어와 같은 OS 락을 떠올려보며 짚어본다.
이어서 핵심 시스템인 Chubby와 ZooKeeper가 각각 분산 합의라는 안전망 위에서 장애 처리와 효율성이라는 두 가지 핵심 과제를 어떤 메커니즘으로 해결했는지 살펴본다.
마지막으로 또 다른 분산 락 시스템으로 현업에서 많이 쓰이는 Redis 락 방식과의 차이를 비교해보면서 실질적인 기술 선택 기준을 살펴보고, 내용 정리하면서 마무리하도록 하겠다.
먼저, 분산 락이란 무엇일까? 이는 운영체제 시간에 배웠던 상호 배제(Mutual Exclusion) 원칙을 네트워크 환경, 즉 분산 환경으로 확장한 시스템이다. 개념과 목표는 세마포어 같은 OS 락과 동일하다. 하지만 그 작용 범위가 달라진 것이라 할 수 있다.
분산 락은 단일 서버의 운영체제 커널 내부라는 경계를 넘어, 분산 데이터베이스 환경에서 여러 서버가 공유 데이터베이스나 파일 같은 공유 자원에 접근할 때 정합성을 보장해준다.
예를 들어, 여러 서버가 하나의 데이터베이스 테이블에 쓰기 작업을 하거나, 공유 파일 시스템의 설정값을 업데이트할 때, 오직 하나의 서버만이 그 순간에 작업을 진행하도록 통제하는 역할을 분산 락이 수행한다. 이를 통해 데이터의 일관성(Consistency)을 유지하는 것이 핵심이다.
만약 이 제어에 실패하면, 동시에 여러 서버가 데이터를 수정하여 데이터가 꼬일 때, 경합 조건, Race Condition이 발생하게 된다.
즉, 둘 이상의 프로세스나 스레드가 공유된 데이터에 동시에 접근하여 조작할 때, 접근 순서에 따라 실행 결과가 달라지거나 예측할 수 없게 되는 상황이 되어 일관성을 보장할 수 없게 된다.
오늘 우리가 다룰 모든 복잡한 메커니즘은 바로 이 단순한 원칙을 분산 네트워크 환경에서도 지키기 위해 고안되었다고 보면 되겠다.
OS에서 사용하는 락 방식인 세마포어를 잠깐 살펴보자.
상호 배제 원칙을 통해 단일 서버의 OS 커널 내부에서 완벽하게 구현된다. CPU 코어가 아무리 많아도, 모든 스레드가 하나의 메모리와 하나의 커널이라는 안전망 안에서 작동한다.
이러한 OS 환경에서는 락을 잡은 스레드에 문제가 생겨도 OS 커널이라는 강력하고 신뢰할 수 있는 안전망이 프로세스나 스레드가 죽었을 때 대부분의 커널 리소스와 락을 회수하여 정리한다.
즉, 로컬 환경에서는 안전성(Safety)이 보장된다고 할 수 있겠다.
하지만 공유의 경계가 메모리에서 네트워크로 확장되는 분산 환경에서는 이 안전망이 사라진다.
OS 락만으로는 다른 컴퓨터의 동작을 제어할 권한이 없다.
우리가 락을 획득하는 순간, 그 락의 안전은 네트워크의 신뢰성에 의존하게 된다. 이 작동 범위의 한계와 신뢰 문제 때문에 분산 락이 필요하게 되는 것이다.
즉, 분산 락이 필요한 근본적인 이유는 네트워크 환경의 불안정성 때문이다.
락을 가진 서버가 응답이 없을 때 락을 가진 서버가 정말 다운된 건지, 아니면 네트워크가 일시적으로 끊긴 것인지 다른 서버들이 절대로 확신할 수 없고 시스템을 마비시키게 된다.
이 불확실성을 해결하지 못하면, 락을 해제하지 않고 기다리는 동안 시스템 전체가 멈추는 영구적인 데드락이 발생한다. 그렇다고 섣불리 락을 해제하면, 죽지 않았던 서버가 다시 돌아와 동시에 자원을 사용하게 되어 데이터 정합성이 깨지게 되고 서비스의 신뢰도가 떨어지게 된다.
이러한 네트워크 때문에 발생하는 분산 환경 장애를 해결하기 위해 새로운 안전장치로 분산 락 시스템이 제안되었고 구글의 Chubby와 아파치의 ZooKeeper가 탄생하게 되었다.
분산 락 시스템의 선구자 격으로 Google에서 개발된 Chubby부터 살펴보도록 하자.
Chubby는 락을 가진 서버에서 장애가 발생할 때 락을 어떻게 안전하게 회수할까?라는 핵심 과제에 집중했다.
그 전에, Chubby를 신뢰할 수 있는 기반을 이해해야 한다. Chubby 클러스터는 분산 합의 알고리즘을 통해 자신의 락 상태 정보를 클러스터 내에서 일관되게 유지한다. 과반수 서버의 동의가 있어야만 락 상태가 기록된다. 이러한 배경 위에서 다음과 같은 메커니즘이 작동한다.
Chubby의 핵심은 세션(Session) 기반 임대 모델이라는 것이다.
클라이언트와 Chubby 서버는 세션을 맺고, 클라이언트는 주기적으로 KeepAlive RPC를 보내 자신이 살아있음을 증명해야 한다. 이 KeepAlive를 통해 세션의 유효성을 계속해서 갱신한다.
Chubby에서 락은 이 세션에 의존하는 임대(Lease) 모델인 것이다.
쉽게 말해 락 자체에 만료 시간이 있는 것이 아니라, 락을 잡고 있는 클라이언트의 세션에 락이 귀속되는 것이다.
따라서 KeepAlive가 끊겨 세션이 만료되면, 세션에 귀속된 모든 락의 임대도 함께 무효화된다.
세션 만료는 '클라이언트가 확실히 죽었다'가 아니라, '더 이상 안전하게 살아있다고 증명할 수 없다'는 의미이다.
Chubby는 이 불확실한 상태를 안전하지 않음으로 간주하고 강제로 락을 회수한다. 회수 과정은 락 서버가 직접 처리하며 다른 클라이언트의 간섭을 막아 안전하다.
이렇듯 Chubby는 세션을 활용하여 네트워크 불확실성을 관리한다.
Chubby는 Google의 분산 파일 시스템(GFS), Bigtable 등 내부 인프라를 위한 중앙 코디네이션 및 락 서비스로 설계되어 운영된 서비스이다.
구글에서 기술 논문은 공개했지만, 오픈 소스 프로젝트로 외부에 공개하거나 상용 서비스로 제공하지는 않았다.
Apache ZooKeeper는 Chubby의 원리를 참고하여 오픈 소스로 개발되었다. 또한 ZooKeeper 역시 분산 합의 프로토콜 위에서 작동한다.
ZooKeeper는 다수 서버의 동시 경쟁을 어떻게 공정하고 효율적으로 관리할까?에 집중했다.
ZooKeeper는 순번 대기표 방식을 통해 락을 기다리는 과정을 최적화했다. ZooKeeper 자체적으로 락 기능을 내장하고 있지는 않지만, 순차 임시 노드를 이용한 구현 패턴인 lock recipe를 공식 문서로 안내한다.
순차적 임시 노드를 사용해 락 요청 시 모든 서버가 ZooKeeper 내에 번호가 붙은 노드를 생성한다. 그리고 가장 작은 번호를 가진 노드가 락을 획득하게 된다. 마치 은행에서 공정하게 순번을 부여받는 것과 같고, 이를 통해 락 획득의 공정성을 확보한다.
또한, 이 노드는 임시 노드이기 때문에, 락을 가진 서버가 죽으면 ZooKeeper가 자동으로 노드를 제거하여 락을 회수한다. 이를 통해 세션 기반 안전성도 함께 가져가게 된다.
추가로, Watch 메커니즘으로 효율성을 극대화한다.
락을 얻지 못한 서버들은 무작정 재시도하며 네트워크 트래픽을 낭비하는 대신 자신의 바로 앞 순서 노드에만 Watch를 건다.
앞 노드가 해제되면 알림을 받고 다음 락을 획득하게 된다.
즉, 불필요한 통신을 최소화하는 효율적인 대기열을 구축했다고 볼 수 있다.
이러한 순차적 임시 노드와 Watch 메커니즘 덕분에, ZooKeeper는 분산 환경에서 불필요한 네트워크 통신을 최소화하면서도 질서 있는 락 대기열을 만들게 되었다.
분산 락을 제공하는 또다른 방법으로 Redis를 활용하는 방법이 있다.
Redis 락이 어떤 원리로 작동하는지 살펴보자. Redis는 SETNX (SET if Not eXists)라는 명령어를 핵심으로 사용한다. 이 명령어는 키가 존재하지 않을 때만 값을 설정하라는 의미를 가진 Redis 내에서 하나의 작업으로 처리되는 원자적(Atomic) 명령이다.
즉, 여러 서버가 동시에 SETNX lock_key 1 명령을 Redis에 보냈을 때, 오직 가장 먼저 도착한 명령 하나만 성공하여 락을 획득하고, 나머지는 실패한다. 이를 통해 상호 배제를 구현한다.
여기에 TTL(Time To Live), 즉 만료 시간을 반드시 설정해야 한다. Redis 락이 단일 서버 기준으로 매우 빠르고 단순하지만, 락을 획득한 서버가 죽었을 때 락을 해제하지 못하는 데드락 위험을 막기 위해서이다. 이 TTL 덕분에 락이 영원히 잡혀있지 않고, 시간이 지나면 자동으로 풀리게 된다.
그렇다면 우리는 ZooKeeper와 Redis 락 중 어떤 기술을 선택해야 할까?
ZooKeeper는 일관성(Consistency)을 보장할 수 있다는 것이 장점이고, Redis 락은 속도(Performance)가 빠르다는 장점이 있다.
일관성과 속도라는 트레이드오프를 가지고 있다고 할 수 있다.
각각의 트레이드오프를 좀 더 살펴보도록 하자.
ZooKeeper는 분산 합의 프로토콜 위에서 작동하는 반면, Redis는 그렇지 않다.
즉, ZooKeeper 클러스터는 과반수 투표를 통해 락 상태를 기록하기 때문에, 리더가 죽더라도 데이터 정합성을 잃지 않는다.
그러나, Redis는 합의 과정을 생략하고 단일 노드에 쓰기 때문에 안전성 문제가 발생한다.
만약 Redis 마스터 노드가 락을 설정한 직후 복제 지연 상태에서 죽어버리면, 새로운 마스터 노드는 락 정보 없이 올라와 동시 락 획득이라는 치명적인 정합성 오류가 발생할 수 있다.
Redis는 이 문제를 해결하기 위해 Redlock이라는 알고리즘을 제안했지만, 이 역시도 강한 분산 합의 알고리즘 수준의 안전성 증명이 부족하여, 분산 환경에서의 안전성 보장이 불완전하다는 논쟁이 여전히 진행 중이라고 한다.
결론적으로, ZooKeeper는 합의를 통해 정합성을 보장하는 반면, Redis는 속도를 위해 안전성을 부분적으로 희생하는 모델이라고 할 수 있겠다.
결국 선택의 기준은 잃을 데이터의 중요도이다. 결제 시스템과 같이 중복 판매가 절대 안 되는 작업에는 ZooKeeper처럼 강력한 안전장치가 필수이다. 반면, 간단한 캐시 락 등 속도가 중요한 곳에는 Redis가 선호된다. 우리는 서비스의 요구사항에 따라 이 Trade-off를 고려해 사용할 기술을 선택하면 되겠다.
티켓팅 시스템을 예시를 들어보도록 하자.
저희가 포도알을 클릭하면서 '이미 선택된 좌석입니다'와 같은 팝업을 수시로 보게되는, 수많은 클라이언트가 좌석을 선점하는 과정은 대량의 트래픽을 빠르게 처리할 필요가 있다.
따라서 이 과정에는 일관성을 조금 포기하더라도 속도가 빠른 Redis 락을 활용할 수 있다.
반면, 저희가 티켓을 결제하고 최종 재고를 차감하는 과정에서는 오류가 발생해서는 안된다. 따라서 이러한 과정에는 Zookeeper 등의 분산 합의 기반 락 시스템을 활용할 수 있겠다.
지금까지 살펴본 분산 락 시스템은 기존 OS에서 배웠던 상호 배제 원칙을 적용해 분산 환경에서 네트워크 장애를 해결한다.
Chubby와 ZooKeeper는 분산 합의 알고리즘을 기반으로 하며, Chubby는 세션 기반 임대로 장애 시 안전한 회수를, ZooKeeper는 순차 노드로 공정하고 효율적인 락 관리를 가능하게 했다. 해당 메커니즘들 덕분에 우리는 분산 환경에서도 데이터 일관성을 지킬 수 있게 되었다.
이러한 분산 락 시스템을 잘 활용해 분산 환경에서 효과적으로 백엔드 설계를 해보도록 하자.
'Newly I Learned' 카테고리의 다른 글
| Memcached (0) | 2025.11.05 |
|---|---|
| 분산 컴퓨팅, RAID의 한계를 넘어 분산 플랫폼의 시대로(대본) (0) | 2025.11.05 |
| NoSQL, Cassandra (0) | 2025.11.05 |
| 모놀리식 아키텍쳐(MA)의 한계와 마이크로서비스 아키텍쳐(MSA)의 등장 (0) | 2025.10.12 |
| 왜 Entity를 그대로 반환하면 안 될까? – 백엔드 계층 분리의 이유 (3) | 2025.05.22 |