Software Architecture/Distributed Systems

Distributed Systems - 09. 시간·순서·클럭

시간, 순서, 클럭 — 분산 시스템에서 "먼저"를 정하는 법

2018년 한 분산 데이터베이스에서 두 사용자가 거의 동시에 같은 계좌에서 출금했다. 한 요청은 서울 노드에, 다른 요청은 부산 노드에 도착했다. 두 노드는 각자 자기 요청을 먼저 처리했다고 기록했다. 왜냐 — 두 노드의 시계가 7ms 차이가 났기 때문이다. 서울 노드의 시계가 10:00:00.007이었고, 부산 노드의 시계가 10:00:00.000이었을 때, 두 요청의 실제 도착 시간이 정확히 같았다면 — 두 노드는 서로 다른 "먼저"를 기록한다.

어느 쪽이 "진짜 먼저"인가. 이 질문에 답하지 못해 시스템은 임의로 한쪽을 골랐다가 사용자 불만을 받았다. 모놀리스였다면 — 단일 시계가 "이 쪽이 1ms 빨랐다"라고 결정했을 것이다. 분산에선 시계가 여러 개라 "먼저" 자체가 모호하다. 이 글은 분산 시스템에서 "무슨 일이 먼저 일어났나"를 정의하는 문제와 해법을 다룬다.

비유로 감 잡기 — 시계가 다른 호텔

방마다 시계가 따로 있는 호텔을 상상하자. 101호 시계는 10:00을 가리키고, 102호 시계는 10:05를 가리킨다. 어느 쪽이 "정확"한지 모른다. 둘 다 자기가 맞다고 주장한다.

이때 101호에서 "10:01에 화재 경보 울렸다"는 기록이 있고, 102호에서 "10:03에 화재 경보 꺼졌다"는 기록이 있다. 어느 게 먼저인가. 101호 시계 기준으로는 경보 울림이 먼저, 102호 시계 기준으로는 경보 꺼짐이 먼저다. 시계가 다르면 "먼저" 자체가 의미 없어진다.

이게 분산 시스템이 직면한 문제의 본질이다. 각 노드가 자기 시계를 갖고, 그 시계는 미세하게 안 맞는다. "무슨 일이 먼저 일어났나"를 시계로 판단하면 — 어느 노드의 시계를 믿느냐에 따라 답이 달라진다.

비유의 한계 — 호텔 비유는 '고정된 시계'를 가정한다. 분산 시스템의 시계는 크리스탈 발진기의 물리적 한계로 미세하게 빨리 가거나 느리게 감 (clock drift). 또한 시계 자체가 거짓말을 할 수도 있다 (NTP 서버 오류). 비유는 '시계 안정성'에서 무너진다.

"먼저"란 — 시간 순서와 인과 순서

이 문제를 풀려면 — "먼저"가 뭔지부터 명확히 해야 한다. 두 종류가 있다.

시간 순서 — 물리적 시간 기준. 사건 A가 사건 B보다 "시간적으로 먼저"인가. 시계로 잰 순서다. 모놀리스에선 단일 시계가 이걸 정한다. 분산에선 시계가 여러 개라 정의가 모호해진다.

인과 순서 — 사건 사이의 영향 관계 기준. A가 B에 영향을 줬으면 A가 "인과적으로 먼저"다. A가 메시지를 보냈고 B가 그 메시지를 받았으면 — A가 B보다 먼저다. 이건 시계 없이도 정의할 수 있다. A의 영향이 B에게 닿았으니 B는 A 이후인 게 확실하다.

분산 시스템에선 — 시간 순서는 정확히 정하기 어렵지만(시계가 안 맞으니까), 인과 순서는 가능하다. 이 통찰이 다음 절들의 핵심이다.

가장 단순한 형태 — 물리 시계 (NTP)

가장 단순한 접근 — 모든 노드의 시계를 NTP로 동기화한다. 다 같은 시간을 가지면 "시간 순서"로 "먼저"를 판단할 수 있다.

문제는 — 완벽한 동기화는 물리적으로 어렵다. 세 가지 이유.

동기화 불완전. NTP는 인터넷으로 동기화하지만, 네트워크 지연 때문에 수 ms~수십 ms 오차가 난다. 데이터센터 내부 LAN이어도 1ms 미만의 오차. 이 오차가 — 두 사건이 1ms 이내에 일어나면 어느 쪽이 먼저인지 못 끊는다는 의미다.

크리스탈 발진기 드리프트. 하드웨어 시계는 미세하게 빨리 가거나 느리게 간다. 하루에 수 초씩 어긋날 수 있어 주기적 동기화가 필요. 동기화 사이에 어긋난 시간만큼 오차가 누적된다.

윤초 (leap second). 지구 자전 속도 변화로 추가되는 1초. 분산 시스템에선 이 1초 때문에 노드들이 다른 시간을 갖고 혼란이 생김. 2012년 윤초로 Java, Cassandra, Linux 커널이 잠시 멈춘 사태가 유명하다.

그래서 — 물리 시계만으로 "정확한 먼저"를 정의하긴 부족하다. 빠르고 단순하지만 부정확하다.

가장 정확한 형태 — TrueTime (Google Spanner)

반대 극단. Google이 Spanner(2012)에서 소개한 TrueTime은 — 시계가 틀릴 수 있음을 인정하고, 그 오차를 명시적으로 모델링한다.

TrueTime API는 시간을 점이 아니라 구간으로 반환한다. "현재 시간이 [earliest, latest] 구간 안에 있다." 이 구간의 폭이 — 시계의 불확실성이다. Google 데이터센터에선 GPS 안테나와 원자 시계를 각 데이터센터에 두고, 이를 통해 불확실성 구간을 수 ms로 좁힌다.

class TrueTimeAPI {
    // 현재 시간이 [earliest, latest] 구간 안에 있다
    Interval now() {
        // 일반 클라우드에선 이 구간이 수십 ms
        // Google 데이터센터에선 GPS/원자 시계로 수 ms
    }

    // 시간 t가 확실히 지났는지
    boolean after(Timestamp t) {
        return now().earliest() > t;
    }
}

트랜잭션을 커밋할 때 — 커밋 타임스탬프를 할당하고, 그 타임스탬프가 TrueTime 불확실성 구간을 벗어날 때까지 기다린다(commit wait). 그 후에야 커밋을 확정. 이렇게 하면 — 다른 트랜잭션이 이 트랜잭션과 동시적이라고 오판할 일이 없다. 강한 일관성(linearizability, 03편)을 물리 시계로 보장.

TrueTime의 비밀은 — GPS 안테나와 원자 시계라는 특수 하드웨어. 일반 클라우드 환경에선 불가능한 인프라. 그래서 Spanner는 Google Cloud 밖에서 재현하기 어렵다. CockroachDB 같은 후속 시스템은 TrueTime 없이 흉내내려고 혼합 시계(hybrid clock)를 쓴다.

그 사이 — 논리 시계 (Lamport, 벡터)

양극단 사이에 — 물리 시계에 의존하지 않고도 "먼저"를 정의하는 접근이 있다. 논리 시계. 시계 자체를 안 쓰고, 사건 사이의 인과적 순서만 기록한다.

happens-before — 인과 순서의 정의

1978년 Leslie Lamport는 "Time, Clocks, and the Ordering of Events in a Distributed System"에서 happens-before라는 관계를 정의했다. 세 가지 규칙이다.

  1. 같은 노드 안의 두 사건 A, B가 순서대로 일어났으면 A → B (A가 B보다 먼저).
  2. A가 메시지를 보냈고 B가 그 메시지를 받았으면 A → B.
  3. A → B이고 B → C이면 A → C (전이적).
  4. A → B도 아니고 B → A도 아니면 — A와 B는 동시적(concurrent)이다.

핵심 통찰 — "실제 시간"이 아니라 인과 관계로 순서를 정한다. 시계가 필요 없다. 인과적으로 연결된 사건들만 순서를 갖고, 연결되지 않은 사건들은 "동시적"으로 둔다.

flowchart LR
    subgraph NodeA["노드 A"]
        A1[사건 A1] --> A2[사건 A2]
    end
    A2 -->|메시지 송신| B1
    subgraph NodeB["노드 B"]
        B1[사건 B1] --> B2[사건 B2]
    end
    A2 -.happens-before.- B1
    C1[사건 C1<br/>노드 C] -.독립적/동시적.- A2
    C1 -.독립적/동시적.- B2

Lamport 시계

각 노드는 단조 증가하는 카운터를 유지한다.

class LamportClock {
    private long counter = 0;

    // 로컬 이벤트
    synchronized long tick() {
        return ++counter;
    }

    // 메시지 수신
    synchronized long receive(long senderTimestamp) {
        counter = Math.max(counter, senderTimestamp) + 1;
        return counter;
    }
}

Lamport 시계의 속성 — A → B이면 clock(A) < clock(B). 인과적 순서가 보장되면 타임스탬프도 그 순서를 반영한다. 역은 성립 안 함clock(A) < clock(B)라고 해서 반드시 A → B인 건 아니다 (동시적 사건이 우연히 다른 타임스탬프를 가질 수 있음). 이게 Lamport 시계의 한계 — 순서를 전체 순서로 못 만든다.

벡터 시계 — 동시성 감지

이 한계를 보완한 게 벡터 시계(Mattern 1989, Fidge 1987 독립 발견). 각 노드마다 카운터를 둔 벡터를 쓴다. N개 노드면 N차원 벡터.

class VectorClock {
    private final Map<NodeId, Long> clock = new HashMap<>();

    synchronized Map<NodeId, Long> tick() {
        clock.merge(self, 1L, Long::sum);
        return Map.copyOf(clock);
    }

    synchronized Map<NodeId, Long> receive(Map<NodeId, Long> senderClock) {
        senderClock.forEach((node, ts) -> 
            clock.merge(node, ts, Long::max));
        clock.merge(self, 1L, Long::sum);
        return Map.copyOf(clock);
    }
}

벡터 시계의 장점 — 동시성을 감지한다. 두 사건이 인과적 순서가 없으면 (어느 쪽도 다른 쪽에 영향을 안 줬으면) 동시적이라 판단. Dynamo, Riak 같은 리더 없는 저장소가 충돌 감지에 벡터 시계를 쓴다. 두 쓰기가 동시적이면 — 충돌이고, 클라이언트나 자동 규칙(LWW, CRDT)으로 해결한다.

단점 — 노드 수만큼 차원이 늘어난다. N=100이면 100차원 벡터. 큰 클러스터에선 비효율적.

세 접근 비교

접근 의존 정확도 비용 적합
물리 시계 (NTP) 하드웨어 시계 + NTP 낮음 (수~수십 ms 오차) 낮음 대부분의 시스템 (오차 감수)
논리 시계 (Lamport) 없음 (시계 안 씀) 인과 순서만 중간 부분 순서가 필요한 시스템
벡터 시계 없음 인과 순서 + 동시성 감지 중간 (차원 = 노드 수) 충돌 감지 (Dynamo, Riak)
TrueTime GPS + 원자 시계 높음 (수 ms) 높음 (특수 하드웨어) 글로벌 강한 일관성 (Spanner)

설계 사례 — 동시 편집 충돌 해결

구글 문서, Notion, Figma 같은 동시 편집 시스템에서 — 두 사용자가 같은 문서를 동시에 수정하면 누가 먼저인가를 결정해야 한다. Lamport 시계로는 부족하고 (동시성 감지 못 함), 벡터 시계가 자연스럽다.

class CollaborativeDoc {
    private final Map<OperationId, VectorClock> opClocks = new HashMap<>();

    void applyRemote(Operation op, VectorClock remoteTs) {
        Ordering order = VectorClockComparison.compare(remoteTs, localClock);
        switch (order) {
            case BEFORE -> {
                // 원격이 내 이전 — 이미 적용된 것, 무시
            }
            case AFTER -> {
                // 원격이 내 이후 — 그냥 적용
                applyOp(op);
                localClock.receive(remoteTs);
            }
            case CONCURRENT -> {
                // 동시적 — 충돌 해결 필요
                resolveConflict(op, remoteTs);
            }
        }
    }

    void resolveConflict(Operation op, VectorClock remoteTs) {
        // OT (Operational Transformation) 또는 CRDT로 병합
        Operation merged = otTransform(op, lastLocalOp());
        applyOp(merged);
        localClock.receive(remoteTs);
    }
}

벡터 시계가 충돌을 감지하고, 그 후의 해결(OT, CRDT)은 별개의 기술. 시간/순서는 충돌을 알아차리게 해줄 뿐, 해결까지는 안 한다.

시간/순서 위반/오용 감지

신호 의미
시스템이 System.currentTimeMillis()로 분산 순서 판단 물리 시계 동기화 한계 — 어긋날 수 있음
분산 캐시의 TTL이 노드마다 다르게 만료 시계 드리프트
동시 쓰기 감지 없이 LWW만 쓰기 사용자 데이터 손실 가능
벡터 시계 크기가 무한히 커짐 노드 수 증가에 따른 압축 부재
TrueTime 같은 강한 시계 없이 linearizability 주장 부정확한 순서 판단
분산 잠금 타임아웃이 노드마다 다름 잠금이 두 노드에서 동시 활성

시간, 순서, 그리고 CAP

시간 문제는 CAP(02편)와 깊이 연결된다.

  • 강한 일관성(linearizability) 은 "실제 시간 순서"를 요구한다. 이건 합의(04편) 또는 TrueTime 같은 강한 시계가 필요.
  • 최종 일관성 은 시간 순서를 느슨하게 다룬다. 벡터 시계로 인과 순서만 보장해도 충분.
  • 동시성 자체가 "순서가 없는 두 사건"이므로, 동시성을 허용하는 시스템(리더 없는, eventual)은 시간 엄격성을 덜 요구한다.

그래서 — 일관성 모델(03편)의 강도와 시간 처리의 정교함은 비례한다. 강한 일관성을 원할수록 더 강한 시계가 필요하고, 그건 더 큰 비용(성능, 복잡성, 인프라)을 의미한다.

다음으로 — 회복탄력성

시간·순서가 "무슨 일이 일어났나"를 다뤘다면, 12편(회복탄력성)은 "일이 안 일어났을 때 (장애) 어떻게 대응하나"를 다룬다. 서킷 브레이커, 벌크헤드, 재시도, 부하 제거 — 분산 시스템의 장애를 코드로 다루는 패턴. 01편(분산의 8가지 오해)에서 시작된 "네트워크는 신뢰할 수 없다"를 실용적으로 다루는 도구들이다.

시간·순서의 핵심 통찰 — 분산 시스템에선 "지금"과 "먼저"가 단일한 의미를 갖지 않는다. 물리 시계는 틀리고, 논리 시계는 인과만 보장하고, 동시성은 "순서가 없는" 상태로 존재한다. 이 점을 인정하고 — 어느 정도의 순서 보장이 필요한지(강한 일관성 vs eventual)에 따라 시계 기술을 선택하는 게 분산 시스템 설계의 한 기둥이다.


참고

  • Lamport — "Time, Clocks, and the Ordering of Events in a Distributed System" (CACM, 1978), happens-before·Lamport 시계 원 논문 — 접근 2026-07-20
  • Mattern — "Virtual Time and Global States of Distributed Systems" (1989), 벡터 시계 독립 발견 — 접근 2026-07-20
  • Fidge — "Timestamps in Message-Passing Systems That Preserve the Partial Ordering" (1987), 벡터 시계 독립 발견 — 접근 2026-07-20
  • Corbett et al. — "Spanner: Google's Globally-Distributed Database" (OSDI 2012), TrueTime 원 논문 — 접근 2026-07-20
  • Kulkarni et al. — "Logical Time for Distributed Systems" (synthesis lecture, 2012) — 접근 2026-07-20
  • Kleppmann — (O'Reilly, 2017), Ch.8 (분산 시스템의 어려움)·Ch.9 (일관성과 합의)