20장. 불확실성을 인정한 시계 — TrueTime과 Spanner

만화로 보는 요약 — 먼저 읽어보세요
점이 아닌 시간 구간 → 불확실성이 지나갈 때까지 기다리는 commit-wait → 대기 비용으로 사는 전역 순서와 외부 일관성.
면접 실전 질문: ① TrueTime이 단일 시각 대신 TTinterval을 반환하는 이유는? ② commit-wait은 외부 일관성을 어떻게 보장하는가? ③ 전용 하드웨어 없이 비슷한 보장을 얻으려는 대안은 무엇인가?
시각을 하나의 점 대신 폭을 가진 구간으로 답하고, 그 구간이 지나가길 잠깐 기다리는 것만으로 전역 순서를 사는 것 — Spanner가 찾은, 아무도 시간을 정하지 못하는 세계에서 순서를 세우는 세 번째 길이다.
면접 실전 질문: ① TrueTime의
TT.now()가 단일 타임스탬프 대신TTinterval을 반환하는 이유와, 그 구간의 폭 ε(엡실론)은 무엇을 뜻하는가? ② commit-wait이 외부 일관성(external consistency)을 보장하는 원리는 무엇인가? ③ GPS·원자시계 같은 전용 하드웨어 없이 비슷한 보장을 얻으려는 시스템은 어떤 대안을 쓰는가?
배경 — 벡터 클록이 답하지 못한 두 가지
19장은 벡터 클록으로 두 이벤트가 인과관계인지 동시적인지 정확히 구별하는 법을 보여줬습니다. 하지만 19장의 마지막 질문이 짚었듯, 벡터 클록은 두 가지를 끝내 답하지 못합니다. V(a)를 아무리 들여다봐도 a가 실제로 몇 시 몇 분에 일어났는지는 알 수 없고, 두 이벤트가 정말 동시적이라고 판명 났을 때 그중 어느 걸 앞에 둘지 정할 근거도 벡터 자체엔 없습니다. 인과관계의 유무는 벡터가 완벽하게 풀었지만, 시각과 동시적인 두 이벤트 사이의 전역 순서는 그대로 남은 숙제입니다.
17장으로 거슬러 올라가면 이 숙제가 왜 생겼는지 다시 보입니다. 어떤 두 서버도 완전히 같은 시각을 가리킨 적이 없다는 것 — NTP는 그 어긋남을 줄일 뿐 지우지 못했고, 그래서 두 기계가 찍은 타임스탬프는 애초에 비교 대상이 아니었습니다. 18장과 19장은 이 결론을 받아들이고 시각을 아예 포기하는 쪽으로 갔습니다. 램포트 카운터와 벡터 클록 둘 다 “몇 시에”가 아니라 “무엇이 무엇보다 먼저인가”만 답하는 시계였고, 그 대가로 실제 시각과의 연결을 끊었습니다.
그런데 정말 시각을 완전히 버려야만 할까요? 문제의 근원을 다시 보면, 시각을 버리게 만든 진짜 원인은 “시계가 어긋난다”는 사실 자체가 아니라 그 어긋남의 크기를 아무도 말해주지 않는다는 데 있었습니다. 17장의 타임스탬프는 그저 숫자 하나였습니다. 그 숫자에 오차가 얼마나 실려 있는지는 어디에도 적혀 있지 않았죠. 시계가 “지금은 정확히 몇 시”라고 우기는 대신 “지금은 이 구간 어딘가”라고 오차까지 함께 답한다면 — 그 오차의 크기를 알고 있는 한, 시각을 완전히 버리지 않고도 순서를 셀 방법이 생기지 않을까요?
이건 이 책 전체가 지나온 길의 요약이기도 합니다. 하늘이 시간을 정하고, 나라가 표준시로 통일하고, 원자시계가 초를 정의했습니다. 그런데 분산 시스템에서는 아무도 시간을 정하지 못했습니다. 18~19장은 그 사실을 받아들이고 시각 자체를 포기했습니다. 이번 장이 시도하는 건 세 번째 길입니다 — 정하지 못한다는 사실을 받아들이되, 그 정하지 못함의 크기를 측정 가능한 값으로 만드는 것.
스토리 — 지금이 아니라, 지금이 속한 구간
Spanner, 그리고 시간을 다시 데려온 이유
2012년, 구글은 논문 “Spanner: Google’s Globally-Distributed Database”(Corbett 외, OSDI 2012)를 통해 전 세계 데이터센터에 데이터를 복제하면서도 트랜잭션 순서를 전역적으로 보장하는 데이터베이스를 공개했습니다. Spanner가 보장하려던 성질은 단순한 순서가 아니라 외부 일관성(external consistency) 이었습니다 — 한 트랜잭션 T1이 실제로 커밋을 마친 뒤에야 다른 트랜잭션 T2가 시작됐다면, 어떤 클라이언트가 어느 리전에서 관찰하든 T1의 커밋 타임스탬프는 반드시 T2의 커밋 타임스탬프보다 작아야 한다는 보장입니다. Google Cloud의 공식 문서는 이를 단일 객체의 원자적 읽기·쓰기를 보장하는 선형성(linearizability)보다 더 강한 성질로 규정합니다 — 선형성이 객체 하나의 이야기라면, 외부 일관성은 여러 행·여러 파티션에 걸친 트랜잭션 전체의 이야기이기 때문입니다.
문제는 이걸 보장하려면 결국 전 세계에 흩어진 서버들이 매긴 타임스탬프를 신뢰할 수 있어야 한다는 것이었습니다. 17장이 이미 확인했듯, 이건 일반적인 벽시계로는 불가능한 요구입니다. 그래서 Spanner 팀이 만든 건 새로운 정렬 알고리즘이 아니라 새로운 시계 API였습니다 — TrueTime.
TrueTime — 점이 아니라 구간을 반환하는 시계
TrueTime의 핵심 호출은 TT.now()입니다. 그런데 이 함수는 우리가 익숙한 Instant.now()처럼 시각 하나를 돌려주지 않습니다. TTinterval, 즉 [earliest, latest]라는 구간 하나를 돌려줍니다. 이 구간에는 명시적인 보장이 붙습니다 — 실제 절대 시각은 반드시 이 구간 안에 있다. TT.now()를 부른 그 순간의 진짜 시각이 earliest보다 이르거나 latest보다 늦을 수는 없습니다.
이 구간 위에 두 개의 술어가 추가로 정의됩니다.
TT.after(t)—t가 확실히 지났으면 참. 현재 구간의earliest조차t보다 뒤일 때만 참입니다.TT.before(t)—t가 확실히 아직 오지 않았으면 참. 현재 구간의latest조차t보다 앞일 때만 참입니다.
두 술어 모두 “혹시 그럴 수도”가 아니라 “확실히” 를 요구한다는 점이 핵심입니다. 구간이 조금이라도 t와 겹치면 두 술어 다 거짓을 돌려줍니다 — 모르면 모른다고 답하는 시계인 셈입니다.
이 구간의 폭을 결정하는 값이 ε(엡실론) 입니다. 📄 문서 기반(미검증) — 2012년 논문을 요약한 다수의 2차 자료에 따르면 ε는 평균 약 4ms, 정상 상태에서 최대 약 7ms 수준으로 보고됩니다. 이 폭은 고정값이 아니라 톱니(sawtooth) 모양으로 오르내립니다. 각 서버의 타임슬레이브 데몬은 GPS·원자시계에 연결된 시간 마스터를 약 30초 간격으로 폴링해 자신의 로컬 시계를 다시 맞추는데, 다음 폴링까지 시간이 흐르는 동안엔 “이 로컬 시계가 최악의 경우 초당 200마이크로초까지 드리프트할 수 있다”는 보수적인 가정 아래 ε를 계속 키워갑니다. 30초 동안 이 드리프트가 누적되면 최대 약 6ms, 여기에 시간 마스터와의 통신 지연 약 1ms를 더해 최대 약 7ms — 그리고 다음 폴링이 성공하는 순간 ε는 다시 작은 값으로 뚝 떨어집니다. 이 오르내림이 반복되는 게 톱니 모양의 정체입니다.
하드웨어 — GPS와 원자시계, 일부러 다르게 고장 나도록
이 구간의 신뢰도는 결국 그 근거가 되는 하드웨어에 달려 있습니다. 각 데이터센터에는 두 종류의 시간 마스터가 배치됩니다 — 위성에서 직접 신호를 받는 GPS 마스터와, 위성 없이도 독자적으로 시각을 유지하는 원자시계 마스터입니다. 굳이 두 종류를 같이 두는 이유는 하나입니다. 두 하드웨어가 서로 다른 방식으로 고장 나기 때문입니다. GPS 수신기는 안테나 문제나 전파 방해로 신호를 잃을 수 있지만, 같은 사건이 원자시계의 발진에는 영향을 주지 않습니다. 반대로 원자시계는 전자적 결함이나 노화로 어긋날 수 있지만, 그런 결함이 GPS 신호 수신에 영향을 주지도 않습니다. 실패 원인이 서로 무관(uncorrelated)하도록 일부러 설계한 이중화인 셈입니다 — 한쪽이 무너져도 다른 쪽이 무너질 이유는 없습니다.
각 서버의 타임슬레이브 데몬은 여러 시간 마스터에 동시에 물어보고, 서로 어긋나는 답을 내놓는 마스터(거짓말하는 시계)를 걸러내면서 “지금 이 순간 진짜 시각이 있을 수 있는 범위”를 계산합니다. 그 계산의 결과가 바로 ε입니다.
핵심 — 대기가 사는 것: commit-wait과 외부 일관성
왜 구간만으로는 부족한가
여기까지만 보면 TrueTime은 그냥 “오차 범위가 표시된 시계”입니다. 그런데 오차 범위를 안다고 저절로 순서가 보장되진 않습니다. 두 트랜잭션이 각각 TT.now()로 커밋 타임스탬프를 받아갔는데 그 두 구간이 겹친다면 — 어느 쪽이 먼저 커밋됐는지는 여전히 아무도 모릅니다. 겹치는 구간에서 하나를 골라 “이게 더 이르다”고 우기는 순간, 17장에서 이미 실패를 본 last-write-wins로 되돌아가는 겁니다.
Spanner의 답은 한 발 더 기다리는 것입니다. 트랜잭션이 커밋 타임스탬프 s를 확정하면, 그 트랜잭션은 커밋을 완료(락 해제, 결과 공개)하기 전에 TT.after(s)가 참이 될 때까지 기다립니다. 이 대기가 commit-wait 입니다.
이 대기가 왜 순서를 보장하는지는 TT.after의 정의를 그대로 따라가면 보입니다. TT.after(s)가 참이라는 건 현재 구간의 earliest조차 s보다 뒤라는 뜻이고, 그건 곧 진짜 절대 시각이 이미 s를 확실히 지났다는 뜻입니다. 어느 관찰자가, 어느 리전에서, 자기 시계로 몇 시라고 재든 상관없이 — 진짜 시각 자체가 s를 지났다는 사실은 관찰자와 무관하게 참입니다. 그러니 이 트랜잭션이 커밋을 완료하고 다음 트랜잭션이 그 뒤에 시작한다면, 다음 트랜잭션이 받아갈 타임스탬프는 반드시 s보다 큰 값이 됩니다 — 두 트랜잭션의 구간이 겹칠 걱정 자체가 사라지는 겁니다. 대기가 사는 건 결국 이겁니다. “내 타임스탬프가 이미 과거라는 사실을, 그 누구의 시계로 재도 부정할 수 없는 상태” 를 시간이 흐르는 것으로 사버리는 것.
실측 — commit-wait이 있을 때와 없을 때
말로는 확신이 안 서니 직접 코드로 확인합니다. TrueTime 실제 하드웨어(GPS·원자시계)는 이 환경에 없으므로, TTinterval을 흉내 낸 시뮬레이션 클래스를 만들었습니다. 여기서 쓰는 ε=5ms는 논문이 측정한 값이 아니라 이 시연을 위해 고른 시뮬레이션 파라미터입니다 — 앞서 본 실제 Spanner의 평균 ε(약 4ms)와 얼추 비슷한 자릿수를 고른 것뿐, 실측치는 아닙니다.
public class TrueTimeSim {
// epsilon(입실론)은 Spanner 논문이 측정한 값이 아니라, 이 시뮬레이션이 고른 파라미터다.
// 실제 TrueTime의 epsilon은 GPS/원자시계 동기화 상태에 따라 매 순간 달라진다.
static final long EPSILON_MILLIS = 5;
static class TTInterval {
final long earliest;
final long latest;
TTInterval(long now, long epsilon) {
this.earliest = now - epsilon;
this.latest = now + epsilon;
}
@Override
public String toString() {
return "[earliest=" + earliest + ", latest=" + latest + "]";
}
}
static TTInterval now() {
return new TTInterval(System.currentTimeMillis(), EPSILON_MILLIS);
}
// t가 확실히 지났다 -- 현재 구간의 가장 이른 값(earliest)조차 t보다 뒤일 때만 참
static boolean after(long t) {
return now().earliest > t;
}
// t가 확실히 아직 오지 않았다 -- 현재 구간의 가장 늦은 값(latest)조차 t보다 앞일 때만 참
static boolean before(long t) {
return now().latest < t;
}
// commit-wait: TT.after(commitTimestamp)가 참이 될 때까지 커밋 완료(락 해제)를 미룬다
static long commitWait(long commitTimestamp) throws InterruptedException {
long startNanos = System.nanoTime();
while (!after(commitTimestamp)) {
Thread.sleep(1);
}
return (System.nanoTime() - startNanos) / 1_000_000; // ms로 환산
}
public static void main(String[] args) throws InterruptedException {
System.out.println("epsilon(시뮬레이션 파라미터) = " + EPSILON_MILLIS + "ms");
System.out.println();
System.out.println("=== 1. commit-wait을 거친 두 트랜잭션 -- 구간이 겹치지 않는다 ===");
long commitA = now().latest; // Spanner식: 타임스탬프 확보 시점 구간의 latest를 커밋 타임스탬프로 쓴다
long waitA = commitWait(commitA);
TTInterval intervalA = now();
System.out.println("A 커밋 타임스탬프 = " + commitA);
System.out.println("A 커밋 완료 시점 구간 = " + intervalA);
System.out.println("A의 commit-wait 소요 시간 = " + waitA + "ms");
long commitB = now().latest;
long waitB = commitWait(commitB);
TTInterval intervalB = now();
System.out.println("B 커밋 타임스탬프 = " + commitB);
System.out.println("B 커밋 완료 시점 구간 = " + intervalB);
System.out.println("B의 commit-wait 소요 시간 = " + waitB + "ms");
boolean nonOverlapping = intervalA.latest < intervalB.earliest;
System.out.println("A.latest < B.earliest ? " + nonOverlapping
+ " (참이면 두 구간이 겹치지 않아 A -> B 순서가 확정된다)");
System.out.println();
System.out.println("=== 2. 대조군 -- commit-wait을 생략하면 구간이 겹친다 ===");
long commitX = now().latest;
long commitY = now().latest; // 대기 없이 곧바로 다음 타임스탬프를 받는다
TTInterval intervalX = new TTInterval(commitX, EPSILON_MILLIS);
TTInterval intervalY = new TTInterval(commitY, EPSILON_MILLIS);
System.out.println("X 구간 = " + intervalX);
System.out.println("Y 구간 = " + intervalY);
boolean overlapping = !(intervalX.latest < intervalY.earliest) && !(intervalY.latest < intervalX.earliest);
System.out.println("두 구간이 겹치는가? " + overlapping
+ " (겹치면 어느 쪽이 먼저인지 구간만으로는 판단할 근거가 없다)");
}
}epsilon(시뮬레이션 파라미터) = 5ms
=== 1. commit-wait을 거친 두 트랜잭션 -- 구간이 겹치지 않는다 ===
A 커밋 타임스탬프 = 1785491133767
A 커밋 완료 시점 구간 = [earliest=1785491133768, latest=1785491133778]
A의 commit-wait 소요 시간 = 11ms
B 커밋 타임스탬프 = 1785491133789
B 커밋 완료 시점 구간 = [earliest=1785491133790, latest=1785491133800]
B의 commit-wait 소요 시간 = 11ms
A.latest < B.earliest ? true (참이면 두 구간이 겹치지 않아 A -> B 순서가 확정된다)
=== 2. 대조군 -- commit-wait을 생략하면 구간이 겹친다 ===
X 구간 = [earliest=1785491133797, latest=1785491133807]
Y 구간 = [earliest=1785491133797, latest=1785491133807]
두 구간이 겹치는가? true (겹치면 어느 쪽이 먼저인지 구간만으로는 판단할 근거가 없다)(✅ 실측 — 2026년 7월, Temurin JDK 21 컨테이너. commit-wait 소요 시간은 System.nanoTime()으로만 쟀고, 타임스탬프 자체는 구간을 만드는 값이라 벽시계(System.currentTimeMillis())를 그대로 썼다. 스레드 스케줄링·컨테이너 부하에 따라 정확한 ms 값은 실행마다 조금씩 달라질 수 있으니, 숫자 자체보다 아래 두 패턴에 주목할 것.)
1번 블록에서 A의 commit-wait 소요 시간은 11ms, B도 11ms — 둘 다 2 × ε(5ms) = 10ms 언저리입니다. 이유는 명확합니다. 커밋 타임스탬프로 구간의 latest(현재+ε)를 골랐으니, TT.after가 참이 되려면 다음 구간의 earliest(미래−ε)가 그 값을 넘어서야 하고, 그러려면 실제 시간이 최소 2ε만큼 흘러야 합니다. 그 결과 A의 커밋 완료 구간(latest=...778)은 B의 커밋 완료 구간(earliest=...790)보다 확실히 앞서고, 겹치지 않습니다. 이게 이 장이 사려는 것 — 순서를 대기 시간 주고 사는 정확한 그림입니다.
2번 블록은 그 대기를 생략하면 무슨 일이 벌어지는지 보여줍니다. X와 Y는 거의 동시에(밀리초 이하 간격으로) TT.now()를 불렀을 뿐인데, ε(5ms)가 그 사이의 실제 간격보다 훨씬 크다 보니 두 구간이 완전히 겹쳐버립니다(같은 밀리초라 X와 Y의 구간이 정확히 일치합니다). 이 상태에서 X와 Y 중 어느 쪽이 실제로 먼저 일어났는지, 구간 정보만으로는 원리적으로 판단할 방법이 없습니다. commit-wait은 바로 이 겹침을 강제로 없애기 위한 장치였던 겁니다.
대가 — 레이턴시로 순서를 사는 거래
commit-wait의 청구서는 명확합니다. 모든 커밋이 ε의 폭만큼 지연됩니다. 위 시뮬레이션에서 ε=5ms일 때 커밋마다 약 10~11ms를 대기했듯, 실제 Spanner에서도 평균 ε가 약 4ms라면 커밋마다 그만큼의 지연을 지불한다는 뜻입니다. 📄 문서 기반(미검증) — 다만 실무에서는 이 대기가 복제(replication)에 걸리는 시간과 겹쳐 상당 부분 가려지는 것으로 알려져 있어, 체감 지연이 ε를 그대로 더한 것만큼 크지는 않다고 합니다. 그래도 원리는 바뀌지 않습니다 — 정확한 전역 순서는 공짜가 아니고, Spanner는 그 값을 시간(레이턴시)으로 치릅니다.
이게 지금까지의 장들과 정반대되는 선택이라는 걸 다시 보면 흥미롭습니다. 18~19장은 시각을 포기하고 순서만 셌습니다. 이번 장은 시각을 완전히 버리지 않고, 대신 그 시각에 실린 불확실성을 숨기지 않고 드러낸 뒤, 그 불확실성이 지나가길 기다렸습니다. 어느 쪽도 “정확한 시각”을 얻지는 못했습니다. 다만 후자는 불확실성의 크기를 알고 있었기에, 그 크기만큼만 정직하게 기다리면 된다는 걸 알았을 뿐입니다.
외부 일관성 — 이 모든 걸로 사려는 것
지금까지 지불한 대가로 얻는 성질이 서두에서 짚은 외부 일관성입니다. 대기가 왜 그걸 사는지가 이제 분명해집니다 — 커밋을 마친 T1의 타임스탬프가 이미 모든 관찰자에게 과거임이 보장되므로, 그 뒤에 시작한 T2는 어떤 시계를 보든 반드시 더 큰 타임스탬프를 받습니다. 그래서 어떤 클라이언트가 어느 리전에서 스냅샷을 읽든, T1이 빠진 채 T2만 반영된 상태는 존재할 수 없습니다.
commit-wait이 정확히 이 성질을 만드는 이유는 이미 위에서 봤습니다. TT.after(s)가 참이 될 때까지 기다린 뒤 커밋을 완료한다는 건, 그 커밋 타임스탬프 s가 이미 확실한 과거라는 사실을 실제 시간의 경과로 보증한 뒤에야 다음 트랜잭션에게 자리를 내준다는 뜻입니다. 이 순서를 어기려면 물리적으로 시간을 거슬러야 하는데, 그건 어떤 클라이언트의 시계로도 불가능합니다.
하드웨어 없이 — HLC라는 다른 선택
📄 문서 기반(미검증) — GPS·원자시계 같은 전용 하드웨어 없이 비슷한 보장을 흉내 내려는 시스템도 있습니다. CockroachDB의 공식 기술 블로그는 자신들이 Spanner와 다른 길을 택했다고 설명합니다 — commit-wait처럼 매번 대기하는 대신, NTP로 관리되는 최대 클록 오프셋(대개 100~250ms 수준으로, Spanner의 ε보다 훨씬 큽니다)을 불확실성 구간으로 삼고, 그 구간 안에서 읽은 값이 실제로는 더 나중에 쓰인 값과 순서가 꼬일 수 있다고 판단되면 그때만 트랜잭션의 타임스탬프를 올리고 다시 시도(uncertainty restart) 합니다. “Spanner는 쓸 때마다 항상 기다리고, CockroachDB는 읽을 때 가끔만 재시도한다”는 게 이 블로그가 요약하는 차이입니다. 하드웨어로 불확실성을 몇 ms로 좁혀 매번 기다리느냐, 소프트웨어로 불확실성을 넉넉히 잡고 문제가 될 때만 재시도하느냐 — 같은 문제를 어느 쪽에서 대가를 치를지 고른 두 가지 답인 셈입니다.
정리
- 19장은 벡터 클록으로 인과관계를 정확히 구별했지만, 실제 시각과 동시적 이벤트 사이의 전역 순서는 답하지 못한 채 끝났다. 18~19장의 공통된 선택은 “시각을 포기하고 순서만 센다”였다.
- Spanner(Corbett 외, OSDI 2012)는 세 번째 길을 택한다 — 시각을 버리지 않되, 불확실성을 명시적인 값으로 드러낸다.
TT.now()는 시각 하나가 아니라TTinterval([earliest, latest])을 돌려주고, 실제 절대 시각이 그 구간 안에 있다는 게 유일한 보장이다.TT.after(t)·TT.before(t)는 이 구간 위에서 “확실히”만 참을 답하는 술어다. - 📄 문서 기반(미검증) — 구간의 폭 ε는 평균 약 4ms, 정상 상태 최대 약 7ms로 알려져 있으며, 시간 마스터를 폴링하는 약 30초 주기 사이 최악의 로컬 드리프트(초당 200마이크로초)가 누적됐다가 재동기화 때마다 뚝 떨어지는 톱니 모양으로 변한다. GPS 마스터와 원자시계 마스터를 함께 두는 이유는 두 하드웨어의 고장 원인이 서로 무관하기 때문이다.
- commit-wait: 트랜잭션은 커밋 타임스탬프
s를 확정한 뒤,TT.after(s)가 참이 될 때까지 커밋 완료를 미룬다. 실측 시뮬레이션(ε=5ms)에서 이 대기는 약2ε(11ms)이 걸렸고, 그 결과 연속된 두 트랜잭션의 구간은 겹치지 않았다 — 대기를 생략한 대조군에서는 두 구간이 그대로 겹쳐, 순서를 판단할 근거가 사라지는 것도 함께 확인했다. - 이 대가로 사는 성질이 외부 일관성(external consistency) 이다 — T1이 실제로 커밋된 뒤 T2가 시작됐다면, 어떤 관찰자에게도 T1의 타임스탬프는 T2보다 작다. 단일 객체의 선형성보다 강한, 트랜잭션 전체에 대한 보장이다.
- 📄 문서 기반(미검증) — CockroachDB는 전용 하드웨어 없이 NTP 기반의 넉넉한 불확실성 구간을 잡고, 매번 기다리는 대신 문제가 감지될 때만 트랜잭션을 재시도하는 쪽을 택했다. 하드웨어로 좁힐 것이냐 소프트웨어로 감당할 것이냐의 차이일 뿐, “불확실성을 명시적으로 다룬다”는 발상 자체는 같다.
생각해볼 질문: 하늘이 시간을 정하고, 나라가 표준시로 통일하고, 원자시계가 초를 정의했습니다. 그런데 분산 시스템에서는 끝내 아무도 시각을 정하지 못했습니다. Spanner의 답은 “그래도 누군가 정했다”가 아니라 “아무도 정하지 못한다는 사실 자체를 측정 가능하게 만들고, 그 측정값만큼 기다린다” 는 것이었습니다. GPS 위성도 원자시계도 없는 평범한 백엔드 서비스에서, 이 교훈 중 가져올 수 있는 건 무엇일까요 — 완벽한 시계를 갖는 게 아니라, 시계를 대하는 태도를 바꾸는 것이라면요?