Fencing Token이 만료된 분산 락 소유자의 쓰기를 차단하는 이유
분산 환경에서 데이터 정합성을 지키는 핵심 열쇠 펜싱 토큰
현대적인 분산 시스템은 여러 서버가 동시에 데이터를 처리하며 높은 가용성과 처리 성능을 확보합니다. 하지만 여러 서버가 동일한 자원을 공유하면 데이터 충돌 가능성도 함께 커집니다. 이를 방지하기 위해 분산 락을 사용하지만, 락을 획득했다는 사실만으로 모든 쓰기의 안전성이 보장되는 것은 아닙니다.
특히 락 소유자가 일시적으로 멈췄다가 락 만료 후 다시 작업을 이어가는 상황에서는 서로 다른 서버가 자신이 정당한 락 소유자라고 판단할 수 있습니다. 이때 오래된 작업의 쓰기를 차단하기 위해 사용하는 값이 펜싱 토큰(Fencing Token)입니다.
분산 락의 한계와 문제가 발생하는 이유
분산 락은 여러 서버 가운데 하나만 특정 자원에 접근하도록 조정하는 장치입니다. 데이터베이스 레코드를 수정하거나 파일을 생성할 때 다른 서버가 동시에 작업하지 못하도록 막는 데 사용됩니다.
하지만 분산 환경에서는 네트워크 단절, 서버 과부하, 긴 가비지 컬렉션 정지와 같은 상황이 발생할 수 있습니다. 서버 A가 락을 획득한 뒤 긴 GC 정지에 빠져 락을 갱신하지 못했다고 가정해 보겠습니다.
- 서버 A가 락을 획득하고 작업을 시작합니다.
- 서버 A가 멈춘 사이 락의 유효 시간이 만료됩니다.
- 서버 B가 새로운 락을 획득하고 같은 자원에 작업합니다.
- 서버 A가 다시 실행되며 자신이 락을 잃었다는 사실을 모른 채 쓰기를 시도합니다.
이 경우 락 관리자는 이미 서버 B에게 락을 넘겼더라도 서버 A가 외부 저장소로 보내는 요청까지 직접 차단할 수 없습니다. 장시간 멈춘 클라이언트가 복구된 뒤 오래된 요청을 실행할 수 있다는 점이 분산 락만 사용할 때의 중요한 한계입니다.
펜싱 토큰이 동작하는 원리
펜싱 토큰은 락이 새로운 소유자에게 할당될 때마다 증가하는 번호입니다. 락 관리자는 단순히 락 획득 성공 여부만 반환하지 않고, 이전 소유자보다 큰 토큰을 함께 발급합니다.
- 서버 A가 락을 획득하면 토큰
1을 받습니다. - A의 락이 만료된 뒤 서버 B가 락을 획득하면 토큰
2를 받습니다. - 모든 쓰기 요청에는 발급받은 토큰이 포함됩니다.
- 최종 저장소는 지금까지 처리한 가장 큰 토큰보다 작은 요청을 거부합니다.
서버 B의 토큰 2가 저장소에 반영된 뒤 서버 A가 토큰 1로 쓰기를 요청하면, 저장소는 이를 만료된 소유자의 작업으로 판단해 거부합니다. 락 소유자를 단조 증가하는 토큰으로 정렬하고 외부 저장소가 그 순서를 검증하는 것이 펜싱 토큰의 핵심입니다.
다만 토큰을 요청에 첨부하는 것만으로는 충분하지 않습니다. 토큰 비교와 실제 데이터 변경이 하나의 원자적 작업으로 처리돼야 합니다. 검증과 쓰기가 따로 실행되면 두 요청 사이에 다른 작업이 끼어들어 다시 경쟁 상태가 발생할 수 있습니다.
펜싱 토큰을 효과적으로 활용하는 실무 팁
저장소의 조건부 쓰기 지원 여부 확인
펜싱 토큰은 락 관리자보다 실제 데이터가 기록되는 최종 저장소에서 검증해야 합니다. 데이터베이스라면 버전 컬럼과 조건부 UPDATE를 사용하고, 외부 스토리지라면 버전 또는 세대 번호를 비교하는 API가 필요합니다.
저장소가 토큰을 검증할 수 없다면 펜싱 토큰을 발급해도 오래된 요청을 차단할 수 없습니다.
단조 증가하는 토큰 사용
새로운 락 소유자가 이전 소유자보다 항상 큰 값을 받아야 합니다. 서버의 현재 시간을 토큰으로 사용하면 시계 보정이나 서버 간 시간 차이 때문에 순서가 뒤바뀔 수 있습니다.
ZooKeeper의 순차 노드처럼 일관된 순서를 제공하는 조정 시스템이나, 선형화 가능한 카운터를 이용해 토큰을 발급하는 편이 안전합니다. ZooKeeper의 순차 노드는 생성 순서에 따라 증가하는 번호를 부여합니다.
거부된 요청의 처리 방식 설계
낮은 토큰으로 쓰기가 거부되었다면 같은 요청을 그대로 반복해서는 안 됩니다. 클라이언트는 락을 다시 획득하고 최신 데이터를 조회한 뒤, 현재 상태를 기준으로 작업을 재계산해야 합니다.
결제 요청이나 외부 API 호출처럼 되돌리기 어려운 작업에는 펜싱 토큰 외에도 멱등성 키, 상태 머신 또는 트랜잭셔널 아웃박스 같은 중복 실행 방지 장치가 필요합니다.
흔히 하는 오해와 진실
분산 락만 있으면 데이터 충돌이 모두 해결된다
분산 락은 같은 시점의 작업을 조정하지만, 이미 만료된 소유자가 뒤늦게 보내는 요청까지 최종 저장소에서 자동으로 막아주지는 않습니다. 펜싱 토큰은 이러한 오래된 요청을 구분하기 위한 추가적인 안전장치입니다.
토큰을 발급하기만 하면 안전하다
토큰을 생성해 로그에 남기는 것만으로는 의미가 없습니다. 최종 저장소가 이전에 처리한 토큰을 기억하고, 더 낮은 값의 요청을 원자적으로 거부해야 실제 보호 효과가 생깁니다.
분산 락과 펜싱 토큰의 비교 분석
| 구분 | 분산 락 | 펜싱 토큰 |
|---|---|---|
| 주요 목적 | 동시 작업 조정 | 오래된 소유자의 쓰기 차단 |
| 주요 처리 주체 | Redis, ZooKeeper 등 락 관리자 | 데이터베이스나 외부 저장소 |
| 판단 기준 | 현재 락 보유 여부 | 지금까지 처리한 토큰의 순서 |
| 추가 요구 사항 | 획득·갱신·해제 로직 | 원자적인 토큰 검증과 조건부 쓰기 |
두 기술은 서로 대체하는 관계가 아닙니다. 분산 락이 작업의 동시 실행을 줄인다면, 펜싱 토큰은 락이 만료되거나 요청 순서가 뒤바뀐 상황에서도 오래된 작업이 최신 데이터를 덮어쓰지 못하도록 막습니다.
실제 시스템에 적용할 때의 설계 전략
펜싱 토큰은 데이터베이스의 낙관적 잠금과 비슷한 원리를 갖지만 완전히 동일한 기술은 아닙니다. 낙관적 잠금은 데이터 버전을 비교해 충돌을 감지하고, 펜싱 토큰은 락 소유권이 부여된 순서를 이용해 만료된 작업을 구분합니다.
모든 작업에 무조건 적용할 필요는 없습니다. 데이터 손상이 치명적인 결제, 재고 차감, 리더 전용 작업, 파일 덮어쓰기 등에 우선 적용하고, 중복 실행을 허용할 수 있는 통계나 로그 수집 작업에는 더 단순한 방법을 선택할 수 있습니다.
자주 묻는 질문과 답변
펜싱 토큰을 구현하려면 데이터베이스 스키마를 변경해야 하나요?
관계형 데이터베이스에서 구현한다면 토큰 또는 버전 값을 보관할 컬럼을 추가하는 경우가 많습니다. 다만 저장소가 자체적인 조건부 쓰기나 버전 비교 기능을 제공한다면 별도 컬럼 없이 구현할 수도 있습니다.
Redis의 Redlock을 사용하면 펜싱 토큰이 필요 없나요?
Redlock도 락의 유효 시간 안에서 소유권을 관리하는 방식이므로, 장시간 멈춘 작업이 락 만료 후 다시 실행되는 문제를 자동으로 제거하지는 않습니다. Redis 공식 문서도 실행 시간이 길거나 정합성이 중요한 작업에서는 펜싱 토큰을 고려해야 한다고 안내합니다.
토큰 번호가 계속 증가하면 고갈되지 않나요?
일반적인 시스템에서 64비트 정수를 사용하면 현실적인 운영 기간에 값이 고갈될 가능성은 매우 낮습니다. 다만 토큰을 초기화하거나 이전 값보다 작은 번호를 다시 발급하는 동작은 피해야 합니다.
분산 시스템의 실패를 전제로 한 안전장치
분산 환경에서는 서버가 느린 것인지 완전히 중단된 것인지 즉시 구분하기 어렵습니다. 따라서 락이 만료됐다는 사실만으로 이전 서버가 더 이상 작업하지 않는다고 단정해서는 안 됩니다.
펜싱 토큰은 새로운 락 소유자에게 더 높은 순서를 부여하고, 최종 저장소가 오래된 요청을 거부하도록 만듭니다. 분산 락이 접근 순서를 조정하는 1차 장치라면, 펜싱 토큰은 예측할 수 없는 지연 이후에도 데이터가 이전 상태로 덮어쓰이지 않도록 보호하는 마지막 방어선입니다.
댓글 0
첫 댓글을 남겨보세요.