it 서적 독후감

[DDIA 8장] 분산 시스템의 골칫거리 — 네트워크, 클럭, 그리고 불확실성

데일리코딩 2026. 4. 3. 10:58

[DDIA 8장] 분산 시스템의 골칫거리 — 네트워크, 클럭, 그리고 불확실성

Designing Data-Intensive Applications (Martin Kleppmann) 를 읽으며 정리한 시리즈입니다.
1장 | 2장 | 3장 | 4장 | 5장 | 6장 | 7장


들어가며

단일 서버에서 개발할 때는 당연하게 여겼던 것들이 있다.

함수를 호출하면 결과가 온다. 시간은 단조롭게 흐른다. 메모리에 쓴 값은 바로 읽힌다. 실패하면 예외가 터진다.

분산 시스템에서는 이 중 어느 것도 당연하지 않다.

8장은 분산 시스템이 본질적으로 왜 어려운지를 다룬다. 특별히 나쁘게 설계해서가 아니라, 분산이라는 특성 자체가 단일 시스템에 없던 문제들을 만들어낸다. 이 장을 읽고 나면 분산 시스템을 설계할 때 왜 그렇게 조심해야 하는지가 뼈저리게 이해된다.


네트워크의 본질 — 신뢰할 수 없다

분산 시스템에서 노드들은 네트워크로 연결된다. 그리고 네트워크는 근본적으로 신뢰할 수 없다.

패킷은 사라질 수 있다

네트워크 요청을 보냈을 때 가능한 결과는 세 가지가 아니라 훨씬 많다.

  • 요청이 상대방에게 도달하지 못했다 (패킷 유실)
  • 요청은 도달했지만 응답이 오는 도중 유실됐다
  • 상대방이 요청을 처리했지만 응답이 지연되고 있다
  • 상대방 노드가 죽어서 응답을 못 한다
  • 상대방이 처리 중이지만 느리다

요청을 보내고 응답이 없을 때, 위의 상황 중 어느 것인지 알 수 없다. 처리가 됐는지 안 됐는지조차 확인이 안 된다.

타임아웃을 설정한다고 해도 문제가 완전히 해결되지 않는다. 타임아웃 후 재시도를 하면, 첫 번째 요청이 사실 처리됐을 경우 중복 처리가 된다. 그래서 멱등성(Idempotency) 이 중요하다. 같은 요청을 여러 번 보내도 결과가 같아야 한다.

지연은 예측 불가능하다

네트워크 지연은 일정하지 않다. 대부분의 요청은 수 밀리초 내에 처리되지만, 가끔 수백 밀리초 혹은 수 초가 걸리기도 한다.

이유는 다양하다. 네트워크 스위치의 큐 적체, TCP 재전송, 운영체제의 컨텍스트 스위칭, 가상화 환경에서의 CPU 스케줄링. 같은 요청이 어떤 날은 1ms, 어떤 날은 1000ms가 걸릴 수 있다.

비결정적 지연(Unbounded Delay) 이 분산 시스템 설계를 어렵게 만드는 핵심 요인이다.

실무적 시사점

타임아웃 값을 어떻게 정해야 하는가? 너무 짧으면 멀쩡한 노드를 죽었다고 오판한다. 너무 길면 장애 감지가 늦어진다. 정답이 없는 트레이드오프다.

Netflix의 Hystrix, 요즘의 Resilience4j 같은 서킷 브레이커 라이브러리가 이 문제를 다루기 위해 존재한다. 일정 횟수 이상 실패하면 빠르게 실패를 반환하고 복구를 기다리는 패턴이다.


클럭 — 시간도 믿을 수 없다

단일 서버에서는 System.currentTimeMillis() 를 호출하면 정확한 현재 시각을 얻는다고 믿는다. 분산 환경에서는 이 믿음이 깨진다.

두 종류의 클럭

물리적 클럭 (Time-of-Day Clock)
실제 달력 시각을 반환한다. System.currentTimeMillis(). NTP(Network Time Protocol)로 외부 서버와 동기화한다.

문제: NTP 동기화는 완벽하지 않다. 네트워크 지연 때문에 수십 밀리초의 오차가 있다. 심한 경우 수 분 차이가 나기도 한다. 더 큰 문제는 시계가 뒤로 갈 수 있다는 것이다. NTP가 로컬 시계가 앞서 있다고 판단하면 시계를 뒤로 조정한다. "현재 시각"이 방금 전 시각보다 작아지는 상황이 발생한다.

단조 클럭 (Monotonic Clock)
항상 증가하는 클럭이다. Java의 System.nanoTime(). 두 시점 사이의 경과 시간을 측정하는 데 사용한다.

절대 시각을 알 수는 없지만 "A가 B보다 얼마나 나중인가"는 신뢰할 수 있다. 단, 같은 JVM 프로세스 내에서만 유효하다. 다른 노드의 nanoTime()과 비교하는 건 의미 없다.

클럭 불일치가 만드는 문제

분산 시스템에서 이벤트의 순서를 타임스탬프로 결정하면 위험하다.

노드A (클럭이 약간 빠름): 이벤트 X 발생, 타임스탬프 T+1
노드B (클럭이 정확): 이벤트 Y 발생, 타임스탬프 T

실제 순서: X가 Y보다 먼저 발생
타임스탬프 기준 순서: Y(T)가 X(T+1)보다 먼저로 보임

5장의 멀티 리더 복제에서 충돌 해결에 Last Write Wins(LWW, 타임스탬프 기준) 전략을 쓰면 이 문제가 생긴다. 나중에 발생한 쓰기가 타임스탬프가 작아서 덮어써진다.

구글의 TrueTime

구글은 Spanner DB에서 이 문제를 독특하게 해결했다. GPS와 원자시계를 데이터센터에 설치해서 시각 오차를 매우 작은 범위(보통 수 밀리초 이내)로 줄인다. 그리고 현재 시각을 단일 값이 아니라 구간(earliest, latest) 으로 반환한다.

TrueTime.now() → [T-ε, T+ε]
현재 시각은 이 구간 어딘가에 있음이 보장됨

트랜잭션 커밋 시 불확실성 구간이 지나갈 때까지 의도적으로 기다린다. 이를 통해 전 세계에 분산된 데이터센터에서도 일관된 스냅샷을 제공한다.

일반 회사에서는 이 수준의 하드웨어 인프라를 갖추기 어렵다. 분산 시스템에서 타임스탬프로 순서를 결정하는 건 근본적으로 위험하다는 교훈이다.


프로세스 일시 정지 — 실행 중에도 멈출 수 있다

더 충격적인 문제가 있다. 프로세스가 실행 중에 예고 없이 멈출 수 있다.

GC 멈춤

JVM의 Stop-The-World GC가 발동하면 프로세스가 수 초 동안 완전히 멈춘다. 멈추는 동안 외부에서 보면 노드가 죽은 것처럼 보인다. GC가 끝나고 깨어나면 자신이 얼마나 멈췄는지 알 수 없다.

가상화 환경의 CPU 스케줄링

VM(가상 머신)에서 하이퍼바이저가 CPU를 다른 VM에 할당하면 내 VM의 프로세스는 CPU를 받지 못해 멈춘다. 수 초, 심한 경우 수 분이 지나고 다시 CPU를 받을 수 있다.

이것이 왜 문제인가 — 리더 선출 시나리오

1. 노드A가 리더로 선출됨
2. 노드A가 GC로 인해 30초간 멈춤
3. 다른 노드들: "노드A 죽었나?" → 노드B를 새 리더로 선출
4. 노드B가 리더로서 쓰기를 처리하기 시작
5. 노드A의 GC 완료, 자신이 여전히 리더라고 생각하고 쓰기 처리 시작
6. 두 리더가 동시에 존재 → 데이터 불일치

노드A는 30초가 지났는지 알 수 없다. 자신의 관점에서는 방금 전까지 리더였고, 아무 문제 없이 계속 실행하면 된다고 생각한다.

펜싱 토큰 (Fencing Token)

이 문제를 해결하기 위한 패턴이다.

리더십을 획득할 때마다 단조 증가하는 토큰을 발급받는다.

노드A: 리더 획득, 토큰 = 33
(노드A GC로 멈춤)
노드B: 새 리더 획득, 토큰 = 34
노드B: 토큰 34로 저장소에 쓰기 시작
노드A: GC 완료, 토큰 33으로 저장소에 쓰려고 시도
저장소: 이미 토큰 34를 처리했음, 토큰 33은 거부

저장소가 이전 토큰의 쓰기를 거부함으로써 구식 리더의 쓰기를 막는다. ZooKeeper의 세션 에포크가 이 방식을 구현한다.


진실은 다수결로 결정된다

분산 시스템의 핵심 통찰 중 하나다.

노드는 자신의 상태를 스스로 결정할 수 없다. 네트워크가 끊겨서 다른 노드와 통신이 안 될 때, 자신이 살아있는 건지 고립된 건지 알 수 없다. 자신은 멀쩡하다고 생각하지만 다른 노드들은 자신이 죽었다고 판단하고 새 리더를 선출했을 수 있다.

그래서 쿼럼(Quorum) — 과반수의 동의 가 필요하다. 어떤 결정을 내리려면 노드 과반수가 동의해야 한다. 5장의 리더 없는 복제에서 w + r > n 조건이 이 원리다.


비잔틴 장애 (Byzantine Faults)

지금까지는 노드가 고장나거나 느려지는 경우를 다뤘다. 더 어려운 경우가 있다. 노드가 거짓말을 하는 경우다.

악의적인 노드가 잘못된 데이터를 전송하거나, 하드웨어 오류로 메모리가 손상되어 잘못된 값을 반환하는 상황이다. 이를 비잔틴 장애라고 한다.

대부분의 분산 DB는 비잔틴 장애를 가정하지 않는다. 노드가 느리거나 죽을 수는 있지만 거짓말은 하지 않는다고 전제한다. 블록체인이 비잔틴 장애를 고려한 합의 알고리즘을 쓰는 이유가 여기에 있다. 서로 신뢰할 수 없는 노드들 사이에서 합의를 이끌어내야 하는 환경이기 때문이다.


분산 시스템의 불확실성을 받아들이기

8장의 메시지를 한 줄로 요약하면 이것이다.

분산 시스템에서 확실한 것은 없다. 불확실성을 전제로 설계해야 한다.

네트워크 요청은 유실될 수 있다. 지연은 예측 불가능하다. 클럭은 틀릴 수 있다. 프로세스는 예고 없이 멈출 수 있다. 이 모든 것을 가정하고도 올바르게 동작하는 시스템을 만드는 것이 목표다.

이게 불가능하게 들릴 수 있다. 하지만 9장에서 다루는 합의 알고리즘들이 이 불확실성 속에서도 일관성을 보장하는 방법을 보여준다.


마치며 — 내 실무와 연결되는 점

8장을 읽고 나서 평소에 당연하게 생각했던 것들을 다시 돌아보게 됐다.

REST API 응답이 없을 때 타임아웃을 너무 짧게 잡아서 멀쩡한 서버를 죽었다고 판단한 적이 있었다. 재시도 로직에 멱등성을 고려하지 않아서 중복 처리가 생긴 경험도 있었다. 이 모든 게 네트워크의 비결정적 지연에서 비롯된 문제였다.

그리고 분산 락을 구현할 때 Redis의 SETNX를 쓰면서 "이거 완벽하게 안전한가?" 라는 의문이 항상 있었다. 펜싱 토큰 개념을 보고 나서 왜 단순한 분산 락이 GC 멈춤 앞에서 깨질 수 있는지, 그리고 Redlock 같은 방식이 왜 논쟁의 여지가 있는지를 이해하게 됐다.

9장은 일관성과 합의다. 이 모든 불확실성 속에서 노드들이 어떻게 합의에 도달하는지, Paxos와 Raft 같은 합의 알고리즘이 어떻게 동작하는지를 다룬다. 2부의 마지막 장이자 가장 이론적으로 깊은 장이다.


읽어주셔서 감사합니다. 잘못된 내용이나 보완할 점이 있으면 댓글로 알려주세요.