분산 락의 소유자가 바뀌었다는 사실만으로 늦게 도착한 이전 작업자의 요청이 사라지지는 않습니다. 펜싱은 소유권 세대를 단조 토큰으로 표현하고, 실제 권위 저장소가 그 토큰을 변경과 같은 원자 경계에서 비교하도록 만드는 설계입니다. 임대 만료·토큰·요청 ID·도메인 version이 각각 무엇을 판정하는지 먼저 나누면 중복과 오래된 쓰기를 다른 문제로 진단할 수 있습니다.
임대 만료와 옛 프로세스 실행 지속
워커 A가 10초짜리 임대를 얻고 보고서를 계산하다가 30초 동안 멈춥니다. 락 서비스는 임대가 끝났으므로 B에게 새 소유권을 줍니다. B가 새 보고서를 저장한 뒤 A가 깨어나 옛 보고서를 저장하면 어떻게 될까요? 락 서비스에는 한 소유자만 있어도 실제 저장소에는 두 워커의 쓰기가 도착합니다.
일정 시간 뒤 권한을 넘길 수 있게 하는 임대(lease)는 죽은 소유자가 자원을 영원히 잡는 문제를 줄입니다. 하지만 살아 돌아온 옛 작업자의 쓰기까지 차단하려면 실제 변경 지점의 검사가 필요합니다. 오래된 소유권의 작업을 저장소가 거절하는 장치가 펜싱(fencing)입니다.
새 소유권과 단조 증가 토큰
| 순서 | 락 서비스 | 작업자 | 저장소 |
|---|---|---|---|
| 1 | A에게 토큰 11 발급 | A 계산 시작 | 최대 토큰 10 |
| 2 | A 임대 만료 | A는 여전히 정지 | 최대 토큰 10 |
| 3 | B에게 토큰 12 발급 | B 계산·저장 요청 | 12와 새 결과를 함께 저장 |
| 4 | B 소유권 유지 | A가 토큰 11로 늦게 저장 | 11이 12보다 작아 거절 |
그림을 글로 읽기
- 옛 워커 A: token 11 · 늦은 결과
- 새 워커 B: token 12 · 새 결과
- 권위 저장소: maxToken=12 · 토큰 검사와 변경을 원자 실행
- 옛 워커 A → 권위 저장소: 11 거절
- 새 워커 B → 권위 저장소: 12 수락
A가 저장 직전에 “내 락이 아직 유효한가?”를 조회해도, 조회 뒤 멈출 수 있습니다. 확인과 쓰기가 떨어져 있는 한 같은 틈이 남습니다. 임대 시간을 길게 잡는 것은 실패 가능성과 복구 지연을 바꾸는 운영 선택이지, 이 논리적 틈을 없애는 증명은 아닙니다.
임대 갱신 요청의 확인 응답(ACK)이 네트워크에서 사라지면, 제공자는 갱신을 반영했어도 작업자는 성공 여부를 모를 수 있습니다. 이때는 새 작업을 보수적으로 막고, 이미 외부에 제출한 효과가 처리됐는지 조회해 남은 정리를 끝냅니다.
한 프로세스 안의 경과 시간은 같은 단조 시계로 잴 수 있지만, 서버 간 임대가 겹치지 않는다는 보장은 시계 진행률(clock rate) 차이·프로세스 정지·네트워크 지연·토큰 발급 프로토콜을 별도로 가정합니다. 재부팅 전 단조 시각이나 다른 노드의 숫자를 서로 비교해 lease를 임의로 연장하지 않습니다.
토큰 검사와 결과 변경의 원자 경계
실무 선택은 락 서비스의 lease 성공률이 아니라 모든 권위 쓰기 지점이 같은 fencing 비교를 실행할 수 있는지로 결정합니다. 진단할 때는 token 발급 시각, 저장소의 maxToken, requestId·fingerprint, 조건 갱신 결과를 함께 기록합니다. B의 12가 저장된 뒤 A의 11이 성공했다면 사전 lease 조회가 아니라 저장소 원자 경계가 빠진 결함입니다.
다음은 같은 토큰으로 여러 요청을 허용하되 요청 ID로 중복 효과를 막는 저장소의 모형입니다. maxToken, 결과, 요청 기록이 같은 트랜잭션 경계에 있고 토큰이 신뢰된 발급기로부터 왔다고 가정합니다.
write(resource, token, requestId, fingerprint, payload):
begin transaction
row = lock_resource(resource)
if token < row.maxToken:
rollback; return stale_owner
old = lookup_request(resource, requestId)
if old exists:
require old.fingerprint == fingerprint
row.maxToken = max(row.maxToken, token)
commit
return old.result
validate_domain_preconditions(row, payload)
apply_change(row, payload)
row.maxToken = token
record_request(resource, requestId, fingerprint, result)
commit
return result
실제 구현은 같은 자원의 모든 쓰기가 같은 잠금·조건 갱신을 통과해야 합니다. 별도 조회로 토큰을 검사한 뒤 조건 없는 UPDATE를 보내면 그 사이 토큰 12가 들어오고 11이 나중에 덮을 수 있습니다. 실패 시 부분 변경을 남기지 않도록 하고, 같은 토큰 안의 요청 순서가 필요하면 기대 버전이나 순번도 추가합니다.
토큰은 소유권 세대, 요청 ID는 한 논리 작업을 나타냅니다. token=12라고 해서 B의 모든 쓰기를 같은 요청으로 합치면 안 됩니다. 반대로 매번 새로운 requestId를 만들면 같은 작업의 재전송을 중복으로 판별할 수 없습니다.
저장소의 새 토큰 반영 전후
임대 서비스가 11에서 12로 넘겼더라도 저장소의 maxToken이 아직 10이면, 저장소에 먼저 도착한 토큰 11은 통과할 수 있습니다. 단조 번호가 막아 주는 시점은 새 번호 12가 실제 저장 지점의 maxToken에 반영된 뒤이며, 그 이후에는 11이 12의 결과를 덮지 못합니다. 발급 순간부터 옛 소유자의 쓰기를 막아야 한다면, 토큰을 발급하는 일과 각 저장소에 새 권한을 등록하는 일을 더 강하게 연결해야 합니다.
UUID는 서로 다름을 나타낼 수 있지만 어느 소유권이 더 새로운지 순서를 주지 않습니다. 시스템 시각만 사용하면 시계 역행·동점·노드 차이가 문제가 됩니다. 내구적으로 증가하는 번호나 내구 세대와 카운터 조합이 필요하고, 모든 저장소가 같은 비교 규칙을 사용해야 합니다.
재시작·복구와 다중 저장소의 펜싱 한계
상태를 추적할 때 token=12가 발급된 것과 각 집행 지점의 최대 토큰이 12가 된 것은 별도 사건입니다. DB에는 12가 기록됐지만 객체 저장소에는 A가 만든 고정 경로 파일이 남아 있다면 DB 결과만 검사한 펜싱으로는 충분하지 않습니다. 고유 버전 파일과 권위 포인터를 사용하면 파일 생성과 게시를 분리할 수 있지만, 메일·결제 같은 외부 효과는 조회·보상·멱등 계약으로 따로 닫아야 합니다.
발급기가 백업에서 복원되어 5부터 다시 발급하면 저장소는 정상 새 워커도 오래됐다고 거절할 수 있습니다. 반대로 저장소의 최대 토큰이 과거로 돌아가면 오래된 토큰을 다시 받아들일 수 있습니다. 발급기와 집행 지점의 복구 기준을 함께 정해야 합니다.
예를 들어 DB 행에는 토큰 검사를 걸었지만 객체 저장소의 report.pdf 같은 고정 경로를 조건 없이 덮어쓰면, A의 늦은 파일 쓰기가 B의 DB 결과와 섞일 수 있습니다. 그래서 파일은 먼저 고유한 버전 경로에 만들고, DB에서는 토큰 검사를 통과한 파일 참조만 권위 포인터로 게시하게 합니다. 독자는 고정 경로가 아니라 그 포인터만 읽고, 더 이상 가리키지 않는 파일은 별도로 정리합니다. 그래도 외부 메일 발송이나 결제 호출까지 DB와 같은 원자 거래가 되지는 않습니다.
| 장치 | 맡는 책임 | 맡지 않는 책임 |
|---|---|---|
| 임대 만료 | 실패한 소유권의 재획득 | 옛 프로세스 강제 종료 |
| 펜싱 토큰 | 더 오래된 소유자의 쓰기 거절 | 같은 소유자의 요청 중복 |
| 요청 ID | 같은 논리 작업의 중복 효과 방지 | 새 소유권의 순서 |
| 도메인 버전 | 같은 소유권 안의 상태 충돌 | 외부 저장소 전체 원자화 |
락 해제와 소유권 검증의 원자성
락을 해제할 때 키 이름만으로 삭제하면 A가 B의 새 락을 지울 수 있습니다. 현재 소유권 값이 자신이 받은 값과 같은지 검사하고 삭제하는 동작을 원자적으로 수행합니다. 저장 토큰과 임대 식별자의 역할을 혼동하지 않습니다.
A를 저장 직전에 정지시키고 B가 토큰 12를 저장한 뒤 A를 재개합니다. A의 요청이 실제 저장소에서 거절되는지 확인합니다. 이어서 B 발급 후 아직 저장 전 A가 쓰는 사례, 발급기·저장소 복원, 동일 requestId 재전송, 다른 저장소의 조건 없는 쓰기를 시험합니다. 락 획득 성공률이 아니라 최종 데이터와 거절된 옛 쓰기의 근거가 검사 기준입니다.