에듀테크 프로젝트 대시보드
램포트 시계 & 벡터 클락
분산 시스템에서 "무엇이 먼저 일어났는가?"를 벽시계 없이 판단하는 논리적 시계
모든 컴퓨터의 수정 진동자(clock)는 온도·전압에 따라 조금씩 빠르거나 느리게 간다. 이것을 클럭 드리프트(clock drift)라 하고, 그 결과 생기는 시계 차이를 클럭 스큐(clock skew)라 한다. NTP로 주기적으로 맞춰도 수 ms~수십 ms의 오차는 남는다. 네트워크 지연은 그보다 크고 들쭉날쭉하다.
서버 B에서 주문 생성이 14:03:07.150에, 서버 C에서 재고 차감이
14:03:07.130에 찍혔다고 하자. 타임스탬프만 보면 재고 차감이 먼저다. 하지만 실제로는 주문이
재고 차감을 유발했을 수도 있다. 시각 비교로는 인과관계를 알 수 없다.
레슬리 램포트는 1978년, 시각 대신 인과관계로 순서를 정의했다. 사건 a → b
("a가 b보다 먼저 일어났다")는 다음 세 경우다:
| 규칙 | 내용 |
|---|---|
| ① 같은 프로세스 | 한 프로세스 안에서 a가 b보다 먼저 실행됐다면 a → b |
| ② 메시지 | a가 메시지 보내기, b가 그 메시지 받기라면 a → b |
| ③ 이행성 | a → b이고 b → c이면 a → c |
어느 쪽으로도 →가 성립하지 않으면 두 사건은 동시(concurrent),
기호로 a ∥ b. "같은 시각"이라는 뜻이 아니라 서로 영향을 줄 수 없었다는 뜻이다.
각 프로세스가 정수 카운터 L 하나를 갖는다.
| 상황 | 동작 |
|---|---|
| 내부 사건 / 메시지 보내기 | L = L + 1 |
| 메시지 보낼 때 | 메시지에 L 값을 함께 실어 보냄 |
| 메시지 받을 때 | L = max(L, 받은 값) + 1 |
아래 타임라인에서 프로세스 3개가 메시지를 주고받는다. 다음 단계를 눌러 사건이 하나씩 일어나는 것을 지켜보자. 각 점 위/아래 숫자가 그 사건의 논리적 시계 값이다.
| 사건 | 프로세스 | 종류 | 램포트 L | 벡터 클락 (P1,P2,P3) |
|---|
램포트 시계는 "a → b 이면 L(a) < L(b)"를 보장한다. 하지만 역은 성립하지 않는다.
즉 L(a) < L(b)라고 해서 a가 b의 원인이라는 보장은 없다 — 그냥 서로 무관한
동시 사건인데 숫자만 다를 수 있다.
e10(P3의 마지막 내부 사건)과 e9(P1의 메시지 받기)를 보자.
램포트로는 L(e10)=3 < L(e9)=7 이라 "e10이 먼저"처럼 보이지만, 두 사건 사이엔
어떤 메시지 경로도 없다. 실제로는 동시(∥)다. 스칼라 하나로는 이 둘을 구분할 수 없다.
프로세스가 N개면, 각자 길이 N짜리 벡터 V를 갖는다.
V[i]는 "프로세스 i에서 일어난 사건을 내가 몇 개나 알고 있는가"를 뜻한다.
| 상황 | 동작 |
|---|---|
| 프로세스 i에서 사건 발생 | V[i] = V[i] + 1 |
| 메시지 보낼 때 | 벡터 V 전체를 함께 실어 보냄 |
| 메시지 받을 때 | 칸마다 V[k] = max(V[k], 받은 V[k]) → 그다음 V[i] += 1 |
위 타임라인의 토글을 벡터 클락으로 바꾸면 각 점의 라벨이 (a,b,c) 형태로 바뀐다.
V(a) ≤ V(b) = 모든 칸에서 V(a)[k] ≤ V(b)[k]
V(a) < V(b) (≤ 이고 서로 다름) → a → b (a가 원인)V(b) < V(a) → b → a스칼라와 달리 벡터는 "비교 불가"라는 상태를 표현할 수 있고, 그게 바로 동시성이다.
타임라인의 두 사건을 골라 관계를 확인해 보자. 램포트 값만으로 판단했다면 틀렸을 경우를 함께 표시한다.
단점: 프로세스가 많아지면 벡터 길이도 그만큼 길어진다. 그래서 노드 수가 유동적인 대규모 시스템에서는 버전 벡터·점 버전 벡터(dotted version vector) 같은 변형을 쓴다.
# 프로세스 i (전체 N개) 의 벡터 클락 class VectorClock: def __init__(self, i, N): self.i = i self.V = [0] * N def local_event(self): self.V[self.i] += 1 def send(self): self.V[self.i] += 1 return list(self.V) # 메시지에 벡터 사본을 실어 보냄 def receive(self, other): for k in range(len(self.V)): self.V[k] = max(self.V[k], other[k]) self.V[self.i] += 1 def happens_before(a, b): # a → b ? return all(x <= y for x, y in zip(a, b)) and a != b def concurrent(a, b): # a ∥ b ? return not happens_before(a, b) and not happens_before(b, a) and a != b
핵심 한 줄 — 스칼라(램포트) 시계는 "순서"를 주고, 벡터 클락은 거기에 더해 "동시성"까지 구분한다.