분산 시스템을 다루다 보면 "이게 됐는지 안 됐는지"조차 모르는 순간이 온다. 백업 잡이 끝났다는 응답이 안 왔는데, 정작 백업은 끝나 있을 수도 있다. 멀티 사이트로 나가는 포털 백엔드에서 노드 하나가 느려진 건지 죽은 건지 판단해야 하는데, 가진 정보는 "아직 응답이 없다"뿐이다. 분산 시스템을 운영하다 이런 순간을 몇 번 겪고 나서, "데이터 중심 애플리케이션 설계"의 두 챕터(8장 "분산 시스템의 골칫거리", 9장 "일관성과 합의")를 다시 정독했다. 사실 이 책은 주니어 때 한 번 펼쳤다가 덮었다. 그땐 "네트워크는 신뢰할 수 없다", "시계는 믿을 수 없다" 같은 문장이 그냥 멋있는 경구로만 읽혔다. 그런데 직접 분산 시스템을 운영하면서 응답 없는 잡 앞에서 몇 번 멍해지고 나니, 같은 문장이 이번엔 내 어제 장애의 이름표로 읽혔다. 워낙 유명한 내용이라 한 번쯤 읽고 넘긴 사람이 많지만, 막상 내가 운영 중인 시스템에 비춰 보니 "아 이래서 이렇게 짰구나" 싶은 지점이 한둘이 아니었다. 이 글은 그 두 챕터를 서버 개발자 관점에서 다시 정리한 기록이다.
Intro
- 분산 시스템의 본질은 부분 장애(partial failure) 와 비결정성이다. 단일 머신은 "되거나 안 되거나"지만, 분산 시스템은 "됐는지 모르는" 상태가 정상이다.
- 네트워크는 신뢰할 수 없다. 요청, 응답 어느 쪽이든 손실, 지연될 수 있고, 이 둘을 구분할 방법이 없다. 그래서 결국 타임아웃에 의존하는데, "올바른 타임아웃 값"은 존재하지 않는다. 실험으로 찾아야 한다.
- 시계도 믿을 수 없다. 이벤트 순서를 물리적 시계(타임스탬프)로 정하려는 시도(LWW)는 인과성을 조용히 위반한다. 순서가 필요하면 논리적 시계(램포트 타임스탬프)나 합의를 써야 한다.
- 프로세스는 GC, VM suspend로 자기도 모르게 멈췄다 깨어난다. "내가 아직 리더다"라는 믿음은 언제든 거짓일 수 있다. 그래서 자원 쪽이 펜싱 토큰으로 능동 거부해야 한다. 클라이언트의 선의를 믿으면 안 된다.
- 진실은 단일 노드가 아니라 정족수(quorum) 가 정한다. 누가 리더인지, 어떤 노드가 죽었는지 모두 다수결이다.
- 9장의 결론은 명쾌하다. 리더 선출, 원자적 커밋, 유일성 제약, 전체 순서 브로드캐스트, 선형적 compare-and-set은 전부 합의(consensus)와 등가다. 그리고 그 어려운 합의는 직접 구현하지 말고 주키퍼, etcd에 아웃소싱하는 게 실무적 정답이다.
- 실무 교훈 한 줄: 모든 강한 보장(선형성, 분산 트랜잭션)에는 성능, 가용성 비용이 따른다. 정말 필요한 곳에만 쓰고, 나머지는 약한 보장(인과적 일관성, 멱등성+재시도)으로 풀어라.
왜 하필 8, 9장이 그렇게 유명할까
이 책을 추천하는 사람들이 유독 8, 9장을 콕 집는 데는 이유가 있다. 그 이유를 먼저 짚어야, 아래 내용이 단순한 "분산 이론 요약"이 아니라 저자(마틴 클레프만)가 던지는 한 가지 태도로 읽힌다.
이 책 전체를 관통하는 주장은 단순하다. "유행어 뒤에 숨은 진짜 보장이 무엇인지 따져 물어라." NoSQL, 이벤트 소싱, "고가용", "강한 일관성" 같은 마케팅 단어를 그대로 믿지 말고, 그 시스템이 정확히 어떤 조건에서 무엇을 보장하고 무엇을 포기하는지 정의하라는 것이다. 8, 9장은 이 태도가 가장 날카롭게 드러나는 절정이다.
8장이 하려는 말은 "당신의 직관은 분산 환경에서 배신당한다"이다. 단일 머신에서 당연하게 여기던 세 가지, 네트워크는 메시지를 전달하고 / 시계는 시간을 알려주고 / 프로세스는 멈추지 않고 흐른다는 가정이 분산 환경에선 전부 무너진다. 저자는 비관주의를 일부러 끝까지 밀어붙인다. "잘못될 수 있는 모든 것은 잘못된다"고 가정하게 만들어, 독자가 "그래서 내 시스템은 지금 어떤 가정 위에 서 있는가?" 를 강제로 자문하게 한다. 이게 8장이 유명한 이유다. 막연히 "장애에 대비해야지"가 아니라, 부분 장애, 비결정성, 기약 없는 지연, 시계 드리프트, 프로세스 중단이라는 구체적인 적의 이름표를 붙여주기 때문이다.
9장이 하려는 말은 "그 불확실성 위에서도 강한 보장을 세울 수 있고, 놀랍게도 그 모든 문제가 하나로 모인다"이다. 8장이 무너뜨렸다면 9장은 다시 세운다. 그리고 이 장의 백미는 마지막에 온다. 따로따로 풀어야 할 것처럼 보이던 문제들, 리더 선출, 원자적 커밋, 유일성 제약, 전체 순서 브로드캐스트, 선형적 compare-and-set이 사실은 전부 "합의(consensus)"라는 한 점으로 환원되는 등가 문제라는 통합. 흩어져 보이던 분산 시스템의 난제들이 하나의 이론적 핵으로 수렴하는 그 순간이, 많은 사람이 "이 장에서 머릿속이 정리됐다"고 말하는 지점이다.
여기에 더해 두 장은 분산 시스템의 정전(canon) 을 실무자 언어로 꿰어준다. FLP 불가능성, CAP 정리, 램포트 타임스탬프, Paxos/Raft, 선형성 같은 개념들은 원래 흩어진 논문과 학술 용어 속에 있어서 서버 개발자가 접근하기 어렵다. 8, 9장은 이걸 "그래서 네 코드에서 무슨 일이 벌어지는가"의 언어로 번역해준다. 유명한 이유는 결국 하나다. 분산 시스템을 막연히 두려워하던 개발자에게, 두려워해야 할 것의 정확한 목록과 그것을 다루는 검증된 도구를 동시에 쥐여주기 때문이다.
출발점: "성공했는지조차 모른다"는 공포
서버 개발을 처음 할 때는 함수 호출이 곧 결과였다. service.save() 가 예외를 안 던지면 저장된 거고, 던지면 안 된 거다. 결정적(deterministic)이다. 같은 입력은 같은 출력을 낸다.
분산 시스템에 들어오면 이 전제가 깨진다. 8장이 가장 먼저 못 박는 문장이 이거다.
단일 컴퓨터는 "올바로 동작하거나 아예 동작하지 않거나"로 설계된다. 분산 시스템은 어떤 부분은 멀쩡하고 어떤 부분은 예측 불가능하게 고장 나는 부분 장애가 일어난다. 그리고 그것은 비결정적이다.
내가 다루는 시스템들(대용량 백업 솔루션, 멀티 사이트로 납품되는 포털 백엔드)은 둘 다 노드 여러 대가 네트워크로만 통신하는 비공유(shared-nothing) 구조다. 여기서 원격 노드에 요청을 보내고 응답을 기다릴 때 잘못될 수 있는 경우를 8장은 6가지로 쪼갠다.

핵심은, 클라이언트 입장에서 이 6가지를 구분할 수 없다는 점이다. 가진 정보라곤 "아직 응답을 못 받았다"뿐이다. 백업 완료 콜백이 안 왔을 때, 작업 자체가 실패한 건지 / 끝났는데 응답만 유실된 건지 알 수가 없다. 이 구분 불가능성이 모든 재시도, 멱등성 설계의 출발점이다.
8장: 분산 시스템의 세 가지 골칫거리
신뢰성 없는 네트워크, 그리고 타임아웃의 딜레마
네트워크 문제는 "가끔" 생기는 예외가 아니다. 8장이 인용하는 연구에 따르면 잘 관리되는 중간 규모 데이터센터에서도 매달 약 12건의 네트워크 결함이 발생한다. 상어가 해저 케이블을 물어뜯기도 하고, NIC가 수신 패킷은 다 버리면서 송신만 멀쩡히 하는 황당한 고장도 있다.
그래서 노드 생사를 판단하는 유일한 현실적 수단이 타임아웃인데, 여기에 정답이 없다.
| 짧은 타임아웃 | 긴 타임아웃 | |
|---|---|---|
| 장점 | 장애를 빨리 감지 | 거짓 양성(false positive)이 적음 |
| 단점 | 멀쩡한(느려졌을 뿐인) 노드를 죽었다고 오판 | 사용자가 오래 기다림 |
| 위험 | 중복 실행 + 부하 전가로 연쇄 장애(cascading failure) | 장애 대응 지연 |
이상적으론 네트워크 최대 지연 d, 처리 시간 r로 2d + r을 쓰면 되지만, 비동기 패킷 네트워크는 기약 없는 지연(unbounded delay) 을 갖는다. 상한이 없다. 지연 가변성의 주범은 큐 대기다(스위치 큐, OS 수신 큐, VM 스틸타임, TCP 흐름 제어). 그리고 이건 시스템이 부하 한계에 가까울수록 폭발적으로 커진다.
서버 개발자 메모: 그래서 타임아웃은 상수로 하드코딩하고 끝낼 게 아니라, 운영 중 RTT 분포를 측정해서 정하거나(파이 증가 장애 감지기처럼) 동적으로 조정해야 한다. 그리고 "노드를 죽었다고 선언하는 행위" 자체가 부하를 다른 노드로 옮겨 연쇄 장애를 촉발할 수 있다는 걸 잊으면 안 된다.
신뢰성 없는 시계: LWW가 데이터를 조용히 삼킨다
현대 컴퓨터엔 두 종류의 시계가 있고, 이걸 헷갈리면 사고가 난다.

가장 위험한 안티패턴이 최종 쓰기 승리(LWW, Last Write Wins) 다. 다중 리더 복제에서 충돌을 해소하려고 "타임스탬프 큰 쪽이 이긴다"를 쓰면, 시계가 뒤처진 노드의 더 늦은(인과적으로 나중인) 쓰기가 더 작은 타임스탬프 때문에 조용히 버려진다. 인과성 위반이고, 무엇보다 에러 없이 데이터가 사라진다.
NTP도 만능이 아니다. 인터넷 NTP 동기화 오차는 최소 35ms, 가끔 1초를 넘긴다. 윤초가 대형 시스템을 다운시킨 사례도 있다. 구글 스패너가 트루타임(TrueTime) 으로 [earliest, latest] 신뢰 구간을 노출하고, 커밋 전에 그 구간만큼 의도적으로 대기하는 이유가 여기 있다. 시계 값은 점이 아니라 구간이다.
결론: 여러 노드에 걸친 이벤트 순서를 물리적 시계로 정하지 마라. 순서가 필요하면 논리적 시계(9장)를 써라.
프로세스 중단: "내가 아직 리더야"라는 착각
이 절이 개인적으로 가장 충격이었다. 리더가 리스(lease)를 들고 일하는 흔한 코드를 보자.
while (true) {
request = getIncomingRequest();
if (lease.expiryTimeMillis - System.currentTimeMillis() < 10000) {
lease = lease.renew();
}
if (lease.isValid()) {
process(request); // ← 여기 직전에 15초 멈추면?
}
}
lease.isValid() 통과와 process(request) 사이에서 stop-the-world GC가 15초 돈다면, 리스는 이미 만료됐고 다른 노드가 리더가 됐는데도 이 노드는 자기가 여전히 리더인 줄 알고 공유 자원에 쓴다. 데이터 손상이다.
프로그램이 자기도 모르게 멈추는 경로는 많다. GC, VM suspend/라이브 마이그레이션, 디스크 I/O 대기, 페이지 폴트 스와핑, SIGSTOP. 멈춘 스레드는 자기가 멈췄다는 사실조차 모른다.
해법이 펜싱 토큰(fencing token) 이다.

핵심은 자원 스스로가 토큰을 검사해 오래된 쓰기를 능동적으로 거부한다는 것이다. 클라이언트가 "나 아직 리더 맞지?"를 스스로 올바르게 판단하리라 믿으면 안 된다. 주키퍼의 zxid가 바로 이 펜싱 토큰이다.
진실은 다수결, 그리고 비잔틴 결함
한 노드의 자기 판단은 믿을 수 없으므로, 분산 알고리즘은 정족수(quorum) 투표에 의존한다. "저 노드 죽었다"는 선언조차 다수결이다. 정족수가 죽었다고 하면, 실제로 살아 있어도 죽은 것으로 간주된다.
노드가 거짓말까지 하는 경우(손상된 응답, 악의적 조작)는 비잔틴 결함이라 부른다. 비트코인 같은 상호 불신 환경엔 필수지만, 단일 조직이 운영하는 데이터센터에선 보통 가정하지 않는다. 비용이 크고, 어차피 공격자가 한 노드를 뚫으면 다른 노드도 같은 방식으로 뚫리기 때문이다. 다만 약한 형태의 거짓말(손상 패킷, 오설정)에 대비하는 값싼 방어(애플리케이션 체크섬, 입력 검증, 다중 NTP 서버)는 해 둘 만하다.
시스템 모델: 안전성과 활성
알고리즘은 어떤 결함을 가정하는지 명확히 해야 한다. 현실 시스템에 가장 유용한 조합은 "충돌-복구(crash-recovery) 결함을 동반한 부분 동기(partially synchronous) 모델" 이다. 평소엔 동기적이지만 가끔 상한을 넘기고, 노드는 죽었다 디스크 상태를 안고 되살아난다.
그리고 알고리즘 속성은 두 가지로 나뉜다. 이 구분이 9장 합의의 토대다.
- 안전성(safety): "나쁜 일은 절대 안 일어난다." 어떤 시스템 모델에서도 항상 보장돼야 한다. (예: 펜싱 토큰 유일성)
- 활성(liveness): "좋은 일은 결국 일어난다." "과반수가 살아 있고 네트워크가 결국 복구된다면"처럼 단서를 붙일 수 있다. (예: 요청은 결국 응답을 받는다)
9장: 일관성과 합의, 그리고 모든 길은 합의로 통한다
8장이 "무엇이 잘못되는가"였다면, 9장은 "그럼에도 어떻게 보장을 만드는가"다. 핵심 전략은 유용한 보장을 주는 범용 추상화를 만들고 앱이 거기에 기대게 하는 것이다.
일관성 스펙트럼: 최종적 일관성 → 선형성
대부분의 복제 DB는 최종적 일관성을 준다. 쓰기를 멈추고 충분히 기다리면 결국 수렴한다. 문제는 "언제"를 보장하지 않는다는 것. 방금 쓴 값을 곧바로 읽었는데 오래된 복제본으로 가서 못 볼 수 있다. 평소엔 잘 되다가 동시성, 결함 상황에서만 미묘한 버그가 터진다.
반대편 극단이 선형성(linearizability) 이다. 복제본이 여러 개라도 마치 하나뿐인 것처럼 보이게 하는 최신성 보장(recency guarantee).
한 클라이언트의 쓰기가 완료되면, 그 이후 읽는 모든 클라이언트는 그 값(또는 더 새 값)을 봐야 한다. 한 번 새 값을 본 뒤 다시 옛 값이 보이면 안 된다.
직렬성(serializability)과 헷갈리기 쉬운데 다르다. 직렬성은 여러 객체에 걸친 트랜잭션의 격리 속성이고, 선형성은 단일 객체의 최신성 보장이다.
선형성이 정말 필요한 곳은 아래와 같다.
- 잠금과 리더 선출: 모든 노드가 "누가 리더/잠금 보유자인지" 동의해야 한다(스플릿 브레인 방지).
- 유일성 제약: 같은 사용자명, 좌석, 재고를 둘이 동시에 못 가져가게.
- 채널 간 타이밍 의존성: 파일 저장소에 원본을 올리고 메시지 큐로 "썸네일 만들어" 작업을 넘기는데, 저장소가 선형적이지 않으면 워커가 아직 없는 이미지를 읽는다.
선형성의 비용: CAP와 지연
선형성은 비싸다. 두 데이터센터 사이에 네트워크 파티션이 나면, 선형성을 지키려면 리더와 단절된 쪽은 요청을 거부(가용성 포기) 해야 한다. 이게 흔히 말하는 CAP다. 정확히는 "파티션(P)이 났을 때 선형성(C)이냐 가용성(A)이냐"이다.
다만 8장, 9장에서 저자는 CAP를 과신하지 말라고 한다. C/A/P를 너무 좁게 정의하고, 네트워크 지연 같은 실무 변수를 못 담는다. 더 중요한 사실은 선형성이 느린 주된 이유가 내결함성이 아니라 성능이라는 것. 멀티코어 CPU의 RAM조차 캐시 때문에 선형적이지 않다. 그러니 정말 필요한 곳만 선형성을 쓰고, 나머지는 약한 모델로 가는 게 합리적이다.
순서화: 인과성과 램포트 타임스탬프
선형성보다 약하지만 실무적으로 충분한 경우가 많은 게 인과적 일관성이다.

- 선형성: 전체 순서. 동시성이 없다. 모든 연산이 단일 시간선 위에 놓인다. 그래서 비싸다.
- 인과성: 부분 순서. 인과적으로 엮인 것만 순서가 있고, 동시 이벤트는 비교 불가. 선형성은 인과성을 함의하지만, 인과성은 글로벌 조정 없이 구현 가능해 파티션에 견디고 지연에 덜 민감하다.
인과성과 일치하는 순서를 값싸게 만드는 도구가 램포트 타임스탬프다. (카운터, 노드 ID) 쌍이고, 모든 노드, 클라이언트가 지금까지 본 카운터 최댓값을 전파한다. 더 큰 카운터를 보면 자기 카운터를 끌어올린다. 이렇게 하면 인과적 순서를 위반하지 않는 전체 순서가 나온다.
그런데 램포트 타임스탬프만으론 부족하다. 유일성 제약 같은 건 "지금 이 순간" 다른 노드가 같은 사용자명을 더 작은 타임스탬프로 만들고 있는지 알아야 하는데, 타임스탬프는 사후 순서만 정할 뿐 "이 순서가 지금 확정됐다"를 보장하지 못한다. 그래서 순서가 전달 시점에 고정(finalized) 되는 전체 순서 브로드캐스트(total order broadcast) 가 필요하다.
분산 트랜잭션: 2PC의 빛과 그림자
여러 노드에 걸친 원자적 커밋을 푸는 고전이 2단계 커밋(2PC) 이다.

2PC의 안전성은 두 개의 "돌아올 수 없는 지점"에서 온다. 참여자가 yes를 던지면 반드시 커밋해야 하고, 코디네이터가 결정하면 번복 못 한다. 둘 다 디스크에 기록돼 장애에서 살아남는다.
문제는 코디네이터 장애다. 참여자가 yes를 던진 뒤 코디네이터가 죽으면, 참여자는 불확실(in-doubt) 상태에 빠진다. 일방적으로 커밋도 어보트도 못 하고 잠금을 든 채 무한정 기다린다. 코디네이터가 단일 장애 지점이 되고, 잠금이 안 풀려 애플리케이션의 큰 부분이 마비될 수 있다. XA 트랜잭션의 고아 불확실 트랜잭션은 운영자가 수동으로 풀어야 하는 악몽이다.
그래서 많은 클라우드 서비스가 분산 트랜잭션을 아예 안 쓰기로 선택했다. 2PC는 "모든 참여자가 응답해야" 커밋되므로 장애를 증폭시킨다. 내결함성을 높이려는 목표와 정면으로 충돌한다. 실무에선 2PC 대신 사가(saga) + 멱등성 + 보상 트랜잭션으로 푸는 흐름이 많은 이유다.
내결함성 합의: 과반수와 에포크
합의 알고리즘(Raft, Paxos, VSR, Zab)이 만족해야 할 속성은 네 가지다.
- 균일한 동의: 두 노드가 다르게 결정하지 않는다. (안전성)
- 무결성: 한 번 결정하면 번복 안 한다. (안전성)
- 유효성: 제안된 값 중에서만 결정한다. (안전성)
- 종료: 죽지 않은 노드는 결국 결정한다. (활성 = 내결함성)
핵심 제약은 과반수(majority)가 살아 있어야 한다는 것. 1개 장애를 견디려면 3노드, 2개면 5노드다. "리더 선출엔 합의가 필요하고 합의엔 리더가 필요한" 순환은 에포크 번호(Raft의 term, Paxos의 ballot)로 푼다. 매 라운드가 아니라 에포크 내에서만 리더 유일성을 보장하고, 리더 선출 정족수와 제안 투표 정족수가 겹치게 만들어 안전성을 지킨다.
그리고 9장의 가장 강력한 통찰은 아래가 아닐까?
리더 선출, 원자적 커밋, 유일성 제약, 전체 순서 브로드캐스트, 선형적 compare-and-set은 모두 서로 등가이며 합의로 환원된다.
실무 정답: 합의를 직접 짜지 말고 아웃소싱하라
합의는 직접 구현하기 매우 어렵고, 잘못 짠 리더 선출은 스플릿 브레인으로 이어진다. 그래서 실무 정답은 주키퍼, etcd에 맡기는 것이다. 이들은 Zab/Raft로 합의를 구현해 다음을 묶음으로 제공한다.

이걸로 리더 선출, 파티션 할당, 서비스 디스커버리, 멤버십 서비스를 안전하게 구현한다. "어떤 노드가 살아 있는가"를 장애 감지 + 합의로 묶으면, 실제로 살아 있는 노드를 죽었다고 오판하더라도 최소한 모든 노드가 동일한 멤버십에 동의하므로 시스템은 일관되게 굴러간다. 주키퍼는 합의를 대신 해주는 "합의의 아웃소싱" 인 셈이다.
서버 개발자로서 남긴 것
두 챕터를 다시 읽고 내 시스템에 비춰 정리한 행동 원칙은 이렇다.
- 재시도는 멱등성과 한 세트다. 응답이 안 왔다고 작업이 실패한 게 아니다. 재시도하려면 같은 작업이 두 번 실행돼도 안전하도록(멱등 키, upsert, 펜싱 토큰) 먼저 만든다.
- 타임아웃을 상수로 하드코딩하지 않는다. RTT 분포를 측정해서 정하고, 죽었다는 선언이 연쇄 장애를 부를 수 있음을 항상 의식한다.
- 이벤트 순서를 물리 시계로 정하지 않는다. LWW는 마지막 수단이고, 인과성이 중요하면 논리적 시계나 합의 기반 순서를 쓴다.
- "내가 리더다"를 믿지 않는다. 자원 쪽에서 펜싱 토큰으로 능동 거부하게 설계한다. 클라이언트의 선의에 안전성을 걸지 않는다.
- 강한 보장은 비싸다. 필요한 곳에만. 선형성, 분산 트랜잭션이 진짜 필요한 지점(유일성, 잔고, 좌석)을 식별하고, 나머지는 인과적 일관성 + 멱등 + 보상으로 푼다.
- 합의는 직접 짜지 않는다. 리더 선출, 멤버십, 분산 락은 etcd/주키퍼에 아웃소싱한다.
8장의 마지막 문장이 오래 남는다. "비관주의에 빠질 필요는 없다. 핵심은 이런 문제들이 존재함을 인정하고, 어떤 가정 하에서 동작하는지 명확히 하며, 신중하게 테스트하고 설계하는 것이다." 신뢰성 없는 구성 요소로 신뢰할 수 있는 시스템을 만드는 일은, 결국 "무엇이 잘못될 수 있는가"를 끝까지 상상해 본 사람이 잘하는 일이라는 걸, 운영을 해 볼수록 더 실감한다.
'CS · 책' 카테고리의 다른 글
| GPU는 '코어 많은 CPU'가 아니다 - 웹 백엔드 개발자가 다시 배운 GPU (0) | 2026.07.11 |
|---|---|
| [컴퓨터 밑바닥의 비밀] 프로그래밍 언어부터 프로그램 실행까지, 이렇게 진행된다: 프로그래밍 언어, 컴파일러, 링커, 그리고 추상화 (0) | 2025.10.12 |