콘텐츠로 바로가기

Messaging Patterns & Ordering Physics

분산 시스템에서 메시지 간의 인과 관계와 순서를 보장하는 기법과 메시징을 활용한 고차원 비즈니스 워크플로우 패턴의 물리학을 다루는 학습 노드입니다.

Article
M

Me

hyunyoun's Blog

network-communicationnetworkcommunicationdistributed-messagingevent-streamingmessaging-patternsordering-physicslearning11 min read

1. Overview

메시징 패턴과 순서 물리학(Messaging Patterns & Ordering Physics)은 "어떻게든 빠르게 많이 처리하라(Throughput)"는 병렬성의 욕망과 "무조건 보낸 순서대로 처리하라(Strict Ordering)"는 무결성의 족쇄 사이에서 벌어지는 분산 시스템 최악의 트레이드오프를 해부합니다.

학습자는 큐(Queue)에 데이터를 밀어 넣고 컨슈머 10대를 붙였더니, 2번 결제가 1번 회원가입보다 먼저 처리되어 DB가 박살 나는 **순서 역전(Out-of-order)**의 재앙을 뜯어봅니다. 나아가 이 순서를 보장하기 위해 파티션(Partition) 키를 써서 1차선 도로로 강제 정렬시킬 때 발생하는 **헤드 오브 라인 블로킹(Head-of-Line Blocking)**의 치명적 병목을 장악합니다. 마지막으로, 영원히 파싱(Parsing)에 실패하는 썩은 사과(Poison Pill) 메시지가 큐 전체를 마비시키는 것을 막기 위해, 에러 난 메시지들만 쓰레기장으로 던져버리는 **DLQ(Dead Letter Queue)**의 고가용성 패턴 역량을 확보합니다.

2. Scope & Boundaries

In-Scope

  • Ordering vs Parallelism: 엄격한 순서 보장(Strict Ordering)과 다중 컨슈머 병렬 처리(High Throughput) 간의 트레이드오프.
  • Partitioning for Order: Kafka의 파티션 키(Partition Key)를 통한 '동일 키 = 동일 파티션' 해싱 순서 보장 원리.
  • Head-of-Line (HoL) Blocking: 에러 난 1번 메시지가 재시도(Retry)의 늪에 빠져 뒤의 2, 3번 메시지까지 멈춰버리는 큐의 본질적 병목.
  • Dead Letter Queue (DLQ): Poison Pill 메시지를 별도의 큐로 격리하여 메인 파이프라인의 붕괴를 막는 예외 처리 패턴.

Out-of-Scope

  • 분산 트랜잭션 (Saga Pattern): 순서 보장을 넘어 데이터베이스 간의 롤백(Rollback)과 보상 트랜잭션 \rightarrow 07-03-03 Saga & Outbox 분산 아키텍처 영역으로 위임.
  • TCP 레벨의 패킷 순서 보장: L4 전송 계층에서의 Sequence Number 순서 재조립 \rightarrow 08-02 TCP/UDP 영역에서 기학습 (본 문서는 L7 애플리케이션 메시지 순서에 집중).

Boundaries

  • Global Ordering vs Partial Ordering: 100만 명의 유저가 쏟아내는 모든 메시지를 완벽한 시간순으로 정렬(Global Ordering)하려 하면, 큐와 컨슈머를 무조건 1개만 써야 하므로 시스템 속도가 0에 수렴합니다. 하지만 비즈니스 로직상 "유저 A의 가입과 결제"만 순서가 맞으면 되지, "유저 A의 가입과 유저 B의 결제"는 누가 먼저 처리되든 상관이 없습니다. 이처럼 글로벌 정렬을 포기하고 해시 키를 이용한 '부분 정렬(Partial Ordering)'을 타협점으로 삼아 분산 확장을 이룩하는 경계를 명확히 긋습니다.

3. Counterexample

  • 순서 역전의 DB 파괴: 쇼핑몰에서 유저가 [1: 물건 담기] -> [2: 결제] -> [3: 배송 시작]을 1초 만에 연속 클릭했습니다. 메시지 큐에 순서대로 들어갔으나, 컨슈머 서버 3대가 각각 메시지를 꺼내어 병렬로 처리했습니다. 컨슈머 C가 [3: 배송 시작]을 제일 먼저 DB에 날렸으나, [1: 물건 담기]를 처리하던 컨슈머 A는 지연(Delay)되어 아직 DB에 넣지도 못했습니다. 없는 물건이 배송 시작되는, 인프라의 '병렬성'이 비즈니스의 '인과성'을 박살 낸 동시성 재앙입니다.
  • 독약 메시지(Poison Pill)에 의한 파이프라인 정지: 결제 큐에 JSON 포맷이 깨진 [ID: Null] 메시지(Poison Pill)가 하나 섞여 들어왔습니다. 컨슈머가 이를 꺼내 파싱하다가 NullPointerException이 발생해 수동 ACK를 안 보내고 에러를 뱉습니다. 브로커는 "처리 못 했네?" 하며 큐의 맨 앞에 다시 집어넣습니다(재전송). 컨슈머는 다시 에러를 뱉습니다. 이 무한 루프(Infinite Retry)가 발생하며, 그 뒤에 줄 서 있던 정상적인 결제 메시지 10만 건이 영원히 처리되지 못하는 HoL Blocking 파업 사태입니다.

4. Prerequisites

  • Message Broker 물리 법칙 (Basic): 큐의 FIFO 특성과 다중 컨슈머 경합. (08-05-01 Broker Physics)
  • Kafka 파티셔닝 (Basic): 파티션 분산 개념. (08-05-02 Event Streaming)

5. Learning Map

Sequence Core Cluster Objective & Description Evidence (BoK)
1 The Ordering Trilemma "속도, 확장성, 완벽한 순서" 이 3가지는 분산 시스템에서 동시에 가질 수 없다는 트릴레마(Trilemma)의 절망을 쥡니다. P1
2 Partial Ordering (Partitioning) 모든 메시지 순서를 맞추는 바보짓을 포기하고, 유저 ID를 키(Key)로 삼아 '같은 유저의 일은 같은 차선'으로 쑤셔 넣는 부분 정렬의 타협을 해부합니다. P5
3 Head-of-Line Blocking in Queues 큐의 치명적 단점, 맨 앞에 선 에러 난 놈(Poison Pill) 하나가 뒤에 줄 선 1만 개의 정상 데이터까지 다 멈춰버리는 길막 병목을 뜯어봅니다. Industry
4 Dead Letter Queue (DLQ) 3번 재시도해도 안 되는 썩은 메시지(Poison Pill)는 과감하게 쓰레기통(DLQ) 큐로 던져버리고, 메인 파이프라인을 재가동하는 고가용성 수술을 장악합니다. Industry

6. Learning Topics

Basic

Core Topic 01: 완벽한 순서의 절망 (The Ordering Trilemma)

  • Why to Learn: 메시지가 들어온 시간순으로 100% 완벽하게 처리해야 한다는 기획자의 무리한 요구를 맞추려다, 수평 확장(Scale-out)이 불가능해져 시스템 전체 성능이 나락으로 떨어지는 역학을 통찰하기 위함입니다.
  • What to Learn:
    • Concepts: FIFO (First-In-First-Out), Global Ordering, Out-of-order Processing (순서 역전), Race Condition.
    • Skills: 단일 큐에 여러 컨슈머(Consumer)를 붙였을 때, 네트워크 딜레이와 스레드 속도 차이에 의해 큐에서 빠져나간 순서와 DB에 기록되는 순서가 뒤섞이는 동시성 경합 추적.
  • How to Learn:
    • 1단계: 경합의 발생: 큐에 [A, B, C] 순서로 메시지가 있습니다. 컨슈머 1, 2, 3이 동시에 덤벼들어 A, B, C를 하나씩 낚아챕니다. C를 물고 간 컨슈머 3이 CPU가 가장 빨라서 0.01초 만에 DB에 넣었습니다. A를 물고 간 컨슈머 1은 가비지 컬렉션(GC)이 터져서 1초 뒤에 넣었습니다. DB에는 [C -> B -> A]로 거꾸로 들어갑니다.
    • 2단계: 글로벌 정렬의 저주: "절대 순서가 섞이면 안 돼!"라고 외치면 방법은 하나뿐입니다. 파티션을 딱 1개만 만들고, 컨슈머도 딱 1대만 띄웁니다. 완벽한 FIFO(Global Ordering)가 보장되지만, 트래픽이 100배로 늘어도 서버를 늘릴 수 없는(스케일 아웃 불가) 끔찍한 병목 시스템의 한계를 뜯어봅니다.
  • Implement: Multi-Consumer 순서 역전 시뮬레이션 (Thread 기반). Queue = [1, 2, 3, 4, 5]. Consumer = 3 Threads. T1이 1 Pop \rightarrow sleep(0.5). T2가 2 Pop \rightarrow sleep(0.1). T3가 3 Pop \rightarrow sleep(0.1). 출력 창에 [DB 렌더링]: 2 처리됨, 3 처리됨, 1 처리됨... 무작위로 뒤섞이는 Race Condition 로깅 시각화.

Core Topic 02: 타협점, 부분 정렬과 파티셔닝 (Partial Ordering)

  • Why to Learn: 앞선 글로벌 정렬의 한계를 부수기 위해, '반드시 순서가 지켜져야 하는 인과적 단위(예: 동일 유저)'만 골라내어 같은 길(파티션)로 강제 배정하는 큐잉 설계의 극의를 장악하기 위함입니다.
  • What to Learn:
    • Concepts: Partial Ordering (부분 정렬), Partition Key / Routing Key, Hashing, Consistent Hashing.
    • Skills: 카프카(Kafka) 환경에서 '유저 A의 가입/탈퇴'와 '유저 B의 가입/탈퇴'가 동시에 쏟아질 때, User ID를 Partition Key로 설정하여 유저별 인과성을 100% 보장하면서도 파티션 100개로 수평 확장(Scale-out)하는 해싱 아키텍처 설계.
  • How to Learn:
    • 1단계: 인과성의 범위: 유저 A가 장바구니에 담고(A1) 결제(A2)하는 건 무조건 A1 \rightarrow A2 순서여야 합니다. 하지만 유저 B의 결제(B1)가 A2보다 먼저 처리되든 말든 비즈니스상 아무 상관이 없습니다.
    • 2단계: 키(Key) 기반 해싱 (Partitioning): 생산자가 메시지를 던질 때 메시지 겉면에 열쇠(Key="UserA")를 답니다. 브로커는 파티션(차선)이 10개 있을 때, 키 값을 해싱(Hash("UserA") % 10)하여 무조건 '3번 차선'으로만 던집니다. UserA의 이벤트 A1, A2, A3는 무조건 3번 차선으로만 직렬로 줄을 섭니다. 3번 차선은 딱 1대의 컨슈머만 순서대로 읽으므로, 유저 단위의 '부분 정렬(Partial Ordering)'이 완벽히 보장되며 10배 확장성까지 챙기는 마법을 해부합니다.
  • Implement: Partition Key 분배 알고리즘 모사. Partitions = 3. Msg(UserA, 가입) \rightarrow Hash("UserA")%3 = P1. Msg(UserB, 결제) \rightarrow Hash("UserB")%3 = P2. Msg(UserA, 탈퇴) \rightarrow Hash("UserA")%3 = P1. 컨슈머 로그에서 P1[가입 -> 탈퇴] 순서가 완벽히 보장되고, P2는 섞여도 무관하게 독립 병렬 처리되는 아키텍처 렌더링.

Practical

Core Topic 03: 길막의 공포, 헤드 오브 라인 블로킹 (Head-of-Line Blocking in Queues)

  • Why to Learn: 순서를 보장하려다 맨 앞에 선 썩은 데이터(Poison Pill) 하나가 전체 큐를 꽉 막아버려, 잘 돌아가던 시스템 전체가 일시 정지해 버리는 끔찍한 연쇄 멈춤 현상을 통찰하기 위함입니다.
  • What to Learn:
    • Concepts: Head-of-Line (HoL) Blocking, Poison Pill Message, Infinite Retry Loop, Queue Backlog.
    • Skills: 파싱 불가능한 깨진 JSON 메시지가 큐의 선두에 있을 때, 컨슈머가 이를 처리(ACK)하지 못하고 무한 재시도(Retry)하며 그 뒤의 정상 메시지 수만 개가 쌓여 큐 모니터링 알람(Lag Spike)이 터지는 과정 디버깅.
  • How to Learn:
    • 1단계: Poison Pill의 침투: 생산자 쪽 버그로 { "userId": null } 이라는 독약 메시지가 큐 맨 앞에 들어갔습니다. 그 뒤로 정상 메시지 1만 개가 줄을 섭니다.
    • 2단계: 무한 재시도 (HoL Blocking): 순서를 보장(FIFO)해야 하므로 컨슈머는 이 1번 메시지부터 꺼냅니다. userId가 null이라서 DB 저장 시 에러가 터집니다. 컨슈머는 "에러 났네? 처리 실패!" 하고 브로커에게 ACK를 안 보냅니다. 브로커는 이 1번 메시지를 다시 큐 맨 앞에 복구(Redelivery)합니다. 컨슈머는 또 1번을 읽고 에러 냅니다. 이 무한 루프가 돌면서, 뒤에 서 있는 정상 메시지 1만 개는 영원히 처리되지 못하고 쌓여(Lag) 전체 시스템이 마비되는 아찔한 병목을 뜯어봅니다.
  • Implement: HoL Blocking 랙(Lag) 누적 시뮬레이터. Queue = [Poison, Normal1, Normal2, ...]. 컨슈머 루프: Msg = Queue.peek(). 파싱 에러 발생 \rightarrow return FAIL. (큐에서 Pop 안 됨). 1초 단위로 브로커에 정상 데이터가 계속 Enqueue 됨. 모니터링 콘솔에 [Lag: 100], [Lag: 500], [Lag: 10000] 숫자가 치솟으며 전체 파이프라인이 하나의 독약 때문에 멈춰 서는 재앙 렌더링.

Advanced

Core Topic 04: 쓰레기장으로 던져라, DLQ (Dead Letter Queue)

  • Why to Learn: 앞서 발생한 HoL Blocking으로 시스템이 죽는 걸 막기 위해, 아무리 재시도해도 안 되는 썩은 메시지들은 메인 큐에서 가차 없이 빼내어 격리소(DLQ)로 던져버리는 고가용성 수술법을 장악하기 위함입니다.
  • What to Learn:
    • Concepts: Dead Letter Queue (DLQ), Dead Letter Exchange (DLX), Max Retry Count, Error Handling Policy.
    • Skills: 메시지 처리 실패 시 재시도 횟수(Max-Retry=3)를 설정하고, 3번 다 실패하면 브로커가 자동으로 해당 메시지를 Order_DLQ라는 별도 큐로 라우팅해 버려 메인 큐의 길막을 뚫어내는(Bypass) 인프라 구성.
  • How to Learn:
    • 1단계: 격리소 (DLQ): 에러 난 놈을 계속 쥐고 있으면 다 같이 죽습니다. 브로커 쪽에 DLQ(Dead Letter Queue)라는 이름의 큐(쓰레기장)를 하나 더 만듭니다.
    • 2단계: 규칙 (Max Retry): 컨슈머가 1번 독약 메시지를 까보고 에러가 나면 "1번 실패" 카운트를 올립니다. 3번까지 재시도했는데 계속 에러가 나면, 컨슈머는 "이건 썩은 사과다!"라고 브로커에 선언(Reject/Nack with no-requeue)합니다. 브로커는 1번 메시지를 메인 큐에서 과감히 뽑아내어 DLQ로 집어 던집니다.
    • 3단계: 파이프라인 재가동: 길을 막던 독약이 사라졌으므로, 멈춰있던 정상 메시지 1만 개가 다시 미친 듯이 처리(Consumer)되며 심장박동을 되찾습니다. DLQ에 쌓인 독약 메시지들은 나중에 개발자가 출근해서 천천히 에러 로그를 까보고 수동으로 씹고 뜯고 맛보는, 결함 허용(Fault Tolerance) 아키텍처의 끝을 해부합니다.
  • Implement: DLQ 격리 및 라우팅 모사. 메시지 [Poison (Retry=0)]. 처리 실패 \rightarrow Retry=1. 처리 실패 \rightarrow Retry=2. 처리 실패 \rightarrow Retry=3. 브로커 설정: If Retry >= 3, Route to DLX(Dead Letter Exchange). 메시지가 Main_Queue에서 Poison_DLQ로 이동(Move) 로그 출력. 직후 Main_Queue에 갇혀있던 정상 메시지들이 [SUCCESS]를 띄우며 와르르 쏟아져 나오는 통쾌한 병목 해소 렌더링.

7. Terminology

Term (EN / ko, abbr) 1문장 정의 단계(기본/권장/실무/심화) 역할/맥락 관련 개념 유사/대비/함께 사용 오해 포인트 Evidence(Primary/Secondary/Industry) Flags(core)
Ordering Trilemma (정렬 트릴레마) 분산 시스템에서 "1. 압도적인 처리량", "2. 서버 수평 확장성", "3. 100% 완벽한 글로벌 순서 보장" 이 세 가지는 물리적으로 절대 동시에 챙길 수 없다는 절망적 법칙입니다. 기본 병렬성과 순서 보장의 본질적 상충 Global vs Partial Ordering CAP Theorem (분산 DB) 돈을 아무리 많이 부어서 카프카 클러스터를 늘려도, 글로벌 순서를 100% 맞추려면 1개의 파티션(1차선)만 써야 하므로 시스템 속도는 바닥을 치게 됨 P1:CS2023 core
Partition Key (파티션 키) 모든 데이터의 순서를 맞추는 바보짓을 포기하는 대신, 무조건 순서가 지켜져야 하는 인과적 단위(예: 동일 유저 ID)를 동일한 해시 차선(파티션)으로 강제 배정하기 위해 메시지 겉면에 붙이는 열쇠입니다. 권장 부분 정렬(Partial Ordering) 강제 Consistent Hashing / Topic Partition Random Routing (라운드 로빈) 키를 생략하면 브로커가 속도를 위해 여러 파티션으로 데이터를 무작위(Round Robin)로 찢어발기기 때문에, 나중에 읽을 때 무조건 순서 역전이 발생함 P5:SFIA core
HoL Blocking (헤드 오브 라인 블로킹) 큐는 무조건 선입선출(FIFO)이어야 하므로, 맨 앞에 있는 메시지가 포맷 에러 등으로 파싱되지 않아 무한 재시도(Retry)의 늪에 빠지면 그 뒤의 수만 개 정상 메시지도 다 같이 멈춰버리는 끔찍한 길막 현상입니다. 실무 큐 기반 파이프라인의 치명적 병목 Poison Pill / Infinite Retry TCP HoL Blocking (L4) 멀티 스레드로 큐를 빼내면 HoL을 완화할 수 있지만, 동일 파티션 안에서는 순서 보장을 위해 병렬 락(Lock)이 걸리므로 완벽한 해결책이 아님 Industry core
DLQ (Dead Letter Queue, 데드 레터 큐) 3번 이상 재시도해도 처리가 불가능한 '독약(Poison Pill)' 메시지들을 메인 파이프라인에서 뽑아내어 쓰레기통처럼 별도로 격리해 두는 특수 목적 큐입니다. 심화 에러 메시지 격리 및 메인 흐름 복구 NACK / Dead Letter Exchange (DLX) Main Queue / Retry Queue DLQ에 들어간 메시지들은 버려지는 게 아니라, 나중에 개발자가 수동으로 원인을 분석하고 다시 살려내어 재처리(Re-drive)하기 위한 최후의 보루임 Industry core

8. References

Primary

  • [P1] CS2023 - Software Engineering (SE) - Distributed System Architectures (Event Ordering)
  • [P5] SFIA - Systems Design (DESN) - Messaging Patterns and Resilience

Secondary

  • [Enterprise Integration Patterns] Gregor Hohpe - Dead Letter Channel, Message Sequence
  • [Designing Data-Intensive Applications] Martin Kleppmann - Partitioning and Order-Preserving

Industry

  • [AWS Architecture Blog] - Using Dead-Letter Queues in Amazon SQS
  • [Apache Kafka Documentation] - Message Ordering and Guarantees

9. Final Checklist

Primary

  • 1개의 큐에 여러 대의 컨슈머(다중 스레드/프로세스)를 붙였을 때, 네트워크 지연과 CPU 처리 속도 차이로 인해 큐에 들어간 순서(FIFO)와 DB에 기록되는 순서가 뒤섞이는 순서 역전(Out-of-order) 현상을 증명할 수 있는가?
  • 글로벌 순서 정렬(Global Ordering)을 강제하기 위해 파티션과 컨슈머를 1개로 제한했을 때 발생하는 수평 확장성(Scale-out)의 붕괴를 트릴레마 관점에서 지적할 수 있는가?

Secondary

  • 카프카 환경에서 User IDOrder ID를 파티션 키(Partition Key)로 지정하여, 인과 관계가 있는 데이터들만 일관성 해싱(Consistent Hashing)으로 묶어 특정 단일 파티션에 줄 세우는 '부분 정렬(Partial Ordering)' 아키텍처를 설계할 수 있는가?
  • JSON 포맷 에러 등으로 처리 불가능한 '독약(Poison Pill)' 메시지가 큐의 선두(Head)에 위치했을 때, 컨슈머의 반복적인 재시도(Retry)가 후속 트래픽의 처리를 전면 중단시키는 HoL(Head-of-Line) Blocking 병목을 해부할 수 있는가?

Industry

  • 위 HoL Blocking을 타파하기 위해, 최대 재시도 횟수(Max Retry)를 초과한 메시지를 RabbitMQ의 x-dead-letter-exchange 혹은 SQS의 DLQ(Dead Letter Queue) 정책을 통해 메인 큐에서 격리(Bypass)시키는 내결함성(Fault-tolerance) 파이프라인을 구축할 수 있는가?
  • 비즈니스 요건 상 순서 보장이 절대적으로 중요한 상황에서 컨슈머가 특정 메시지 처리에 실패했을 때, 이를 섣불리 DLQ로 넘기지 않고 뒤따라오는 연관 메시지들의 처리까지 홀드(Hold)시키는 '순서 엄격 유지 큐'의 딜레마를 평가할 수 있는가?

Distributed Messaging & Event Streaming

4 / 4