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

Leslie Lamport는 이론적 연구가 실제 시스템을 배포할 때마다 등장하는 드문 연구자입니다. 데이터베이스 클러스터, 메시지 큐, 워크플로 엔진 또는 요청을 재시도하고 장애를 견디는 어떤 시스템을 운영해본 적이 있다면, 당신은 Lamport가 명명하고 해결하는 문제들 속에서 살아온 것입니다.
그의 아이디어가 오래가는 이유는 특정 기술에 묶여 있지 않기 때문입니다. 이 아이디어들은 여러 대의 기계가 하나의 시스템처럼 행동하려 할 때 언제나 드러나는 불편한 진실을 설명합니다: 시계는 어긋나고, 네트워크는 지연·손실하며, 장애는 예외가 아니라 정상입니다.
시간: 분산 시스템에서 “지금 몇 시인가?”는 단순한 질문이 아닙니다. 물리 시계는 어긋나고, 각 기계가 관찰하는 이벤트의 순서가 다를 수 있습니다.
순서: 단일 시계를 신뢰할 수 없을 때, 어떤 이벤트가 먼저 일어났는지 말할 다른 방법이 필요합니다—그리고 모두가 동일한 순서를 따르게 해야 하는 경우 이를 강제하는 방법이 필요합니다.
정확성: “보통은 작동한다”는 설계가 아닙니다. Lamport는 (안전성 vs. 진행성) 같은 명확한 정의와 테스트뿐 아니라 논리적으로 따져볼 수 있는 명세로 분야를 이끌었습니다.
우리는 개념과 직관에 집중할 것입니다: 문제, 명확하게 생각하기 위한 최소한의 도구들, 그리고 그 도구들이 실제 설계에 어떤 영향을 미치는지.
지도는 다음과 같습니다:
시스템이 “분산”이라는 것은 여러 대의 기계가 네트워크를 통해 협력하여 하나의 작업을 수행한다는 의미입니다. 이 말은 간단하게 들리지만, 두 가지 사실을 받아들이면 복잡해집니다: 기계는 독립적으로 실패할 수 있고(부분적 실패), 네트워크는 메시지를 지연·손실·중복·재정렬할 수 있습니다.
한 컴퓨터에서 실행되는 단일 프로그램에서는 일반적으로 “무엇이 먼저 일어났는가”를 가리킬 수 있습니다. 그러나 분산 시스템에서는 서로 다른 기계가 서로 다른 사건 순서를 관찰할 수 있고—각각의 관점에서 둘 다 맞을 수 있습니다.
조정을 타임스탬프로 해결하는 것이 유혹적이지만, 기계 간에 의존할 수 있는 단일 시계는 없습니다:
따라서 한 호스트에서 “이벤트 A가 10:01:05.123에 발생했다”는 다른 호스트의 “10:01:05.120”과 신뢰성 있게 비교할 수 없습니다.
네트워크 지연은 당신이 본 것의 순서를 뒤집을 수 있습니다. 먼저 보낸 쓰기가 나중에 도착할 수 있고, 재시도가 원본보다 늦게 도착할 수 있습니다. 두 개의 데이터센터가 같은 요청을 반대 순서로 처리할 수도 있습니다.
이로 인해 디버깅이 독특하게 혼란스러워집니다: 여러 기계의 로그가 서로 다를 수 있고, “타임스탬프별 정렬”은 실제로 일어나지 않았던 서사를 만들어낼 수 있습니다.
단일 타임라인이 존재한다고 가정하면 다음과 같은 구체적 실패가 발생합니다:
Lamport의 핵심 통찰은 여기서 시작합니다: 시간이 공유될 수 없다면, 우리는 순서를 다르게 추론해야 합니다.
분산 프로그램은 이벤트들로 이루어집니다: 특정 노드(프로세스, 서버, 스레드)에서 일어나는 어떤 일입니다. 예로는 “요청 수신”, “행 삽입”, “메시지 전송” 등이 있습니다. 메시지는 노드 간을 잇는 연결고리입니다: 하나의 이벤트는 *전송(send)*이고 다른 하나는 *수신(receive)*입니다.
Lamport의 핵심 통찰은, 신뢰할 수 있는 공유 시계가 없는 시스템에서 가장 확실히 추적할 수 있는 것은 인과관계—어떤 이벤트가 다른 이벤트에 영향을 미쳤을 가능성이 있는지—라는 것입니다.
Lamport는 happened-before라는 간단한 규칙을 정의했습니다. 기호로는 A → B (이벤트 A가 이벤트 B보다 먼저 발생했다)는 다음을 의미합니다:
이 관계는 **부분 순서(partial order)**를 제공합니다: 어떤 쌍들은 순서가 정해지지만, 모든 쌍이 정해지는 것은 아닙니다.
사용자가 “구매”를 클릭합니다. 그 클릭은 API 서버에 요청을 트리거하는 이벤트 A입니다. 서버는 데이터베이스에 주문 행을 쓰는 이벤트 B를 수행합니다. 쓰기가 완료된 후 서버는 “주문 생성” 메시지를 발행하는 이벤트 C를 발생시키고, 캐시 서비스가 이를 받아 캐시 항목을 업데이트하는 이벤트 D가 일어납니다.
여기서는 A → B → C → D입니다. 시계가 어긋나더라도 메시지와 프로그램 구조가 실제 인과 연결을 만듭니다.
두 이벤트는 서로 인과관계가 없을 때 동시적입니다: 즉 (A → B)도 아니고 (B → A)도 아닙니다. 동시성은 “같은 시간”을 의미하지 않습니다—“어떤 인과 경로도 연결하지 않는다”는 뜻입니다. 그래서 두 서비스가 각각 자신이 “먼저” 행동했다고 주장할 수 있고, 특별한 순서 규칙을 추가하지 않으면 둘 다 맞을 수 있습니다.
여러 대의 기계에서 “무엇이 먼저 일어났는가”를 재구성하려 해본 적이 있다면 기본 문제를 마주했을 것입니다: 컴퓨터는 완벽히 동기화된 시계를 공유하지 않습니다. Lamport의 해결책은 완벽한 시간을 쫓는 것을 멈추고 대신 순서를 추적하는 것입니다.
Lamport 타임스탬프는 프로세스의 각 의미 있는 이벤트에 붙이는 숫자입니다(서비스 인스턴스, 노드, 스레드 등). 이를 “이벤트 카운터”로 생각하면, 벽시계 시간이 신뢰할 수 없을 때도 “이 이벤트가 저 이벤트보다 먼저 일어났다”고 일관되게 말할 수 있습니다.
로컬에서 증가: 이벤트를 기록하기 전에(예: “DB에 쓰기”, “요청 전송”, “로그에 항목 추가”) 로컬 카운터를 증가시킵니다.
수신 시 max + 1 적용: 발신자의 타임스탬프가 포함된 메시지를 받으면 카운터를 다음으로 설정합니다:
max(local_counter, received_counter) + 1
그런 다음 수신 이벤트에 그 값을 찍습니다.
이 규칙들은 인과관계를 존중하도록 타임스탬프를 보장합니다: 이벤트 A가 메시지를 통해 간접적이든 직접적이든 이벤트 B에 영향을 미칠 수 있다면 A의 타임스탬프는 B보다 작습니다.
그것들은 인과적 순서에 대해 알려줍니다:
TS(A) < TS(B)라면, A가 B보다 먼저 발생했을 수 있습니다.TS(A) < TS(B)입니다.그들은 실제 시간에 대해서는 알려주지 못합니다:
따라서 Lamport 타임스탬프는 순서를 매기는 데는 훌륭하지만 지연을 측정하거나 “실제 시각이 언제였나”를 묻는 데는 적합하지 않습니다.
서비스 A가 서비스 B를 호출하고 둘 다 감사 로그를 쓴다고 합시다. 인과관계를 보존하는 통합 로그 뷰를 원합니다.
max(local, 42) + 1로 설정하여 예를 들어 43을 찍고 “카드 검증”을 로그로 남깁니다.이제 두 서비스의 로그를 집계할 때 (lamport_timestamp, service_id)로 정렬하면 벽시계가 어긋나거나 네트워크가 지연되었더라도 실제 인과 사슬과 일치하는 안정적이고 설명 가능한 타임라인을 얻습니다.
인과관계는 부분 순서를 제공합니다: 메시지나 의존성으로 연결된 일부 이벤트는 명확히 “이전”으로 정해지지만, 많은 이벤트는 단순히 동시적입니다. 이것은 버그가 아니라 분산 현실의 자연스러운 형태입니다.
“무엇이 이것에 영향을 미쳤을까?”를 디버깅하거나 “응답은 요청 뒤에 와야 한다” 같은 규칙을 강제하려면 부분 순서가 정확히 필요한 것입니다. happened-before 간선만 존중하면 되고, 나머지는 독립적으로 취급할 수 있습니다.
어떤 시스템은 “어느 쪽 순서든 괜찮다”는 것을 허용할 수 없습니다. 특히 다음의 경우에는 단일 순서가 필요합니다:
전체 순서가 없다면 두 복제본이 지역적으로는 둘 다 “정상”으로 보이지만 전역적으로는 서로 달라질 수 있습니다: 하나는 A 다음 B를 적용하고 다른 하나는 B 다음 A를 적용하면 서로 다른 결과가 나옵니다.
순서를 만들어내는 메커니즘을 도입합니다:
전체 순서는 강력하지만 대가가 있습니다:
설계 선택은 명확합니다: 올바른(shared narrative) 서사가 필요하면 해당 비용을 지불하고 조정을 얻어야 합니다.
합의(consensus)는 여러 대의 기계가 하나의 결정—하나의 값, 하나의 리더, 하나의 구성—에 동의하게 만드는 문제입니다. 각 기계는 자신의 로컬 이벤트와 도착한 메시지들만 볼 수 있습니다.
이것은 간단해 보이지만 분산 시스템의 제약을 기억하면 복잡해집니다: 메시지는 지연·중복·재정렬·손실될 수 있고, 기계는 충돌 후 재시작할 수 있으며 어떤 노드가 "확실히 죽었다"는 신호를 얻기 어렵습니다. 합의는 이러한 조건 하에서 동의를 안전하게 만드는 문제입니다.
두 노드가 일시적으로 통신할 수 없으면(네트워크 분할) 각 쪽이 독자적으로 앞으로 나아가려 할 수 있습니다. 양쪽이 다른 값을 결정하면 split-brain(분열된 두 독립 운영) 현상이 발생할 수 있습니다: 두 리더, 서로 다른 구성, 또는 경쟁하는 이력들이 생길 수 있습니다.
분할이 없더라도 지연만으로도 문제가 됩니다. 어떤 노드가 제안을 들었을 때 다른 노드들은 이미 진행했을 수 있습니다. 공유된 시계가 없으면 물리적 시간 때문에 “제안 A가 제안 B보다 먼저 일어났다”라고 신뢰할 수 없습니다.
매일 “합의”라고 부르진 않더라도 인프라에서 흔히 마주칩니다:
각 경우 시스템은 모두가 수렴할 수 있는 단일 결과가 필요하거나, 적어도 상충하는 결과가 동시에 유효하지 않도록 규칙을 가져야 합니다.
Lamport의 Paxos는 이 “안전한 합의” 문제에 대한 기본적인 해법입니다. 핵심 아이디어는 마법 같은 타임아웃이나 완벽한 리더가 아니라 오직 한 값만 선택될 수 있도록 하는 규칙 집합입니다. 이 규칙들은 메시지가 지연되거나 노드가 실패해도 "두 값이 선택되는 일"을 방지합니다.
Paxos는 안전성(safety)과 진행성(liveness)을 분리합니다: "두 다른 값이 선택되지 않는 것"은 보장하고, "언젠가 무언가가 선택된다"는 것은 상황이 안정되면 보장합니다. 이를 통해 실전 적용에서 성능튜닝을 하면서도 핵심 보장을 유지할 수 있습니다.
Paxos는 난해하다는 평판이 있지만, 그 이유의 상당 부분은 "Paxos"가 하나의 간단한 알고리즘이 아니라 비슷한 패턴들의 집합이기 때문입니다. 핵심은 그룹이 서로 동의하게 만드는 방식으로, 메시지가 지연·중복되거나 기계가 일시적으로 실패해도 동작합니다.
제안하는 사람과 검증하는 사람을 분리해 생각하면 도움이 됩니다.
핵심 구조적 아이디어: 두 과반수는 항상 겹친다는 점입니다. 그 겹침이 안전성이 사는 곳입니다.
Paxos의 안전성은 간단히 말할 수 있습니다: 시스템이 한 값을 결정했다면 두 번째로 다른 값을 결정해서는 안 됩니다—split-brain 결정을 방지합니다.
핵심 직관은 제안에는 번호(투표번호, 마치 발의번호)가 붙는다는 것입니다. 수용자는 더 높은 번호의 제안을 본 뒤에는 더 낮은 번호의 제안을 무시하겠다고 약속합니다. 새로운 제안자가 새 번호로 시도할 때는 먼저 쿼럼에게 지금까지 무엇을 수용했는지 묻습니다.
쿼럼들은 겹치기 때문에, 새로운 제안자는 반드시 최근에 수용된 값을 "기억하는" 적어도 한 수용자로부터 응답을 받습니다. 규칙은: 쿼럼 중 누군가가 이미 어떤 값을 수용했다면, 당신은 그 값을(또는 그 중 최신의 값을) 제안해야 한다는 것입니다. 이 제약이 두 다른 값이 선택되는 것을 막습니다.
진행성은 시스템이 합리적인 조건(예: 안정적인 리더가 등장하고 네트워크가 결국 메시지를 전달함)에서 결국 무언가를 결정한다는 것입니다. Paxos는 혼란 상태에서의 속도를 약속하지 않습니다; 대신 상황이 안정되면 진행한다는 정당성을 제공합니다.
상태 머신 복제(SMR)는 많은 고가용성 시스템 뒤에 있는 실무 패턴입니다: 하나의 서버가 결정을 내리는 대신 여러 복제본이 동일한 명령 시퀀스를 처리합니다.
중심에는 복제된 로그가 있습니다: "put key=K value=V"나 "A에서 B로 $10 이체" 같은 순서 있는 명령 목록입니다. 클라이언트는 명령을 모든 복제본에 그냥 보내지 않고 그룹에 제출하며, 시스템은 이러한 명령에 대해 하나의 순서에 합의한 뒤 각 복제본이 그것들을 로컬에서 적용합니다.
모든 복제본이 동일한 초기 상태에서 동일한 명령을 동일한 순서로 실행하면, 결국 동일한 상태에 도달합니다. 이것이 안전성의 핵심 직관입니다: 여러 기계를 시간으로 동기화하려는 대신, 결정론과 공유된 순서로 동일하게 만듭니다.
이 때문에 합의(예: Paxos/ Raft 스타일)가 SMR과 자주 결합됩니다: 합의는 다음 로그 항목을 결정하고, SMR은 그 결정을 각 복제본의 일관된 상태로 바꿉니다.
로그는 관리하지 않으면 계속 자랍니다:
SMR은 마법이 아니라 "순서에 대한 합의"를 "상태에 대한 합의"로 바꾸는 규율적인 방법입니다.
분산 시스템은 이상한 방식으로 실패합니다: 메시지는 늦게 도착하고, 노드는 재시작하며, 시계는 어긋나고, 네트워크는 분할됩니다. “정확성”은 감각이 아니라—정확히 명시할 수 있는 약속들의 집합이며, 이를 모든 상황(장애 포함)에 대해 검사할 수 있어야 합니다.
안전성은 "나쁜 일이 절대 일어나지 않는다"는 뜻입니다. 예: 복제 키-값 저장소에서 동일한 로그 인덱스에 대해 두 가지 다른 값이 커밋되어서는 안 됩니다. 또는 락 서비스는 같은 락을 두 클라이언트에 동시에 부여해서는 안 됩니다.
진행성은 "언젠가 좋은 일이 일어난다"는 뜻입니다. 예: 과반수 복제본이 살아 있고 네트워크가 결국 메시지를 전달하면 쓰기 요청은 결국 완료됩니다. 락 요청은 무한 대기하지 않고 결국 승인 또는 거부를 받습니다.
안전성은 모순 방지에 관한 것이고 진행성은 영구 정지를 피하는 것입니다.
불변식은 모든 도달 가능한 상태에서 항상 유지되어야 하는 조건입니다. 예:
불변식이 충돌, 타임아웃, 재시작 중에 위반될 수 있다면 그것은 실제로 강제되지 않은 것입니다.
증명은 "정상 경로"만이 아니라 모든 가능한 실행을 다루는 논증입니다. 메시지 손실·중복·재정렬; 노드 충돌 및 재시작; 경쟁 리더; 클라이언트 재시도 등 모든 경우를 따져야 합니다.
명확한 명세는 상태, 허용된 동작, 그리고 반드시 지켜야 할 속성을 정의합니다. 이는 "시스템은 일관적이어야 한다" 같은 모호한 요구가 생산에서 당신을 가르치기 전까지 상충하는 기대가 되지 않도록 합니다. 명세는 분할 시 어떤 일이 발생하는지, "커밋"이 무엇을 의미하는지, 클라이언트가 무엇을 신뢰할 수 있는지 등을 미리 정의하게 만듭니다.
Lamport의 가장 실용적인 교훈 중 하나는 분산 프로토콜을 코드보다 높은 수준에서 설계할 수 있고 설계해야 한다는 점입니다. 스레드, RPC, 재시도 루프를 걱정하기 전에 시스템의 규칙: 허용된 동작, 상태가 어떻게 변하는지, 무엇이 절대 일어나면 안 되는지 등을 적어둘 수 있습니다.
TLA+는 동시성과 분산 시스템을 기술하기 위한 명세 언어이자 모델 체킹 도구입니다. 시스템의 간단한 수학적 모델—상태와 전이—와 당신이 신경 쓰는 속성(예: "리더는 한 번에 하나만" 또는 "커밋된 항목은 사라지지 않는다")을 작성합니다.
그다음 모델 체커는 가능한 인터리빙, 메시지 지연, 장애를 탐색하여 위배 사례를 찾아냅니다: 속성을 깨는 구체적 단계들의 시퀀스입니다. 회의에서 가장자리 케이스를 논쟁하는 대신 실행 가능한 논증을 얻습니다.
복제 로그에서의 "커밋" 단계 같은 것은 코드에서 우연히 두 노드가 같은 인덱스에 다른 항목을 커밋하도록 허용할 수 있습니다.
TLA+ 모델은 다음과 같은 트레이스를 드러낼 수 있습니다:
이것은 안전성 위반입니다(중복 커밋). 프로덕션에서 한 달에 한 번만 나타날 수도 있는 버그가, 포괄적 탐색으로는 금방 드러납니다. 유사한 모델들은 종종 업데이트 손실, 이중 적용, 또는 "인증은 되었지만 영구화되지 않음" 같은 문제를 잡아냅니다.
TLA+는 리더 선출, 멤버십 변경, 합의와 상호작용하는 모든 프로토콜—즉 순서와 장애 처리가 섞이는 핵심 조정 로직에 가장 가치가 있습니다. 버그가 데이터를 손상시키거나 수동 복구를 요구할 가능성이 있다면 작은 모델을 작성하는 것이 나중에 디버깅하는 것보다 보통 더 저렴합니다.
내부 도구를 이 아이디어들 주위에 구축한다면, 실무 워크플로 중 하나는 가벼운 명세(심지어 비공식적인 것)를 작성한 다음 시스템을 구현하고 명세의 불변식에서 테스트를 생성하는 것입니다. Koder.ai와 같은 플랫폼은 평문으로 의도한 순서/합의 동작을 설명하고 서비스 스캐폴딩(React 프론트엔드, PostgreSQL을 사용하는 Go 백엔드, 또는 Flutter 클라이언트)을 빠르게 반복하며 "절대 일어나면 안 되는 것"을 가시적으로 유지하면서 배포를 가속화하는 데 도움이 될 수 있습니다.
Lamport가 실무자에게 준 큰 선물은 마인드셋입니다: 시간과 순서를 가정으로 물려받는 것이 아니라 모델링해야 할 데이터로 취급하라는 것입니다. 이 마인드셋은 월요일에 적용할 수 있는 습관들로 이어집니다.
메시지가 지연·중복·재정렬될 수 있다면, 각 상호작용을 그 조건에서 안전하도록 설계하세요.
타임아웃은 진리가 아니라 정책입니다. 타임아웃은 단지 "제때 응답을 받지 못했다"는 뜻이지 "상대가 행동하지 않았다"는 뜻이 아닙니다. 구체적 함의:
좋은 디버깅 도구는 단지 타임스탬프뿐만 아니라 순서를 인코딩합니다.
분산 기능을 추가하기 전에 다음 질문들로 명확성을 강제하세요:
이 질문들은 박사 학위를 요구하지 않습니다—단지 순서와 정확성을 1차 제품 요구사항으로 다루는 규율이 필요할 뿐입니다.
Lamport의 지속적인 선물은 시스템들이 시계를 공유하지 않고 기본적으로 "무슨 일이 일어났는가"에 동의하지 않을 때 명확하게 사고하는 방법입니다. 완벽한 시간을 쫓는 대신, 당신은 인과관계(무엇이 무엇에 영향을 미쳤는가)를 추적하고, 논리적 시간(Lamport 타임스탬프)으로 이를 표현하며—제품이 하나의 히스토리를 요구한다면—모든 복제본이 동일한 의사결정 순서를 적용하도록 합의를 구축합니다.
이 실마리는 실용적인 엔지니어 마인드셋으로 이어집니다:
당신이 필요로 하는 규칙을 적으세요: 절대 일어나면 안 되는 것(안전성)과 결국 일어나야 하는 것(진행성). 그런 다음 그 명세에 맞춰 구현하고 지연, 분할, 재시도, 중복 메시지, 노드 재시작 하에서 시스템을 테스트하세요. 많은 "미스터리한 장애"는 사실 "요청은 두 번 처리될 수 있다" 혹은 "리더는 언제든 바뀔 수 있다" 같은 누락된 가정들입니다.
더 깊게 들어가고 싶다면:
당신이 책임지는 컴포넌트를 골라 한 페이지짜리 "장애 계약(failure contract)"을 작성하세요: 네트워크와 저장소에 대해 어떤 가정을 하는지, 어떤 동작이 멱등인지, 어떤 순서 보장이 제공되는지를 적으세요.
이 연습을 더 구체적으로 만들고 싶다면, 작은 "순서 데모" 서비스를 만들어보세요: 명령을 로그에 추가하는 요청 API, 이를 적용하는 백그라운드 워커, 그리고 인과 메타데이터와 재시도를 보여주는 관리자 뷰를 포함하세요. Koder.ai에서 빠른 스캐폴딩과 배포/호스팅, 실험을 위한 스냅샷/롤백, 만족스러우면 소스 코드 내보내기 기능을 이용하면 반복이 빠릅니다.
잘하면 이러한 아이디어는 암묵적인 동작을 줄여 장애를 줄이고 추론을 단순화합니다: 시간에 대해 논쟁을 멈추고 당신의 시스템에서 순서, 합의, 그리고 정확성이 실제로 무엇을 의미하는지 증명할 수 있게 됩니다.