8분

Leslie Lamport와 분산 시스템: 시간, 순서, 정확성

Lamport의 핵심 분산 시스템 아이디어—논리적 시계, 순서화, 합의, 정확성—을 배우고 이들이 현대 인프라를 어떻게 이끄는지 이해하세요.

Leslie Lamport와 분산 시스템: 시간, 순서, 정확성

왜 Lamport의 아이디어가 현대 분산 시스템에서 여전히 중요한가

Leslie Lamport는 이론적 연구가 실제 시스템을 배포할 때마다 등장하는 드문 연구자입니다. 데이터베이스 클러스터, 메시지 큐, 워크플로 엔진 또는 요청을 재시도하고 장애를 견디는 어떤 시스템을 운영해본 적이 있다면, 당신은 Lamport가 명명하고 해결하는 문제들 속에서 살아온 것입니다.

그의 아이디어가 오래가는 이유는 특정 기술에 묶여 있지 않기 때문입니다. 이 아이디어들은 여러 대의 기계가 하나의 시스템처럼 행동하려 할 때 언제나 드러나는 불편한 진실을 설명합니다: 시계는 어긋나고, 네트워크는 지연·손실하며, 장애는 예외가 아니라 정상입니다.

전체에서 계속 사용할 세 가지 주제

시간: 분산 시스템에서 “지금 몇 시인가?”는 단순한 질문이 아닙니다. 물리 시계는 어긋나고, 각 기계가 관찰하는 이벤트의 순서가 다를 수 있습니다.

순서: 단일 시계를 신뢰할 수 없을 때, 어떤 이벤트가 먼저 일어났는지 말할 다른 방법이 필요합니다—그리고 모두가 동일한 순서를 따르게 해야 하는 경우 이를 강제하는 방법이 필요합니다.

정확성: “보통은 작동한다”는 설계가 아닙니다. Lamport는 (안전성 vs. 진행성) 같은 명확한 정의와 테스트뿐 아니라 논리적으로 따져볼 수 있는 명세로 분야를 이끌었습니다.

기대할 내용(복잡한 수학 없음)

우리는 개념과 직관에 집중할 것입니다: 문제, 명확하게 생각하기 위한 최소한의 도구들, 그리고 그 도구들이 실제 설계에 어떤 영향을 미치는지.

지도는 다음과 같습니다:

  • 왜 공유된 시계가 없으면 단일한 사건 서사가 불가능한가
  • 인과관계(“happened-before”)가 어떻게 논리적 시계와 Lamport 타임스탬프로 이어지는가
  • 부분 순서가 충분하지 않을 때 언제 하나의 타임라인이 필요한가
  • 합의와 Paxos가 순서 합의와 어떻게 연결되는가
  • 순서가 공유될 때 상태 머신 복제가 왜 작동하는가
  • 명세에서 정확성에 대해 어떻게 이야기하는가 — 그리고 TLA+ 같은 모델링 도구가 어떻게 도움이 되는가

핵심 문제: 공유된 시계가 없으면 단일 현실이 없다

시스템이 “분산”이라는 것은 여러 대의 기계가 네트워크를 통해 협력하여 하나의 작업을 수행한다는 의미입니다. 이 말은 간단하게 들리지만, 두 가지 사실을 받아들이면 복잡해집니다: 기계는 독립적으로 실패할 수 있고(부분적 실패), 네트워크는 메시지를 지연·손실·중복·재정렬할 수 있습니다.

한 컴퓨터에서 실행되는 단일 프로그램에서는 일반적으로 “무엇이 먼저 일어났는가”를 가리킬 수 있습니다. 그러나 분산 시스템에서는 서로 다른 기계가 서로 다른 사건 순서를 관찰할 수 있고—각각의 관점에서 둘 다 맞을 수 있습니다.

왜 글로벌 클록을 신뢰할 수 없는가

조정을 타임스탬프로 해결하는 것이 유혹적이지만, 기계 간에 의존할 수 있는 단일 시계는 없습니다:

  • 각 서버의 하드웨어 시계는 각기 다른 속도로 드리프트합니다.
  • NTP 같은 시계 동기화는 최선의 노력(best-effort)일 뿐 보장이 아닙니다.
  • 가상화, CPU 부하 또는 일시 중단이 시간의 점프나 정지를 만들 수 있습니다.

따라서 한 호스트에서 “이벤트 A가 10:01:05.123에 발생했다”는 다른 호스트의 “10:01:05.120”과 신뢰성 있게 비교할 수 없습니다.

지연이 현실을 어떻게 뒤섞는가

네트워크 지연은 당신이 본 것의 순서를 뒤집을 수 있습니다. 먼저 보낸 쓰기가 나중에 도착할 수 있고, 재시도가 원본보다 늦게 도착할 수 있습니다. 두 개의 데이터센터가 같은 요청을 반대 순서로 처리할 수도 있습니다.

이로 인해 디버깅이 독특하게 혼란스러워집니다: 여러 기계의 로그가 서로 다를 수 있고, “타임스탬프별 정렬”은 실제로 일어나지 않았던 서사를 만들어낼 수 있습니다.

실제적 결과

단일 타임라인이 존재한다고 가정하면 다음과 같은 구체적 실패가 발생합니다:

  • 중복 처리 (재시도로 인해 결제가 두 번 청구되는 경우)
  • 불일치 (두 사용자가 같은 마지막 품목을 둘 다 “성공적으로” 획득하는 경우)
  • 명백한 데이터 손실 (나중에 도착한 업데이트가 더 최신의 것을 덮어쓰는 경우)

Lamport의 핵심 통찰은 여기서 시작합니다: 시간이 공유될 수 없다면, 우리는 순서를 다르게 추론해야 합니다.

인과관계와 happened-before 관계

분산 프로그램은 이벤트들로 이루어집니다: 특정 노드(프로세스, 서버, 스레드)에서 일어나는 어떤 일입니다. 예로는 “요청 수신”, “행 삽입”, “메시지 전송” 등이 있습니다. 메시지는 노드 간을 잇는 연결고리입니다: 하나의 이벤트는 *전송(send)*이고 다른 하나는 *수신(receive)*입니다.

Lamport의 핵심 통찰은, 신뢰할 수 있는 공유 시계가 없는 시스템에서 가장 확실히 추적할 수 있는 것은 인과관계—어떤 이벤트가 다른 이벤트에 영향을 미쳤을 가능성이 있는지—라는 것입니다.

happened-before 관계(→)

Lamport는 happened-before라는 간단한 규칙을 정의했습니다. 기호로는 A → B (이벤트 A가 이벤트 B보다 먼저 발생했다)는 다음을 의미합니다:

  • 동일 프로세스 내 순서: A와 B가 같은 머신/프로세스에서 발생하고 그 프로세스에서 A가 먼저 관찰되었다면 A → B.
  • 메시지 순서: A가 “메시지 m 전송”이고 B가 “메시지 m 수신”이라면 A → B.
  • 추이성: A → B이고 B → C이면 A → C.

이 관계는 **부분 순서(partial order)**를 제공합니다: 어떤 쌍들은 순서가 정해지지만, 모든 쌍이 정해지는 것은 아닙니다.

구체적 예: 사용자 → 요청 → DB → 캐시

사용자가 “구매”를 클릭합니다. 그 클릭은 API 서버에 요청을 트리거하는 이벤트 A입니다. 서버는 데이터베이스에 주문 행을 쓰는 이벤트 B를 수행합니다. 쓰기가 완료된 후 서버는 “주문 생성” 메시지를 발행하는 이벤트 C를 발생시키고, 캐시 서비스가 이를 받아 캐시 항목을 업데이트하는 이벤트 D가 일어납니다.

여기서는 A → B → C → D입니다. 시계가 어긋나더라도 메시지와 프로그램 구조가 실제 인과 연결을 만듭니다.

“동시성(concurrent)”의 실제 의미

두 이벤트는 서로 인과관계가 없을 때 동시적입니다: 즉 (A → B)도 아니고 (B → A)도 아닙니다. 동시성은 “같은 시간”을 의미하지 않습니다—“어떤 인과 경로도 연결하지 않는다”는 뜻입니다. 그래서 두 서비스가 각각 자신이 “먼저” 행동했다고 주장할 수 있고, 특별한 순서 규칙을 추가하지 않으면 둘 다 맞을 수 있습니다.

논리적 시계: Lamport 타임스탬프 쉽게 이해하기

여러 대의 기계에서 “무엇이 먼저 일어났는가”를 재구성하려 해본 적이 있다면 기본 문제를 마주했을 것입니다: 컴퓨터는 완벽히 동기화된 시계를 공유하지 않습니다. Lamport의 해결책은 완벽한 시간을 쫓는 것을 멈추고 대신 순서를 추적하는 것입니다.

아이디어: 각 이벤트에 붙이는 카운터

Lamport 타임스탬프는 프로세스의 각 의미 있는 이벤트에 붙이는 숫자입니다(서비스 인스턴스, 노드, 스레드 등). 이를 “이벤트 카운터”로 생각하면, 벽시계 시간이 신뢰할 수 없을 때도 “이 이벤트가 저 이벤트보다 먼저 일어났다”고 일관되게 말할 수 있습니다.

두 규칙(정말 간단합니다)

  1. 로컬에서 증가: 이벤트를 기록하기 전에(예: “DB에 쓰기”, “요청 전송”, “로그에 항목 추가”) 로컬 카운터를 증가시킵니다.

  2. 수신 시 max + 1 적용: 발신자의 타임스탬프가 포함된 메시지를 받으면 카운터를 다음으로 설정합니다:

max(local_counter, received_counter) + 1

그런 다음 수신 이벤트에 그 값을 찍습니다.

이 규칙들은 인과관계를 존중하도록 타임스탬프를 보장합니다: 이벤트 A가 메시지를 통해 간접적이든 직접적이든 이벤트 B에 영향을 미칠 수 있다면 A의 타임스탬프는 B보다 작습니다.

Lamport 타임스탬프가 알려주고 알려주지 못하는 것

그것들은 인과적 순서에 대해 알려줍니다:

  • 만약 TS(A) < TS(B)라면, A가 B보다 먼저 발생했을 있습니다.
  • 만약 A가 B를 원인으로 했다면(직접적이든 간접적이든), 반드시 TS(A) < TS(B)입니다.

그들은 실제 시간에 대해서는 알려주지 못합니다:

  • 더 낮은 타임스탬프가 "초 단위로 더 이전"을 의미하지 않습니다.
  • 두 이벤트가 동시적일 수 있고 메시지 패턴 때문에 서로 다른 타임스탬프를 얻을 수 있습니다.

따라서 Lamport 타임스탬프는 순서를 매기는 데는 훌륭하지만 지연을 측정하거나 “실제 시각이 언제였나”를 묻는 데는 적합하지 않습니다.

실제 예: 서비스 간 로그 항목 정렬

서비스 A가 서비스 B를 호출하고 둘 다 감사 로그를 쓴다고 합시다. 인과관계를 보존하는 통합 로그 뷰를 원합니다.

  • 서비스 A는 카운터를 증가시키고 “결제 시작”을 로그로 남기고, 타임스탬프 42와 함께 B에 요청을 보냅니다.
  • 서비스 B는 42를 받은 후 카운터를 max(local, 42) + 1로 설정하여 예를 들어 43을 찍고 “카드 검증”을 로그로 남깁니다.
  • B는 44로 응답하고, A는 이를 받아 45로 업데이트하여 “결제 완료”를 로그로 남깁니다.

이제 두 서비스의 로그를 집계할 때 (lamport_timestamp, service_id)로 정렬하면 벽시계가 어긋나거나 네트워크가 지연되었더라도 실제 인과 사슬과 일치하는 안정적이고 설명 가능한 타임라인을 얻습니다.

부분 순서에서 전체 순서로: 하나의 타임라인이 필요할 때

인과관계는 부분 순서를 제공합니다: 메시지나 의존성으로 연결된 일부 이벤트는 명확히 “이전”으로 정해지지만, 많은 이벤트는 단순히 동시적입니다. 이것은 버그가 아니라 분산 현실의 자연스러운 형태입니다.

부분 순서는 많은 질문에 충분하다

“무엇이 이것에 영향을 미쳤을까?”를 디버깅하거나 “응답은 요청 뒤에 와야 한다” 같은 규칙을 강제하려면 부분 순서가 정확히 필요한 것입니다. happened-before 간선만 존중하면 되고, 나머지는 독립적으로 취급할 수 있습니다.

전체 순서: 시스템이 하나의 서사를 선택해야 할 때

어떤 시스템은 “어느 쪽 순서든 괜찮다”는 것을 허용할 수 없습니다. 특히 다음의 경우에는 단일 순서가 필요합니다:

  • 공유 객체에 대한 쓰기(“잔액 설정”, “프로필 업데이트”, “로그에 추가”)
  • 명령을 어디서나 동일하게 적용해야 하는 경우(상태 머신 복제)
  • "마지막 쓰기 승"이 모든 노드에 같은 의미여야 하는 충돌 해결

전체 순서가 없다면 두 복제본이 지역적으로는 둘 다 “정상”으로 보이지만 전역적으로는 서로 달라질 수 있습니다: 하나는 A 다음 B를 적용하고 다른 하나는 B 다음 A를 적용하면 서로 다른 결과가 나옵니다.

하나의 타임라인을 얻는 방법

순서를 만들어내는 메커니즘을 도입합니다:

  • 각 명령에 단조 증가하는 위치를 할당하는 시퀀서/리더.
  • 또는 지연과 실패가 있어도 클러스터가 다음 로그 항목에 대해 합의하도록 하는 합의(예: Paxos 스타일).

피할 수 없는 트레이드오프

전체 순서는 강력하지만 대가가 있습니다:

  • 지연: 커밋 전에 조정을 기다려야 할 수 있습니다.
  • 처리량: 단일 정렬 로그가 병목이 될 수 있습니다.
  • 장애 시 가용성: 충분한 노드와 합의할 수 없다면 진행이 멈출 수 있습니다.

설계 선택은 명확합니다: 올바른(shared narrative) 서사가 필요하면 해당 비용을 지불하고 조정을 얻어야 합니다.

합의: 지연과 실패 속에서 동의하기

복제 로그 프로토타입
React UI와 Go·PostgreSQL 백엔드로 복제 로그 API 프로토타입을 만들어 보세요.

합의(consensus)는 여러 대의 기계가 하나의 결정—하나의 값, 하나의 리더, 하나의 구성—에 동의하게 만드는 문제입니다. 각 기계는 자신의 로컬 이벤트와 도착한 메시지들만 볼 수 있습니다.

이것은 간단해 보이지만 분산 시스템의 제약을 기억하면 복잡해집니다: 메시지는 지연·중복·재정렬·손실될 수 있고, 기계는 충돌 후 재시작할 수 있으며 어떤 노드가 "확실히 죽었다"는 신호를 얻기 어렵습니다. 합의는 이러한 조건 하에서 동의를 안전하게 만드는 문제입니다.

왜 동의가 어려운가

두 노드가 일시적으로 통신할 수 없으면(네트워크 분할) 각 쪽이 독자적으로 앞으로 나아가려 할 수 있습니다. 양쪽이 다른 값을 결정하면 split-brain(분열된 두 독립 운영) 현상이 발생할 수 있습니다: 두 리더, 서로 다른 구성, 또는 경쟁하는 이력들이 생길 수 있습니다.

분할이 없더라도 지연만으로도 문제가 됩니다. 어떤 노드가 제안을 들었을 때 다른 노드들은 이미 진행했을 수 있습니다. 공유된 시계가 없으면 물리적 시간 때문에 “제안 A가 제안 B보다 먼저 일어났다”라고 신뢰할 수 없습니다.

실제 시스템에서 합의를 만나는 곳

매일 “합의”라고 부르진 않더라도 인프라에서 흔히 마주칩니다:

  • 리더 선출 (지금 누가 책임자인가?)
  • 복제 로그 (공유 히스토리의 다음 항목은 무엇인가?)
  • 구성 변경 (어떤 노드 집합이 투표/커밋에 참여할 수 있는가?)

각 경우 시스템은 모두가 수렴할 수 있는 단일 결과가 필요하거나, 적어도 상충하는 결과가 동시에 유효하지 않도록 규칙을 가져야 합니다.

Lamport의 답변: Paxos

Lamport의 Paxos는 이 “안전한 합의” 문제에 대한 기본적인 해법입니다. 핵심 아이디어는 마법 같은 타임아웃이나 완벽한 리더가 아니라 오직 한 값만 선택될 수 있도록 하는 규칙 집합입니다. 이 규칙들은 메시지가 지연되거나 노드가 실패해도 "두 값이 선택되는 일"을 방지합니다.

Paxos는 안전성(safety)과 진행성(liveness)을 분리합니다: "두 다른 값이 선택되지 않는 것"은 보장하고, "언젠가 무언가가 선택된다"는 것은 상황이 안정되면 보장합니다. 이를 통해 실전 적용에서 성능튜닝을 하면서도 핵심 보장을 유지할 수 있습니다.

복잡함 없이 본 Paxos의 핵심 안전 직관

Paxos는 난해하다는 평판이 있지만, 그 이유의 상당 부분은 "Paxos"가 하나의 간단한 알고리즘이 아니라 비슷한 패턴들의 집합이기 때문입니다. 핵심은 그룹이 서로 동의하게 만드는 방식으로, 메시지가 지연·중복되거나 기계가 일시적으로 실패해도 동작합니다.

등장인물: 제안자(proposers), 수용자(acceptors), 그리고 쿼럼

제안하는 사람과 검증하는 사람을 분리해 생각하면 도움이 됩니다.

  • **제안자(proposers)**는 값이 선택되게 하려 합니다(예: "다음 로그 항목은 X다").
  • **수용자(acceptors)**는 제안에 대해 투표합니다.
  • 쿼럼은 진행에 필요한 "충분한 수의 수용자"입니다—대개 과반수.

핵심 구조적 아이디어: 두 과반수는 항상 겹친다는 점입니다. 그 겹침이 안전성이 사는 곳입니다.

안전성 목표: 두 다른 값이 결정되지 않게 하기

Paxos의 안전성은 간단히 말할 수 있습니다: 시스템이 한 값을 결정했다면 두 번째로 다른 값을 결정해서는 안 됩니다—split-brain 결정을 방지합니다.

핵심 직관은 제안에는 번호(투표번호, 마치 발의번호)가 붙는다는 것입니다. 수용자는 더 높은 번호의 제안을 본 뒤에는 더 낮은 번호의 제안을 무시하겠다고 약속합니다. 새로운 제안자가 새 번호로 시도할 때는 먼저 쿼럼에게 지금까지 무엇을 수용했는지 묻습니다.

쿼럼들은 겹치기 때문에, 새로운 제안자는 반드시 최근에 수용된 값을 "기억하는" 적어도 한 수용자로부터 응답을 받습니다. 규칙은: 쿼럼 중 누군가가 이미 어떤 값을 수용했다면, 당신은 그 값을(또는 그 중 최신의 값을) 제안해야 한다는 것입니다. 이 제약이 두 다른 값이 선택되는 것을 막습니다.

진행성(라이브니스), 높은 수준에서

진행성은 시스템이 합리적인 조건(예: 안정적인 리더가 등장하고 네트워크가 결국 메시지를 전달함)에서 결국 무언가를 결정한다는 것입니다. Paxos는 혼란 상태에서의 속도를 약속하지 않습니다; 대신 상황이 안정되면 진행한다는 정당성을 제공합니다.

상태 머신 복제: 공유된 순서를 통한 정확성

관리 패널 추가
어디서든 로그·트레이스·복제본 상태를 확인할 수 있는 Flutter 관리자 클라이언트를 만드세요.

상태 머신 복제(SMR)는 많은 고가용성 시스템 뒤에 있는 실무 패턴입니다: 하나의 서버가 결정을 내리는 대신 여러 복제본이 동일한 명령 시퀀스를 처리합니다.

복제 로그 아이디어

중심에는 복제된 로그가 있습니다: "put key=K value=V"나 "A에서 B로 $10 이체" 같은 순서 있는 명령 목록입니다. 클라이언트는 명령을 모든 복제본에 그냥 보내지 않고 그룹에 제출하며, 시스템은 이러한 명령에 대해 하나의 순서에 합의한 뒤 각 복제본이 그것들을 로컬에서 적용합니다.

왜 순서가 정확성을 주는가

모든 복제본이 동일한 초기 상태에서 동일한 명령을 동일한 순서로 실행하면, 결국 동일한 상태에 도달합니다. 이것이 안전성의 핵심 직관입니다: 여러 기계를 시간으로 동기화하려는 대신, 결정론과 공유된 순서로 동일하게 만듭니다.

이 때문에 합의(예: Paxos/ Raft 스타일)가 SMR과 자주 결합됩니다: 합의는 다음 로그 항목을 결정하고, SMR은 그 결정을 각 복제본의 일관된 상태로 바꿉니다.

실제 시스템에서의 사례

  • 구성 및 리더 선출을 위한 조정 서비스
  • 복제된 쓰기 앞선 로그를 가진 데이터베이스
  • 파티션 엄격 순서를 요구하는 메시지 시스템

엔지니어가 무시할 수 없는 실무적 고려사항

로그는 관리하지 않으면 계속 자랍니다:

  • 스냅샷: 현재 상태를 주기적으로 캡처하여 신규 노드가 전체 기록을 재생하지 않고도 따라잡을 수 있게 합니다.
  • 로그 압축: 스냅샷에 반영되어 더 이상 필요 없는 오래된 로그 항목을 안전하게 폐기합니다.
  • 멤버십 변경: 복제본 추가/제거도 순서화되어야 합니다. 그렇지 않으면 다른 노드들이 누가 그룹에 속하는지에 대해 의견이 달라져 split-brain이 발생할 수 있습니다.

SMR은 마법이 아니라 "순서에 대한 합의"를 "상태에 대한 합의"로 바꾸는 규율적인 방법입니다.

정확성: 안전성, 진행성, 그리고 명확한 명세 작성

분산 시스템은 이상한 방식으로 실패합니다: 메시지는 늦게 도착하고, 노드는 재시작하며, 시계는 어긋나고, 네트워크는 분할됩니다. “정확성”은 감각이 아니라—정확히 명시할 수 있는 약속들의 집합이며, 이를 모든 상황(장애 포함)에 대해 검사할 수 있어야 합니다.

안전성 vs 진행성(구체 예 포함)

안전성은 "나쁜 일이 절대 일어나지 않는다"는 뜻입니다. 예: 복제 키-값 저장소에서 동일한 로그 인덱스에 대해 두 가지 다른 값이 커밋되어서는 안 됩니다. 또는 락 서비스는 같은 락을 두 클라이언트에 동시에 부여해서는 안 됩니다.

진행성은 "언젠가 좋은 일이 일어난다"는 뜻입니다. 예: 과반수 복제본이 살아 있고 네트워크가 결국 메시지를 전달하면 쓰기 요청은 결국 완료됩니다. 락 요청은 무한 대기하지 않고 결국 승인 또는 거부를 받습니다.

안전성은 모순 방지에 관한 것이고 진행성은 영구 정지를 피하는 것입니다.

불변식(Invariants): 비타협 조건

불변식은 모든 도달 가능한 상태에서 항상 유지되어야 하는 조건입니다. 예:

  • "각 로그 인덱스는 최대 하나의 커밋된 값을 가진다."
  • "리더의 임기(term) 번호는 감소하지 않는다."

불변식이 충돌, 타임아웃, 재시작 중에 위반될 수 있다면 그것은 실제로 강제되지 않은 것입니다.

여기서 말하는 '증명'의 의미

증명은 "정상 경로"만이 아니라 모든 가능한 실행을 다루는 논증입니다. 메시지 손실·중복·재정렬; 노드 충돌 및 재시작; 경쟁 리더; 클라이언트 재시도 등 모든 경우를 따져야 합니다.

명세는 놀라운 행동을 방지한다

명확한 명세는 상태, 허용된 동작, 그리고 반드시 지켜야 할 속성을 정의합니다. 이는 "시스템은 일관적이어야 한다" 같은 모호한 요구가 생산에서 당신을 가르치기 전까지 상충하는 기대가 되지 않도록 합니다. 명세는 분할 시 어떤 일이 발생하는지, "커밋"이 무엇을 의미하는지, 클라이언트가 무엇을 신뢰할 수 있는지 등을 미리 정의하게 만듭니다.

이론에서 실무로: TLA+로 모델링하기

Lamport의 가장 실용적인 교훈 중 하나는 분산 프로토콜을 코드보다 높은 수준에서 설계할 수 있고 설계해야 한다는 점입니다. 스레드, RPC, 재시도 루프를 걱정하기 전에 시스템의 규칙: 허용된 동작, 상태가 어떻게 변하는지, 무엇이 절대 일어나면 안 되는지 등을 적어둘 수 있습니다.

TLA+는 무엇을 위한 것인가

TLA+는 동시성과 분산 시스템을 기술하기 위한 명세 언어이자 모델 체킹 도구입니다. 시스템의 간단한 수학적 모델—상태와 전이—와 당신이 신경 쓰는 속성(예: "리더는 한 번에 하나만" 또는 "커밋된 항목은 사라지지 않는다")을 작성합니다.

그다음 모델 체커는 가능한 인터리빙, 메시지 지연, 장애를 탐색하여 위배 사례를 찾아냅니다: 속성을 깨는 구체적 단계들의 시퀀스입니다. 회의에서 가장자리 케이스를 논쟁하는 대신 실행 가능한 논증을 얻습니다.

모델이 잡아낼 수 있는 버그

복제 로그에서의 "커밋" 단계 같은 것은 코드에서 우연히 두 노드가 같은 인덱스에 다른 항목을 커밋하도록 허용할 수 있습니다.

TLA+ 모델은 다음과 같은 트레이스를 드러낼 수 있습니다:

  • 노드 A가 과반수의 응답을 받고 인덱스 10에서 항목 X를 커밋한다.
  • 오래된 데이터를 가진 노드 B도 과반수를 형성하고 인덱스 10에서 항목 Y를 커밋한다.

이것은 안전성 위반입니다(중복 커밋). 프로덕션에서 한 달에 한 번만 나타날 수도 있는 버그가, 포괄적 탐색으로는 금방 드러납니다. 유사한 모델들은 종종 업데이트 손실, 이중 적용, 또는 "인증은 되었지만 영구화되지 않음" 같은 문제를 잡아냅니다.

언제 모델링할 가치가 있는가

TLA+는 리더 선출, 멤버십 변경, 합의와 상호작용하는 모든 프로토콜—즉 순서와 장애 처리가 섞이는 핵심 조정 로직에 가장 가치가 있습니다. 버그가 데이터를 손상시키거나 수동 복구를 요구할 가능성이 있다면 작은 모델을 작성하는 것이 나중에 디버깅하는 것보다 보통 더 저렴합니다.

내부 도구를 이 아이디어들 주위에 구축한다면, 실무 워크플로 중 하나는 가벼운 명세(심지어 비공식적인 것)를 작성한 다음 시스템을 구현하고 명세의 불변식에서 테스트를 생성하는 것입니다. Koder.ai와 같은 플랫폼은 평문으로 의도한 순서/합의 동작을 설명하고 서비스 스캐폴딩(React 프론트엔드, PostgreSQL을 사용하는 Go 백엔드, 또는 Flutter 클라이언트)을 빠르게 반복하며 "절대 일어나면 안 되는 것"을 가시적으로 유지하면서 배포를 가속화하는 데 도움이 될 수 있습니다.

신뢰할 수 있는 시스템을 구축하고 운영하기 위한 실무적 요점들

완전한 소유권 유지
디자인이 마음에 들면 생성된 소스 코드를 저장소에 보내세요.

Lamport가 실무자에게 준 큰 선물은 마인드셋입니다: 시간과 순서를 가정으로 물려받는 것이 아니라 모델링해야 할 데이터로 취급하라는 것입니다. 이 마인드셋은 월요일에 적용할 수 있는 습관들로 이어집니다.

이론을 일상 엔지니어링으로 전환하기

메시지가 지연·중복·재정렬될 수 있다면, 각 상호작용을 그 조건에서 안전하도록 설계하세요.

  • 기본적으로 멱등성(idempotency): "다시 해도 안전"하게 만드세요. 결제, 프로비저닝, 재시도할 수 있는 모든 쓰기에는 멱등성 키를 사용하세요.
  • 중복 제거와 함께하는 재시도: 재시도는 필요하지만 중복 제거 없이는 중복 쓰기를 만듭니다. 요청 ID를 추적하고 "이미 처리됨" 표시를 저장하세요.
  • 적어도 한 번 전달(at-least-once) + 한 번만의 효과(exactly-once effects): 네트워크가 두 번 전달할 수 있음을 받아들이고 상태 변경이 한 번만 적용되도록 하세요.

타임아웃과 시계는 조심해서 다루기

타임아웃은 진리가 아니라 정책입니다. 타임아웃은 단지 "제때 응답을 받지 못했다"는 뜻이지 "상대가 행동하지 않았다"는 뜻이 아닙니다. 구체적 함의:

  • 타임아웃을 결정적 실패로 취급하지 마세요. 보상 및 조정 경로를 설계하세요.
  • 노드 간 이벤트 순서를 매기기 위해 로컬 시계 시간을 사용하지 마세요. 대신 시퀀스 번호, 단조 증가 카운터, 또는 명시적 인과 메타데이터(예: "이 업데이트는 버전 X를 대체한다")를 사용하세요.

인과관계를 존중하는 관찰성(Observability)

좋은 디버깅 도구는 단지 타임스탬프뿐만 아니라 순서를 인코딩합니다.

  • 모든 곳에 트레이스 ID: 모든 홉과 로그 라인에 연관/추적 ID를 전파하세요.
  • 로그에 인과 단서 포함: 메시지 ID, 부모 요청 ID, 결정을 내릴 때 "내가 믿고 있던 최신 버전"을 로그에 남기세요.
  • 결정론적 재실행: 입력(명령)을 기록하여 동작을 재생하고 버그가 타이밍 의존인지 로직 의존인지 확인하세요.

출시 전에 던져야 할 설계 질문들

분산 기능을 추가하기 전에 다음 질문들로 명확성을 강제하세요:

  1. 동일한 요청이 두 번 처리되면 무슨 일이 일어나는가?
  2. 우리가 필요로 하는 순서는 무엇이며(있다면), 어디에서 강제되는가?
  3. 어떤 실패가 "안전한가"(나쁜 상태 없음) vs. "시끄러운가"(사용자에게 보임) vs. "무성(침묵)한가"(숨겨진 손상)?
  4. 부분 장애나 네트워크 분할 후 복구 경로는 무엇인가?
  5. happened-before 이야기를 프로덕션에서 재구성하기 위해 무엇을 로깅할 것인가?

이 질문들은 박사 학위를 요구하지 않습니다—단지 순서와 정확성을 1차 제품 요구사항으로 다루는 규율이 필요할 뿐입니다.

결론과 다음 단계 제안

Lamport의 지속적인 선물은 시스템들이 시계를 공유하지 않고 기본적으로 "무슨 일이 일어났는가"에 동의하지 않을 때 명확하게 사고하는 방법입니다. 완벽한 시간을 쫓는 대신, 당신은 인과관계(무엇이 무엇에 영향을 미쳤는가)를 추적하고, 논리적 시간(Lamport 타임스탬프)으로 이를 표현하며—제품이 하나의 히스토리를 요구한다면—모든 복제본이 동일한 의사결정 순서를 적용하도록 합의를 구축합니다.

이 실마리는 실용적인 엔지니어 마인드셋으로 이어집니다:

먼저 명세하고, 그다음 빌드하라

당신이 필요로 하는 규칙을 적으세요: 절대 일어나면 안 되는 것(안전성)과 결국 일어나야 하는 것(진행성). 그런 다음 그 명세에 맞춰 구현하고 지연, 분할, 재시도, 중복 메시지, 노드 재시작 하에서 시스템을 테스트하세요. 많은 "미스터리한 장애"는 사실 "요청은 두 번 처리될 수 있다" 혹은 "리더는 언제든 바뀔 수 있다" 같은 누락된 가정들입니다.

심화 학습—과도한 형식주의에 빠지지 않고

더 깊게 들어가고 싶다면:

  • Lamport의 “Time, Clocks, and the Ordering of Events in a Distributed System”를 읽어 happened-before를 내재화하세요.
  • “Paxos Made Simple”을 훑어보며 안전성 직관을 익히세요: 한 값이 선택되면 이후 진행이 그것과 모순될 수 없습니다.
  • TLA+ 입문 강연을 보고 작은 프로토콜(예: 락 서비스 또는 두 복제 레지스터)을 모델링하고 체크해보세요.

한 가지 실습 과제

당신이 책임지는 컴포넌트를 골라 한 페이지짜리 "장애 계약(failure contract)"을 작성하세요: 네트워크와 저장소에 대해 어떤 가정을 하는지, 어떤 동작이 멱등인지, 어떤 순서 보장이 제공되는지를 적으세요.

이 연습을 더 구체적으로 만들고 싶다면, 작은 "순서 데모" 서비스를 만들어보세요: 명령을 로그에 추가하는 요청 API, 이를 적용하는 백그라운드 워커, 그리고 인과 메타데이터와 재시도를 보여주는 관리자 뷰를 포함하세요. Koder.ai에서 빠른 스캐폴딩과 배포/호스팅, 실험을 위한 스냅샷/롤백, 만족스러우면 소스 코드 내보내기 기능을 이용하면 반복이 빠릅니다.

잘하면 이러한 아이디어는 암묵적인 동작을 줄여 장애를 줄이고 추론을 단순화합니다: 시간에 대해 논쟁을 멈추고 당신의 시스템에서 순서, 합의, 그리고 정확성이 실제로 무엇을 의미하는지 증명할 수 있게 됩니다.

자주 묻는 질문

Lamport의 아이디어가 오늘날에도 중요한 이유는 무엇인가요?

Lamport의 아이디어는 시계가 서로 맞지 않고 메시지가 늦게, 두 번, 또는 순서와 다르게 도착할 때 기계들이 어떻게 조율하는지 설명합니다. 이런 조건은 매일 데이터베이스, 큐, 결제 흐름, 복제 서비스에 영향을 줍니다.

분산 시스템은 왜 타임스탬프에 의존할 수 없나요?

그렇지 않습니다. 각 기계에는 자체 시계가 있고, 동기화는 오차가 생기거나 멈출 수 있습니다. 사람이 읽는 타임스탬프에는 실제 시각을 쓰되, 정확성이 순서에 달려 있다면 시퀀스 번호나 프로토콜 규칙을 사용하세요.

happened-before는 무엇을 의미하나요?

이벤트 A가 이벤트 B에 영향을 미칠 수 있었다는 뜻입니다. 하나의 프로세스에서 발생한 이벤트에는 순서가 있고, 메시지를 보내는 일은 받는 일보다 먼저 일어납니다. 이 관계는 이벤트 사슬을 따라 이어집니다.

분산 시스템에서 동시 이벤트란 무엇인가요?

동시 이벤트 사이에는 인과 관계가 없습니다. 실제 시간상 가까이 일어났더라도 어느 이벤트도 다른 이벤트에 영향을 주지 않았습니다. 이런 이벤트에 하나의 공통 순서가 필요하다면 시스템이 규칙을 추가해야 합니다.

Lamport 타임스탬프란 무엇인가요?

Lamport 타임스탬프는 각 프로세스가 로컬 이벤트와 메시지에 맞춰 갱신하는 카운터입니다. 인과 순서를 보존합니다. A가 B를 일으켰다면 A의 타임스탬프는 B보다 작습니다. 경과 시간을 측정하지는 않습니다.

언제 전체 순서가 필요한가요?

복제본이 공유 로그에 쓰기를 적용하거나 잔액을 갱신할 때처럼 명령을 똑같이 적용해야 한다면 전체 순서를 사용하세요. 의존성만 추적하면 되고 독립적인 작업을 계속 독립적으로 둘 수 있다면 부분 순서로 충분합니다.

합의는 어떤 문제를 해결하나요?

합의는 장애와 지연된 메시지가 있어도 그룹이 하나의 결정을 내리게 합니다. 시스템은 이를 이용해 리더를 선출하고, 복제 로그 항목을 선택하며, 구성 변경이 충돌하지 않도록 합니다.

Paxos는 스플릿 브레인 결정을 어떻게 막나요?

Paxos는 합의 프로토콜 계열입니다. 번호가 매겨진 제안과 서로 겹치는 과반수를 사용하는 안전 규칙 덕분에 그룹이 값을 선택한 뒤에는 같은 슬롯에 다른 값을 선택할 수 없습니다.

상태 머신 복제가 작동하는 이유는 무엇인가요?

복제본은 같은 상태에서 시작해 같은 결정적 명령을 같은 순서로 실행합니다. 합의가 그 순서를 제공하므로 동기화된 시계에 의존하지 않고도 각 복제본이 같은 결과에 도달합니다.

안전성과 활성의 차이는 무엇인가요?

안전성은 하나의 로그 위치에 두 값을 커밋하는 일처럼 절대 일어나면 안 되는 일을 말합니다. 활성은 네트워크와 노드 과반수가 복구된 뒤 쓰기가 완료되는 일처럼 결국 일어나야 하는 일을 말합니다.

Related posts