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라는 관계를 정의했다. 세 가지 규칙이다.
- 같은 노드 안의 두 사건 A, B가 순서대로 일어났으면 A → B (A가 B보다 먼저).
- A가 메시지를 보냈고 B가 그 메시지를 받았으면 A → B.
- A → B이고 B → C이면 A → C (전이적).
- 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 (일관성과 합의)
'Software Architecture > Distributed Systems' 카테고리의 다른 글
| Distributed Systems - 11. 서비스 디스커버리·게이트웨이 (0) | 2026.07.20 |
|---|---|
| Distributed Systems - 10. 일관적 해싱 (0) | 2026.07.20 |
| Distributed Systems - 08. 멱등성·정확히 한 번 (0) | 2026.07.20 |
| Distributed Systems - 07. 분산 트랜잭션 (0) | 2026.07.20 |
| Distributed Systems - 06. 복제 패턴 (0) | 2026.07.20 |