콘텐츠로 이동

Ch.22 분산이 어려운 진짜 이유 - 부분 실패·멱등성·CAP

< Container와 Orchestration | 유사 사례와 키워드 정리 >


Container와 K8s로 "내 컴퓨터에서는 되는데요" 문제와 배포·확장은 풀었다. 그런데 서비스를 여러 프로세스·여러 서버로 쪼개는 순간, 단일 서버에는 없던 종류의 어려움이 생긴다. 이게 분산 시스템의 본질이고, "분산 시스템의 기초"라는 이 챕터 제목이 가리키는 진짜 주제다.

단일 서버엔 없던 문제 - 부분 실패(Partial Failure)

단일 프로세스 안에서 함수 호출은 성공 아니면 실패, 둘 중 하나다. 그런데 네트워크 너머의 서비스를 호출하면 제3의 상태가 생긴다. 요청은 보냈는데 응답이 안 오는 경우다.

[ 내 서비스 ] ──요청──▶ [ 결제 서비스 ]
            ◀──응답?──   ← 안 온다. 그럼 셋 중 무엇인가?

  (1) 상대가 요청을 못 받았다           → 처리 안 됨
  (2) 상대가 처리하고 응답 보내다 끊겼다  → 처리 됨
  (3) 상대가 처리 못 하고 죽었다         → 처리 안 됨

부른 쪽은 이 셋을 구분할 수 없다. 타임아웃이 났다는 건 "실패했다"가 아니라 "결과를 모른다"는 뜻이다.

부분 실패 (Partial Failure)

분산 시스템에서 일부 구성 요소(또는 그들 사이의 네트워크)만 실패하는 상황이다. 단일 컴퓨터에서는 "전체가 살아 있거나 전체가 죽거나"지만, 분산에서는 "어떤 노드는 살아 있고 어떤 노드는 죽었으며, 누가 죽었는지조차 모르는" 상태가 정상이다.

그래서 분산 시스템의 1번 전제가 "네트워크는 신뢰할 수 없다"다. 이걸 받아들이는 데서 모든 설계가 시작된다.

모르면 다시 보낸다 - 그런데 두 번 실행되면?

결과를 모르면 보통 다시 보낸다(retry). 그런데 "결제 요청"을 재시도하면? 위의 (2)번처럼 첫 요청이 사실 성공했는데 응답만 유실된 거라면, 재시도로 결제가 두 번 된다. 그래서 분산에서 재시도와 멱등성은 항상 한 쌍으로 다닌다.

멱등성 (Idempotency)

같은 요청을 한 번 보내든 여러 번 보내든 결과가 같은 성질이다. x = 5는 멱등하고(몇 번 해도 5), x += 5는 멱등하지 않다(할 때마다 증가).

HTTP에서 GET·PUT·DELETE는 멱등하도록 설계됐고, POST는 그렇지 않다. 그래서 "결제", "주문 생성" 같은 POST는 멱등성을 따로 만들어줘야 한다.

해법은 idempotency key다. 클라이언트가 요청마다 고유 키를 붙이고, 서버는 그 키를 기억한다. 이미 처리한 키가 또 오면, 다시 실행하지 않고 그때의 결과를 그대로 돌려준다.

요청1: POST /payments  Idempotency-Key: abc-123  → 서버가 처리, 결과 저장
(응답 유실)
재시도: POST /payments  Idempotency-Key: abc-123  → 서버: "abc-123 이미 처리함"
                                                   → 재실행 없이 저장된 결과 반환

결제 API(Stripe 등)가 Idempotency-Key 헤더를 요구하는 이유가 이것이다. 재시도가 안전해진다.

여러 서비스에 걸친 일은 어떻게 원자적으로 - 2PC vs Saga

단일 DB라면 트랜잭션(Ch.15)이 "전부 되거나 전부 안 되거나"를 보장한다. 그런데 주문·결제·재고가 서로 다른 서비스(다른 DB)에 있으면, 하나의 DB 트랜잭션으로 묶을 수 없다. 두 가지 길이 있다.

  • 2PC (Two-Phase Commit): 코디네이터가 모든 참가자에게 "커밋 준비됐나?"(prepare)를 묻고, 전부 OK면 "커밋"을 명령한다. 강한 일관성을 주지만, 준비~커밋 사이 모든 참가자가 자원을 잠그고 대기하며, 코디네이터가 그 사이 죽으면 전체가 멈춘다(blocking). 그래서 MSA에서는 잘 쓰지 않는다.
  • Saga: 각 서비스가 자기 로컬 트랜잭션을 차례로 커밋한다. 중간에 실패하면, 앞서 성공한 단계를 보상 트랜잭션(compensating transaction)으로 되돌린다.
Saga (정상):   주문 생성 ──▶ 결제 ──▶ 재고 차감 ──▶ 완료
Saga (실패):   주문 생성 ──▶ 결제 ──▶ 재고 차감 실패!
                  │            │
                  ▼            ▼
              주문 취소 ◀── 결제 환불   (보상 트랜잭션으로 되감기)

Saga는 "모든 단계가 동시에 잠긴 상태"를 만들지 않으므로 잠금 없이 확장되지만, 강한 일관성 대신 최종 일관성(eventual consistency) 을 받아들인다(중간 순간에는 "결제는 됐는데 재고는 아직"인 상태가 보일 수 있다). 그리고 각 보상 단계는 재시도되므로 - 다시 멱등성이 필요하다.

무엇을 포기할지 정한다 - CAP

분산에서 네트워크 분할(partition, 노드 사이 통신 두절)은 "일어날 수도 있다"가 아니라 "언젠가 반드시 일어난다"다. 분할이 났을 때, 일관성과 가용성 중 하나는 포기해야 한다.

CAP 정리

분산 시스템은 Consistency(일관성), Availability(가용성), Partition tolerance(분할 내성) 셋을 동시에 완벽히 만족할 수 없다는 정리다. 네트워크 분할(P)은 분산에서 피할 수 없으므로, 실질적으로는 분할이 났을 때 C와 A 중 무엇을 포기할지의 선택이 된다.

  • CP: 분할 시 일관성을 지키려고 일부 요청을 거부한다(가용성 포기). 결제·재고처럼 틀리면 안 되는 데이터.
  • AP: 분할 시 일단 응답하고 나중에 맞춘다(일관성 잠시 포기). 좋아요 수, 피드처럼 잠깐 어긋나도 되는 데이터.

이건 Ch.17~19에서 "캐시·Read Replica는 stale을 감수할 수 있는 데이터에만"이라고 한 것과 정확히 같은 결정이다. (분할이 없을 때조차 일관성 vs 지연(Latency) 사이의 선택이 남는다는 걸 PACELC가 덧붙인다.) 핵심은 하나다 - 분산에는 공짜 점심이 없다. 무엇을 포기할지 의식적으로 고른다.

정리

  • 분산의 1번 전제: 네트워크는 신뢰할 수 없다 → 부분 실패가 기본값이다.
  • 결과를 모르면 재시도한다 → 재시도와 멱등성(idempotency key)은 한 쌍이다.
  • 여러 서비스에 걸친 일은 2PC(강한 일관성·blocking) 아니면 Saga(보상·최종 일관성)다.
  • 분할이 나면 C와 A 중 무엇을 포기할지 CAP로 의식적으로 정한다.

이 강의는 합의 알고리즘(Raft·Paxos)이나 분산 시계(Lamport Clock)까지는 다루지 않는다. 하지만 "왜 멱등성 키가 필요한가", "왜 Saga를 쓰나", "왜 이 데이터는 잠깐 옛 값이어도 되는가"를 설명할 수 있으면, MSA에서 데이터가 꼬이는 사고의 대부분을 미리 막을 수 있다.


< Container와 Orchestration | 유사 사례와 키워드 정리 >