Software Architecture/Distributed Systems
Distributed Systems - 01. 분산의 8가지 오해
분산의 8가지 오해 — 로컬 호출과 원격 호출은 같지 않다
2017년 한 팀이 주문 서비스를 마이크로서비스로 쪼갰다. 모놀리스 시절 한 메서드 안에 있던 inventory.reserve(order)와 payment.charge(order)가 HTTP 호출로 바뀌었다. 코드는 똑같아 보였다 — 메서드 호출을 클라이언트 호출로 바꿨을 뿐. 3주 뒤 프로덕션에서 장애가 터졌다. 결제 서비스가 30초간 응답하지 않았고, 주문 서비스의 스레드 풀이 고갈됐다. 모놀리스 시절엔 결제가 느려지면 사용자가 잠깐 기다리는 걸로 끝났다. 마이크로서비스에선 회사 전체가 30분간 주문을 못 받았다. 로컬 호출을 원격 호출로 치환하는 게 얼마나 다른 결과를 낳는지, 그 팀은 비용으로 배웠다.
이 글이 다루는 질문: 로컬 호출과 원격 호출은 왜 근본적으로 다른가. 그리고 "다르다"를 인정할 때 어떤 설계가 따라오나.
비유로 감 잡기 — 같은 방에서 말하기 vs 전화
같은 방에 앉은 동료에게 말하는 것과, 다른 도시에 있는 동료에게 전화하는 것의 차이를 생각해보자. 같은 방에서 말하면 상대방은 듣고 (대부분) 이해하고 바로 답한다. 통신 비용은 0에 가깝고, 상대가 안 들으면 "잘 안 들려"라고 즉시 알린다. 상대가 자리에 없으면 빈자리가 보이니까 금방 안다.
전화는 다르다. 전화는 안 될 수 있다 (신뢰성). 상대가 받을 때까지 몇 번 울려야 할 수도 있다 (지연). 통화 중이면 끊어야 한다 (경합). 중간에 회선이 끊길 수 있다 (부분 장애). 상대가 누구인지 잘못 걸었을 수도 있다 (토폴로지). 누가 엿들을 수도 있다 (보안). 통화 요금이 과금될 수도 있다 (비용). 전화기 모델이 달라 통화 품질이 다를 수 있다 (이질성).
로컬 호출은 같은 방에서 말하기에 가깝다. 메모리 공간을 공유하고, 함수 호출 한 번이면 끝나며, 실패하면 호출자에게 즉시 예외가 돌아온다. 원격 호출은 전화에 가깝다. 네트워크를 거치고, 응답이 안 올 수도 있으며, 중간에 끊길 수도 있다. 메서드 시그니처는 같아도, 그 아래에서 일어나는 일은 완전히 다르다. 분산 시스템을 다루는 모든 어려움의 출발점이 이 차이다.
비유의 한계 — 전화 비유는 '인간 소통'을 강조하지만, 컴퓨터의 원격 호출은 인간보다 훨씬 더 빠르고 빈번하게 일어난다. 하루 1억 번의 "전화"가 오가는 시스템도 흔하다. 또한 인간은 맥락으로 오류를 메우지만, 컴퓨터는 명시적 처리 없이는 메우지 못한다. 비유는 '빈도·맥락 메움'에서 무너진다.
8가지 오해의 유래
1994년, Sun Microsystems의 Peter Deutsch는 분산 컴퓨팅에서 개발자가 흔히 빠지는 잘못된 가정을 목록으로 정리했다. 처음엔 7개였고, 나중에 James Gosling이 8번째를 추가했다. 이 목록은 단순한 조언이 아니라 — 30년이 지난 지금도 분산 시스템 장애의 대부분을 설명하는, 사실상 불변의 진리다. 네트워크는 1994년이나 2026년이나 근본적으로 같은 속성을 갖는다.
Deutsch의 8가지 오해를 하나씩 본다. 각 오해의 형태는 "~라고 가정한다"다. 그 가정이 깨지는 순간 시스템이 어떻게 부서지는지가 이 영역 전체(12편)의 출발점이다.
오해 1 — 네트워크는 신뢰할 수 있다
가장 치명적인 오해. "메시지를 보내면 도착한다"는 가정이다. 현실은 — 패킷은 라우터에서 버려질 수 있고, 스위치는 장애날 수 있고, DNS 조회는 실패할 수 있고, 상대 노드는 죽어있을 수 있다. 한 연구에 따르면, 실제 데이터센터 네트워크에서도 월 수백 건의 패킷 손실·연결 장애가 일어난다.
// 안티패턴 — 네트워크가 신뢰할 수 있다고 가정
class OrderService {
void placeOrder(Order order) {
inventoryClient.reserve(order); // 응답 안 오면 영원히 블록
paymentClient.charge(order); // 결제 서비스 다운 시?
// 예외 처리 없음 — "안 되면 안 되겠지" 식 가정
}
}
이 코드는 모놀리스에서는 작동했다. 로컬 호출은 실패해도 즉시 예외가 돌아오므로. HTTP 호출에서는 — 응답이 안 올 수 있다. 기본 설정의 HTTP 클라이언트는 무한정 기다린다. 요청이 몰리면 모든 스레드가 "응답 대기" 상태로 빠지고, 시스템 전체가 멈춘다. 앞서 도입부 사례의 장애가 정확히 이 형태였다.
대응: timeout, retry, circuit breaker (12편에서 깊이 다룸). "네트워크 호출은 항상 실패할 수 있다"는 전제로 코드를 짠다.
오해 2 — 지연은 0이다
로컬 함수 호출은 나노초다. 원격 호출은 밀리초다. 백만 배 차이. 이 차이를 무시하면 — 로컬 시절엔 한 번에 끝나던 작업이 분산에선 수십 번의 직렬 호출로 느려진다.
// 안티패턴 — 각 호출이 로컬인 것처럼 N+1 직렬 호출
class OrderService {
List<OrderDetail> getDetails(List<OrderId> ids) {
return ids.stream()
.map(id -> orderClient.find(id)) // N번의 HTTP 호출, 직렬
.toList();
// 100개 주문이면 100 * 50ms = 5초
}
}
이 코드는 로컬 메모리 조회였다면 1ms 만에 끝났다. 원격 호출 100번 직렬로 하면 5초. 사용자는 페이지가 안 뜬다고 생각하고 새로고침을 누르고, 그러면 더 많은 요청이 몰린다 — 장애의 악순환.
대응: batch 호출(한 번에 여러 ID 조회), 캐싱, 비동기 처리. "원격 호출은 비싸다"는 전제로 설계.
오해 3 — 대역폭은 무한하다
1994년에는 모뎀 시대였다. 2026년엔 기가비트 이더넷이 일상이다. 그런데도 대역폭은 무한하지 않다. 한 노드가 처리할 수 있는 데이터의 양은 물리적 한계가 있다. 특히 직렬화된 객체를 크게 주고받으면 — JSON 응답 하나가 10MB면, 초당 1000 요청은 초당 10GB. 그 정도면 네트워크 인터페이스가 포화된다.
// 안티패턴 — 한 번에 모든 데이터를 가져옴
class ReportService {
Report yearlyReport(Year year) {
List<Order> allOrders = orderClient.findAllOfYear(year); // 수백만 건
// 네트워크로 수백 MB를 직렬화해 전송
return aggregate(allOrders);
}
}
로컬이라면 메모리 부하만 신경 쓰면 된다. 분산에선 — 그 데이터가 네트워크를 건너가야 한다. 직렬화 비용, 전송 비용, 역직렬화 비용이 전부 가중된다.
대응: 페이징(pagination), 필요한 필드만 전송(projection), 압축, 데이터를 보내지 않고 연산만 보내기(server-side aggregation). 05편(파티셔닝)·08편(멱등)·10편(일관적 해싱)에서 이 주제가 다시 등장한다.
오해 4 — 네트워크는 안전하다
"내부망이니까 괜찮다"는 가정. 현실은 — 내부망 침투 사고의 대부분은 한 노드가 뚫려서 시작된다. lateral movement라 부르는, 한 번 뚫린 노드에서 다른 내부 노드로 퍼지는 공격이 흔하다. 2017년 Equifax 침해, 2013년 Target 침해가 이 패턴이었다.
이 오해가 분산 시스템 설계에 미치는 영향은 — 서비스 간 통신에 암호화(mTLS)와 인증이 기본이어야 한다는 점이다. "내부망이니까 평문 HTTP로"는 더 이상 유효한 가정이 아니다. Zero-trust network라 부르는 접근은 — "네트워크가 안전하다"는 오해를 버리고, 모든 통신을 신뢰하지 않는 것에서 출발한다.
대응: mTLS (상호 인증), 서비스 간 인가, 감사 로깅. 11편(서비스 디스커버리·게이트웨이)에서 인증의 중앙화를 다룬다.
오해 5 — 토폴로지는 변하지 않는다
"노드 A의 IP는 항상 10.0.0.5다"라는 가정. 현실은 — 노드는 재시작되고 IP는 바뀌고, 오토스케일링으로 새 노드가 생기고, 장애로 노드가 사라진다. 설정 파일에 IP를 박아넣으면 첫 번째 재시작에 깨진다.
# 안티패턴 — IP를 하드코딩
order_service:
url: http://10.0.0.5:8080 # 이 노드가 재시작되면?
이 문제를 푸는 게 서비스 디스커버리(11편)다. 노드는 동적으로 등록되고, 클라이언트는 조회를 통해 현재 살아있는 노드를 찾는다. Kubernetes의 Service, DNS 기반 디스커버리, etcd/Consul 같은 서비스 레지스트리가 이 역할을 한다.
대응: 서비스 디스커버리(11편), 로드 밸런싱, 동적 재구성.
오해 6 — 관리자는 하나다
"전체 시스템을 한 관리자가 통제한다"는 가정. 현실의 분산 시스템은 — 여러 팀, 여러 부서, 심지어 여러 회사가 관여한다. 한 서비스는 내부 팀이, 다른 서비스는 외부 SaaS가, DB는 다른 팀이, 네트워크는 또 다른 팀이 관리한다. 장애가 나면 "내" 영역이 어디까지인지, "남"의 영역이 어디서부터인지 경계가 모호해진다.
이 오해가 미치는 영향 — 장애 대응이 복잡해진다. 모놀리스에선 한 팀이 전체 스택을 디버깅했다. 분산 시스템에선 "내 서비스엔 문제 없어, 문제는 네트워크/DB/상대 서비스에 있어"라는 상호 책임 전가가 일어난다. 이를 해결하는 게 관측성(observability)이다 — 분산 추적(03-architectural-styles/06-microservices-pitfalls.md 참조), 메트릭, 로그의 중앙 집중.
대응: 분산 추적(Jaeger, Zipkin), 명확한 SLA/SLO, 책임 경계(ownership boundary).
오해 7 — 전송 비용은 0이다
"메시지를 보내는 건 공짜다"는 가정. 현실은 — 클라우드 환경에서 네트워크 전송은 과금된다. AWS에서 같은 리전 내 전송은 1GB당 1센트 수준이지만, 리전 간이나 인터넷으로 나가는 전송은 훨씬 비싸다. 하루 1TB를 리전 간에 주고받으면 한 달에 수천 달러.
또한 CPU 비용도 있다. 직렬화/역직렬화는 CPU를 잡아먹는다. JSON 100MB를 파싱하는 데는 수백 밀리초. Protocol Buffers는 더 빠르지만 여전히 공짜가 아니다.
대응: 데이터 지역성(data locality) — 연산을 데이터가 있는 곳으로 보내기(데이터를 연산으로 보내지 말고). 캐싱. 적절한 직렬화 포맷 선택(Avro, Protobuf).
오해 8 — 네트워크는 균일하다
Gosling이 추가한 8번째. "모든 노드의 네트워크 설정, 하드웨어, 소프트웨어가 같다"는 가정. 현실은 — 클라우드에선 같은 리전 안에서도 가용 영역(AZ)마다 지연이 다르고, 하드웨어 세대가 섞여 있고, 운영체제 버전이 다르고, 미들웨어 설정이 다르다. 한 노드는 정상인데 다른 노드는 2배 느린 일이 흔하다.
이 이질성이 미치는 영향은 — 부하 분산이 균등하지 않게 된다. 가장 느린 노드가 전체 처리량을 결정한다. 또한 특정 설정에서만 재현되는 버그가 생긴다 — "내 컴퓨터에선 되는데"의 분산 버전.
대응: 부하 분산 시 노드 용량 가중치 부여, 헬스체크와 느린 노드 격리, 설정의 중앙 관리.
Waldo의 통찰 — 로컬 객체 ≠ 분산 객체
Deutsch의 목록과 같은 해(1994), Jim Waldo와 세 동료는 "A Note on Distributed Computing"이라는 논문에서 한 발 더 나아갔다. 객체 지향의 "투명성(transparency)" — 로컬 객체와 분산 객체를 같은 방식으로 다루려는 시도 — 은 근본적으로 잘못됐다는 주장이었다. 그 근거는 다음 다섯 가지다.
- 지연 — 로컬 호출은 나노초, 원격은 밀리초. 이 차이를 무시하면 성능이 무너진다.
- 메모리 접근 — 로컬 객체는 포인터 참조, 원격은 네트워크 호출. 참조와 값의 구분이 명확해야 한다.
- 부분 장애 — 로컬은 전체 장애(프로세스 죽음) 아니면 정상. 원격은 일부만 실패할 수 있다. 이 부분 장애를 다루는 게 분산 시스템의 핵심 과제.
- 동시성 — 로컬은 보통 단일 스레드 문맥. 원격은 여러 클라이언트가 동시에. 동시성 제어가 필수.
- 보안 — 로컬은 신뢰 경계 안. 원격은 신뢰 경계를 넘는다.
이 다섯 가지가 의미하는 바 — 분산 객체를 로컬 객체처럼 투명하게 다루려는 시도(RMI, CORBA)는 근본적 한계가 있다. 분산을 "숨기려" 하지 말고 명시적으로 다뤄야 한다. REST API, gRPC, 메시지 브로커가 이 명시적 접근의 결과다. 호출이 네트워크를 건넌다는 사실을 코드에 드러내는 게 — 투명성을 포기하는 대신 — 실제로는 더 견고한 시스템을 만든다.
flowchart LR
subgraph Local["로컬 호출 (함수)"]
A[호출자] -->|ns 단위| B[피호출자]
B -->|즉시 반환, 예외 시 즉시 전파| A
end
subgraph Remote["원격 호출 (HTTP/gRPC)"]
C[호출자] -->|직렬화| D1[소켓]
D1 -->|ms 단위| D2[네트워크]
D2 -->|부분 장애 가능| D3[소켓]
D3 --> E[피호출자]
E -->|역방향 동일 경로| C
end
Local -. 같아 보이지만 .-x Remote
설계 사례 — 로컬 호출을 분산으로 안전하게 옮기기
주문 서비스가 재고 서비스를 호출하는 흐름을 세 단계로 발전시킨다. 각 단계가 8가지 오해 중 하나에 대응한다.
1단계 — naive 전환 (8가지 오해 전부 내포)
class OrderService {
private final InventoryClient inventoryClient; // HTTP 클라이언트
public void placeOrder(Order order) {
inventoryClient.reserve(order); // 오해 1, 2 무시 — 타임아웃·재시도 없음
orders.save(order);
}
}
이 코드는 모놀리스 시절 작동하던 코드에서 메서드 호출을 클라이언트 호출로 바꾼 것. 장애 시나리오:
- 재고 서비스가 죽으면 응답 영원히 대기 (오해 1)
- 재고 서비스가 3초 걸리면 스레드 3초 점유 (오해 2)
- 요청 몰리면 스레드 풀 고갈 → 전체 장애
2단계 — timeout과 retry 추가 (오해 1, 2 대응)
class OrderService {
private final InventoryClient inventoryClient;
public void placeOrder(Order order) {
RetrySpec retry = RetrySpec.of(3, Duration.ofMillis(100), Duration.ofSeconds(1));
TimeoutSpec timeout = TimeoutSpec.of(Duration.ofSeconds(2));
try {
inventoryClient.withTimeout(timeout)
.withRetry(retry)
.reserve(order);
orders.save(order);
} catch (TimeoutException e) {
// 재고 예약 실패 — 보상 또는 사용자 알림
throw new OrderFailedException("재고 서비스 응답 없음", e);
}
}
}
timeout 2초, 지수 백오프로 3회 재시도. 이제 재고 서비스가 일시적 장애면 자동 회복되고, 영원히 대기하지 않는다. 하지만 여전히 한계가 있다 — 재고 서비스가 완전히 죽어 있으면 매 요청마다 2초 × 3회 재시도 = 6초씩 낭비한다.
3단계 — circuit breaker 추가 (오해 1 심화 대응)
class OrderService {
private final InventoryClient inventoryClient;
private final CircuitBreaker breaker; // Resilience4j 등
public void placeOrder(Order order) {
try {
breaker.execute(() -> inventoryClient.withTimeout(ofSeconds(2))
.withRetry(retry)
.reserve(order));
orders.save(order);
} catch (CircuitBreakerOpenException e) {
// 회로 열림 — 빠른 실패. 큐에 담아 나중에 재시도
pendingQueue.enqueue(order);
throw new OrderQueuedException("재고 서비스 장애, 대기열로 이동");
}
}
}
// CircuitBreaker 설정 — 10초 동안 50% 이상 실패 시 회로 open
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.slidingWindowSize(20)
.build();
circuit breaker는 연속 실패 시 일정 시간 동안 호출 자체를 차단한다 (fast fail). 10초 동안 50% 실패하면 회로가 열리고, 그 후 호출은 즉시 실패한다. 재고 서비스가 회복됐는지 확인하기 위해 10초마다 반쯤 열린(half-open) 상태로 시도해본다.
이 단계까지 오면 — 8가지 오해 중 1, 2를 코드로 방어하게 됐다. 12편(회복탄력성)에서 이 패턴들을 깊이 다룬다.
8가지 오해 감지 — 코드에서 어떻게 알아내나
| 오해 | 코드 신호 | 의미 |
|---|---|---|
| 1. 신뢰성 | HTTP 호출에 timeout 없음 | 응답 안 오면 영원히 대기 |
| 1. 신뢰성 | retry 없이 한 번 실패로 끝 | 일시적 장애에 취약 |
| 2. 지연 | N+1 직렬 호출 (루프 안에서 원격 호출) | N배 지연 |
| 2. 지연 | 로컬 캐시 없이 매번 원역 조회 | 불필요한 호출 누적 |
| 3. 대역폭 | 큰 객체를 통째로 직렬화 전송 | 네트워크 포화 위험 |
| 4. 보안 | 내부망이라 평문 HTTP | lateral movement 위험 |
| 5. 토폴로지 | 설정에 IP/호스트 하드코딩 | 노드 교체 시 깨짐 |
| 6. 관리자 | 분산 추적(trace) 없음 | 장애 시 책임 소재 파악 불가 |
| 7. 비용 | 데이터를 연산으로 보내지 않고 연산을 데이터로 | 전송 비용 과다 |
| 8. 균일성 | 부하 분산에 노드 가중치 없음 | 느린 노드가 전체 지연 |
이 표의 신호가 많으면, 그 시스템은 "8가지 오해를 안 믿는다"고 말로는 하면서 코드로는 믿고 있는 것이다. 분산 시스템의 장애 대부분은 이 표의 한 항목 이상에서 비롯된다.
8가지 오해가 이 영역 전체의 출발점
이후 11편은 이 8가지 오해 중 하나 이상을 다룬다. 02편(CAP)은 오해 1을 형식화한다 — "네트워크는 분단될 수 있다"는 사실이 C·A·P trade-off의 출발. 03편(일관성 모델)은 오해 1·2의 결과를 다룬다 — 복제 지연과 부분 장애가 일관성을 어떻게 갈라놓는지. 04편(합의)은 오해 1을 극복하려는 시도 — 비신뢰 네트워크에서 합의에 도달하는 알고리즘. 07편(분산 트랜잭션)은 오해 1·2가 트랜잭션 원자성을 어떻게 깨는지. 12편(회복탄력성)은 8가지 오해를 코드로 방어하는 패턴들의 총정리.
Waldo의 통찰을 한 문장으로 요약하면 — 분산을 "숨기려" 하지 말고 "드러내서" 다뤄라. 원격 호출은 로컬 호출이 아니라는 사실을 코드에 명시하는 게 — 놀랍게도 — 더 견고한 분산 시스템을 만든다. 그 명시가 없으면, 시스템은 장애 상황에서 처음으로 "이건 로컬이 아니었네"를 깨닫게 되고, 그 깨달음의 비용은 장애 한 번이 감당할 수 없다.
참고
- Deutsch — "Fallacies of Distributed Computing Explained" (원래 1994 Sun Microsystems 내부 목록, Gosling이 8번째 추가) — 접근 2026-07-20
- Waldo, Wyant, Kendall, Wollrath — "A Note on Distributed Computing" (Sun Microsystems Laboratories, 1994), https://www.javacorner.com/waldo.pdf — 접근 2026-07-20
- Kleppmann —
(O'Reilly, 2017), Ch.8 (분산 시스템의 어려움) - Richards, Ford —
(O'Reilly, 2020), 분산 관련 장 - Newman —
(2nd ed, O'Reilly, 2021), Ch.6 (관측성·회복탄력성)
'Software Architecture > Distributed Systems' 카테고리의 다른 글
| Distributed Systems - 06. 복제 패턴 (0) | 2026.07.20 |
|---|---|
| Distributed Systems - 05. 파티셔닝과 샤딩 (0) | 2026.07.20 |
| Distributed Systems - 04. 합의 알고리즘 (0) | 2026.07.20 |
| Distributed Systems - 03. 일관성 모델 (0) | 2026.07.20 |
| Distributed Systems - 02. CAP PACELC (0) | 2026.07.20 |