한양과학기술고등학교

에듀테크 프로젝트 대시보드

램포트 시계 & 벡터 클락

램포트 시계 & 벡터 클락

분산 시스템에서 "무엇이 먼저 일어났는가?"를 벽시계 없이 판단하는 논리적 시계

한눈에 — 여러 대의 컴퓨터는 각자 자기 시계를 본다. 그런데 그 시계들은 미세하게 어긋나 있어서 "A 컴퓨터의 이 사건이 B 컴퓨터의 저 사건보다 먼저"라고 시각만으로 단정할 수 없다. 대신 사건이 일어날 때마다 카운터를 하나씩 올리고, 메시지를 보낼 때 그 값을 같이 실어 보낸다. 받는 쪽은 자기 값과 비교해 더 크게 맞춘다. 이렇게 만든 논리적 시계로 사건들의 인과 순서를 알아낸다.

1. 왜 벽시계로는 안 되는가

모든 컴퓨터의 수정 진동자(clock)는 온도·전압에 따라 조금씩 빠르거나 느리게 간다. 이것을 클럭 드리프트(clock drift)라 하고, 그 결과 생기는 시계 차이를 클럭 스큐(clock skew)라 한다. NTP로 주기적으로 맞춰도 수 ms~수십 ms의 오차는 남는다. 네트워크 지연은 그보다 크고 들쭉날쭉하다.

서버 A
14:03:07.180
기준
서버 B
14:03:07.146
−34 ms 느림
서버 C
14:03:07.221
+41 ms 빠름

서버 B에서 주문 생성이 14:03:07.150에, 서버 C에서 재고 차감이 14:03:07.130에 찍혔다고 하자. 타임스탬프만 보면 재고 차감이 먼저다. 하지만 실제로는 주문이 재고 차감을 유발했을 수도 있다. 시각 비교로는 인과관계를 알 수 없다.

2. "먼저 일어났다"의 정확한 정의 — happens-before (→)

레슬리 램포트는 1978년, 시각 대신 인과관계로 순서를 정의했다. 사건 a → b ("a가 b보다 먼저 일어났다")는 다음 세 경우다:

규칙내용
① 같은 프로세스한 프로세스 안에서 a가 b보다 먼저 실행됐다면 a → b
② 메시지a가 메시지 보내기, b가 그 메시지 받기라면 a → b
③ 이행성a → b이고 b → c이면 a → c

어느 쪽으로도 →가 성립하지 않으면 두 사건은 동시(concurrent), 기호로 a ∥ b. "같은 시각"이라는 뜻이 아니라 서로 영향을 줄 수 없었다는 뜻이다.

3. 램포트 시계 — 사건마다 +1

각 프로세스가 정수 카운터 L 하나를 갖는다.

상황동작
내부 사건 / 메시지 보내기L = L + 1
메시지 보낼 때메시지에 L 값을 함께 실어 보냄
메시지 받을 때L = max(L, 받은 값) + 1

아래 타임라인에서 프로세스 3개가 메시지를 주고받는다. 다음 단계를 눌러 사건이 하나씩 일어나는 것을 지켜보자. 각 점 위/아래 숫자가 그 사건의 논리적 시계 값이다.

내부 사건 메시지 보내기 메시지 받기 → 메시지 전달
사건프로세스종류램포트 L벡터 클락 (P1,P2,P3)

4. 램포트 시계의 한계

램포트 시계는 "a → b 이면 L(a) < L(b)"를 보장한다. 하지만 역은 성립하지 않는다. 즉 L(a) < L(b)라고 해서 a가 b의 원인이라는 보장은 없다 — 그냥 서로 무관한 동시 사건인데 숫자만 다를 수 있다.

위 타임라인에서 e10(P3의 마지막 내부 사건)과 e9(P1의 메시지 받기)를 보자. 램포트로는 L(e10)=3 < L(e9)=7 이라 "e10이 먼저"처럼 보이지만, 두 사건 사이엔 어떤 메시지 경로도 없다. 실제로는 동시(∥)다. 스칼라 하나로는 이 둘을 구분할 수 없다.

5. 벡터 클락 — 프로세스마다 칸을 따로

프로세스가 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
  • 둘 다 아님 (서로 어떤 칸은 크고 어떤 칸은 작음)  →  a ∥ b (동시)

스칼라와 달리 벡터는 "비교 불가"라는 상태를 표현할 수 있고, 그게 바로 동시성이다.

6. 두 사건 비교기

타임라인의 두 사건을 골라 관계를 확인해 보자. 램포트 값만으로 판단했다면 틀렸을 경우를 함께 표시한다.

vs

7. 어디에 쓰나

  • Amazon DynamoDB / Riak — 같은 키에 대한 두 갱신의 벡터를 비교해, 한쪽이 다른 쪽을 포함하면 최신값 채택, 동시면 충돌(conflict)로 보고 양쪽 버전을 모두 보관해 앱이 병합하게 한다.
  • 협업 편집기 (Google Docs, Figma) — 각 편집 연산의 인과 순서를 벡터/버전 벡터로 추적해, 네트워크 순서가 뒤바뀌어 도착해도 같은 최종 문서가 되도록 한다.
  • 인과적 일관성(causal consistency) — SNS에서 "댓글"이 "원글"보다 먼저 보이지 않도록, 메시지에 벡터를 붙여 원인 사건이 반영된 뒤에만 결과 사건을 적용한다.
  • 분산 시스템 디버깅 — 여러 서버 로그를 모아 실제 인과 순서대로 재구성할 때.

단점: 프로세스가 많아지면 벡터 길이도 그만큼 길어진다. 그래서 노드 수가 유동적인 대규모 시스템에서는 버전 벡터·점 버전 벡터(dotted version vector) 같은 변형을 쓴다.

8. 코드로 보기

# 프로세스 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

핵심 한 줄 — 스칼라(램포트) 시계는 "순서"를 주고, 벡터 클락은 거기에 더해 "동시성"까지 구분한다.