운영체제 아주 쉬운 세가지 이야기 18주차(분산 시스템)

2026. 1. 6. 17:36개념 공부

분산시스템의 출발점은 간단합니다. 컴포넌트는 언제든 고장나고, 특히 “통신”은 근본적으로 신뢰할 수 없다는 전제에서 설계를 시작해야 합니다. 이 문서도 그 전제를 깔고, (1) 네트워크/메시지 전달의 불안정성, (2) 그 위에 신뢰성(reliability)을 올리는 방법, (3) RPC 같은 “프로그래밍 모델(추상화)”이 왜 필요한지를 설명합니다.


1) 통신은 왜 “근본적으로” 신뢰할 수 없나?

네트워크에서 패킷은 자주 유실(lost), 손상(corrupted), 혹은 목적지에 도달하지 못하는 일이 생깁니다. 이유는 단순한 비트 플립(전기적 문제)부터 케이블 단선 같은 하드웨어 고장까지 다양하지만, 더 “본질적인” 이유는 버퍼링 한계로 인한 드랍(drop)입니다. 라우터/스위치/호스트는 들어오는 패킷을 메모리에 잠깐 쌓아 처리하는데, 폭주하면 결국 일부를 버릴 수밖에 없습니다. 즉, 모든 장비가 정상이어도 패킷 유실은 발생합니다.

UDP 예시가 등장하는 이유

UDP는 “신뢰성”을 거의 제공하지 않는 대표적인 통신 계층입니다. 패킷이 유실돼도 송신자는 통지를 못 받을 수 있고, 순서 보장도 약합니다. 대신 체크섬(checksum)으로 일부 손상(corruption) 정도만 감지합니다.


2) 체크섬(checksum): “손상 감지”는 되지만, “전달 보장”은 아니다

체크섬은 메시지 바이트들로부터 짧은 요약값을 만들고, 수신 측이 다시 계산해서 비교함으로써 전송 중 데이터가 깨졌는지를 어느 정도 감지합니다.

중요한 트레이드오프도 같이 기억해야 합니다: 강한 체크섬일수록(효과성↑) 계산 비용이 커져(성능↓) 둘이 충돌합니다. 

체크섬은 “깨짐(corruption)”만 잡습니다.
“유실(loss)”, “중복(duplicate)”, “재전송(retry)”, “순서(out-of-order)”는 다른 메커니즘이 필요합니다.

3) 신뢰성 있는 통신 레이어: ACK + Timeout/Retry + (중복 방지)

3-1. ACK(acknowledgment): “받았다”를 알려줘야 한다

송신자가 메시지를 보냈을 때, 수신자가 짧은 확인 응답(ACK)을 보내면 송신자는 “아, 목적지에 도착했구나”를 알 수 있습니다. 

3-2. Timeout/Retry: ACK가 안 오면 어떻게 할까?

ACK가 안 오면, 송신자는 메시지 유실을 의심해야 합니다. 그래서 타이머(timeout)를 걸고, 일정 시간 내 ACK가 없으면 재전송(retry)합니다. 이때 재전송을 위해 송신자는 보낸 메시지 사본을 보관해야 합니다.

// 핵심 흐름 (개념)
// send(msg)
// keep copy
// set timer
// if ack arrives: delete copy, stop timer
// if timer fires: resend(msg), reset timer

위 흐름 자체가 문서에 그림으로 제시됩니다.

3-3. “ACK 유실”이 더 골치다: 중복 메시지가 생긴다

문제는 “원본 메시지”가 아니라 ACK가 유실되는 경우입니다. 송신자 입장에서는 ACK가 안 왔으니 재전송을 하게 되고, 수신자 입장에서는 같은 메시지를 두 번 받는 중복(duplicate) 상황이 됩니다. 파일 다운로드 같은 케이스에서 중복 패킷이 섞이면 데이터가 망가질 수 있죠.

3-4. 중복 방지: Sequence Counter(시퀀스 번호)

중복을 감지하려면 메시지마다 ID가 필요합니다. 하지만 모든 ID를 영원히 기억하면 메모리가 무한히 필요하니, 문서는 시퀀스 카운터(sequence counter)를 제시합니다. 송신/수신이 카운터 값을 공유하고, 메시지에 (N)을 붙여 보내 ID처럼 쓰는 방식입니다. 

수신자는 같은 시퀀스 번호의 메시지를 다시 받으면 ACK는 보내되, 애플리케이션에는 두 번 전달하지 않아 “정확히 한 번(exactly-once)처럼 보이게” 만듭니다.

문서가 말하는 “exactly-once semantics”는 통신 레벨에서의 중복 전달 억제를 강조합니다.
하지만 현실의 백엔드에서는 서버 크래시/재시작, DB 커밋 타이밍, 사이드이펙트(결제/메일발송) 때문에 “진짜 exactly-once”는 훨씬 어렵습니다. 그래서 보통은 at-least-once + idempotency(멱등성)로 풀어갑니다.

4) 분산시스템의 ‘추상화’: DSM vs RPC

4-1. DSM(Distributed Shared Memory): 메모리처럼 보이게 만들기

DSM은 여러 머신이 하나의 공유 메모리를 쓰는 것처럼 보이게 하는 접근입니다. 로컬 가상 메모리처럼 접근하지만, 어떤 페이지가 원격에 있으면 그 페이지를 가져오거나 동기화해야 합니다.

4-2. RPC(Remote Procedure Call): 함수 호출처럼 보이게 만들기

RPC는 “원격 호출”을 “로컬 함수 호출”처럼 보이게 만드는 추상화입니다. 클라이언트는 그냥 함수를 부르는 것처럼 호출하고, 내부적으로는 메시지를 보내 서버가 실행 후 결과를 돌려줍니다.


5) RPC가 동작하는 내부: Stub / (Un)marshaling / Runtime

5-1. Stub(스텁)과 Stub Generator(스텁 컴파일러)

RPC의 핵심은 “인자/리턴값을 네트워크로 보낼 수 있는 바이트열로 바꿔서 주고받기”입니다. 클라이언트 스텁은 호출 인자를 메시지로 마샬링(marshaling, serialization)하고, 서버 쪽에서는 이를 언마샬링(unmarshaling, deserialization)하여 실제 함수를 호출합니다.

5-2. 포인터/복잡한 인자는 왜 어렵나?

로컬 프로세스에서의 “포인터”는 주소 공간 내부 의미일 뿐이라, 그대로 원격으로 보낼 수 없습니다. 그래서 RPC는 포인터를 받으면 “어떤 바이트들을 보내야 하는지”를 별도 규칙(잘 알려진 타입, 사이즈 정보, 어노테이션 등)으로 알아야 합니다.

5-3. 서버 동시성: 단일 루프 vs 스레드풀

서버가 요청을 하나씩 순서대로 처리하면, 한 요청이 I/O로 막히는 동안 전체가 놀게 됩니다. 문서는 이를 피하기 위해 스레드풀(thread pool) 같은 동시성 구조를 언급합니다.

5-4. Runtime(런타임 라이브러리)의 현실 문제들

  • 이름/서비스 위치 찾기(naming): 클라이언트는 서버의 주소(IP/호스트명)와 포트 등을 알아야 하고, 네트워크는 그 주소로 라우팅할 수 있어야 합니다.
  • TCP 위에 RPC를 얹으면 왜 비효율이 생기나? 요청/응답 형태(RPC)를 신뢰적 전송(TCP 같은 ack+retransmit) 위에 올리면 “추가 ACK 메시지”가 붙어 오버헤드가 생길 수 있고, 그래서 일부 RPC는 UDP 같은 비신뢰 계층 위에서 자체적으로 timeout/retry를 구현하기도 합니다.
  • 정확히 한 번 vs 최대 한 번: 시퀀스 번호 등을 통해 “실패가 없을 때는 exactly once”, 실패가 있을 때는 “at most once” 같은 의미를 목표로 합니다.
  • 롱런 RPC: 호출이 오래 걸리면 타임아웃 때문에 실패로 보이고 재시도가 발생합니다. 그래서 “명시적 ACK(작업 접수 ACK)” 같은 주의가 필요합니다.

6) Aside: End-to-End Argument — “진짜 보장은 맨 위(애플리케이션)에서”

문서는 end-to-end argument를 통해 중요한 원칙을 강조합니다. 예를 들어 “파일 A→B 전송에서 바이트가 완전히 동일함”을 보장하려면, 네트워크/디스크 같은 하위 계층의 신뢰성만으로는 부족하고, 애플리케이션 레벨에서 end-to-end 검증(예: 전체 파일 해시 비교)이 필요하다는 주장입니다.

백엔드 적용 예시:
“TCP가 보장하니까 OK”가 아니라, 결제/정산/로그 적재처럼 치명적인 데이터는
업무 레벨 checksum, idempotency key, 상태머신 같은 end-to-end 장치를 반드시 둡니다.

7) (요청사항) CAP 이론과의 연결 — 이 문서가 ‘왜 CAP로 이어지는가’

“통신은 unreliable”은 결국 네트워크 분할(partition)과 동치 수준의 사건(장시간 단절/유실/지연 폭증)을 언제든 맞이할 수 있다는 뜻입니다.

그리고 CAP의 질문은 여기서 시작합니다: partition이 발생했을 때도 ‘항상 일관성(Consistency)’을 지킬 것인가, 아니면 ‘항상 응답(Availability)’을 지킬 것인가?
백엔드 실무에서는 “타임아웃/리트라이/RPC”가 바로 그 선택을 코드로 드러내는 지점입니다. 예: 타임아웃을 짧게 두면 가용성(빨리 실패/대체 경로) 쪽으로, 길게 두면 일관성(결과를 기다림) 쪽으로 기울기 쉽습니다.


8) 이 장을 다 읽고 나면 챙길 것

  • Timeout은 “실패 탐지”가 아니라 “내가 포기하는 시간”이다. 너무 짧으면 오탐(불필요 재시도) 폭증, 너무 길면 사용자 체감 장애.
  • Retry는 공짜가 아니다. 재시도는 부하를 증폭시키고(“폭주 시 더 폭주”), 중복 실행을 만든다. (멱등성 키, 중복 제거 저장소가 필요)
  • 중복 제거는 “통신 레벨”만으로 끝나지 않는다. DB 커밋/외부 API 호출/메시지 발행까지 포함한 “업무 레벨” 중복 제거가 핵심.
  • 롱런 작업은 RPC로 “기다리기”보다 비동기 잡으로 쪼개라. (접수 ACK + job id + 폴링/SSE/웹훅)
  • 관측가능성(로그/트레이싱): 타임아웃·재시도·중복이 생기는 시스템은 반드시 correlation id가 필요하다.

마무리

이 장의 메시지는 하나로 요약됩니다: “신뢰할 수 없는 통신 위에, 프로토콜(ACK/timeout/retry/sequence)과 추상화(RPC)를 쌓아 신뢰를 만든다.”