<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>김신건의 로그 - 위키 / distributed-systems</title><description>&apos;distributed-systems&apos; 카테고리 위키 페이지 최신 업데이트</description><link>https://shinkeonkim.com/</link><language>ko-KR</language><item><title>[Distributed] NATS: 가벼움, JetStream, subject 라우팅</title><link>https://shinkeonkim.com/wiki/distributed-systems/nats/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/nats/</guid><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;NATS&lt;/strong&gt; = &lt;em&gt;극도로 가벼운 메시징 시스템&lt;/em&gt;. core NATS = &lt;em&gt;at-most-once Pub/Sub + request-reply&lt;/em&gt;. JetStream = &lt;em&gt;영속 / at-least-once / exactly-once&lt;/em&gt; 까지.&lt;/p&gt;
&lt;p&gt;특징:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1 바이너리&lt;/strong&gt; (Go), 메모리 ~10MB.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;마이크로초 latency&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;수백만 msg/s/노드&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;edge / IoT / 마이크로서비스&lt;/em&gt; 친화.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;두-모드-core-vs-jetstream&quot;&gt;두 모드: Core vs JetStream&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;-&lt;/th&gt;&lt;th&gt;Core NATS&lt;/th&gt;&lt;th&gt;JetStream&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;영속&lt;/td&gt;&lt;td&gt;없음 (in-memory)&lt;/td&gt;&lt;td&gt;&lt;em&gt;있음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;신뢰성&lt;/td&gt;&lt;td&gt;at-most-once&lt;/td&gt;&lt;td&gt;at-least-once / exactly-once&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Latency&lt;/td&gt;&lt;td&gt;매우 낮음&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;사용&lt;/td&gt;&lt;td&gt;telemetry, signal&lt;/td&gt;&lt;td&gt;event log, queue&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;subject-계층&quot;&gt;Subject 계층&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;orders.created&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;orders.paid&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;orders.shipped.us&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;orders.shipped.eu&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;inventory.low.warehouse-a&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;와일드카드&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;&lt;td&gt;단어 1개&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;나머지 모두&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;subscribe orders.*    → orders.created, orders.paid (1단 차이만)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;subscribe orders.&gt;    → orders.created, orders.paid, orders.shipped.us, ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;request-reply-core-nats-의-강점&quot;&gt;Request-Reply (Core NATS 의 강점)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant R as Requester&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant N as NATS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant S as Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S-&gt;&gt;N: SUB shop.getItem&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    R-&gt;&gt;N: PUB shop.getItem (reply=INBOX.abc)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N-&gt;&gt;S: deliver&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S-&gt;&gt;S: 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S-&gt;&gt;N: PUB INBOX.abc 결과&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N-&gt;&gt;R: deliver&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;RPC&lt;/em&gt; 가 &lt;em&gt;Pub/Sub 위에 자연스럽게&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;load balancing&lt;/em&gt; = 같은 subject 의 &lt;em&gt;queue group&lt;/em&gt; 으로.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Queue group: 여러 service 가 같은 subject 구독, 한 명에게 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;nats.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;subscribe&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;shop.getItem&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, { queue: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;shop&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }, (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;msg&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;jetstream-개요&quot;&gt;JetStream 개요&lt;/h2&gt;
&lt;p&gt;JetStream 은 NATS 에 내장된 &lt;em&gt;영속 스트리밍 레이어&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Producer] --&gt;|&quot;PUB orders.&gt;&quot;| JS[(&quot;JetStream Stream&quot;)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    JS --&gt; Consumer1[Push consumer]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    JS --&gt; Consumer2[Pull consumer]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    JS --&gt; Consumer3[Durable consumer]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Consumer 타입&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Push&lt;/td&gt;&lt;td&gt;broker 가 능동 push&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Pull&lt;/td&gt;&lt;td&gt;consumer 가 poll&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ephemeral&lt;/td&gt;&lt;td&gt;임시 (재접속 시 처음부터)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Durable&lt;/td&gt;&lt;td&gt;진행 상태 영속&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;jetstream-stream-설정&quot;&gt;JetStream Stream 설정&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# nats CLI 또는 API 로 Stream 생성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;stream&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;ORDERS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  subjects&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orders.&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  storage&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;file&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          # file (영속) / memory (빠름)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  retention&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;limits&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # limits / workqueue / interest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  max_age&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;24h&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;           # 메시지 보존 기간&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  replicas&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # 노드 수 (HA 필수)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  max_bytes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;10GB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  discard&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;old&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;           # 한계 초과 시 오래된 것 삭제&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;jetstream-메시지-흐름-ack&quot;&gt;JetStream 메시지 흐름 (Ack)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant P as Producer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant JS as JetStream&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as Consumer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P-&gt;&gt;JS: PUB orders.created&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    JS--&gt;&gt;P: PubAck (seq=42)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    JS-&gt;&gt;C: deliver msg (seq=42)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;C: 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;JS: Ack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over JS: 해당 seq 완료 마킹&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    alt Ack 없이 timeout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        JS-&gt;&gt;C: redeliver (at-least-once)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;consumer-ack-정책&quot;&gt;Consumer Ack 정책&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;AckPolicy&lt;/th&gt;&lt;th&gt;동작&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;explicit&lt;/code&gt;&lt;/td&gt;&lt;td&gt;각 메시지 개별 Ack. 가장 안전&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;&lt;td&gt;N번 Ack = 1~N 모두 확인 (배치 처리 효율)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Ack 불필요. fire-and-forget&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Durable Consumer: 재연결 후 마지막 위치부터&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; consumer&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; js.consumers.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ORDERS&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;order-processor&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; msg&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; consumer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;consume&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; processOrder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(msg.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  msg.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ack&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// explicit ack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;nats-kv--object-store&quot;&gt;NATS KV / Object Store&lt;/h2&gt;
&lt;p&gt;JetStream 위 &lt;em&gt;추가 추상&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// KV&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; kv&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jsm.views.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kv&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;config&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; kv.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;put&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;feature.x&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;enabled&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; val&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; kv.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;feature.x&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Object Store&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; os&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; jsm.views.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;os&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;images&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; os.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;put&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;logo.png&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, readableStream);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Redis 같은 KV&lt;/em&gt;, &lt;em&gt;S3 같은 object store&lt;/em&gt; 가 NATS 안에 통합. &lt;em&gt;작은 인프라&lt;/em&gt; 친화.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;nats-cluster-구성&quot;&gt;NATS Cluster 구성&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# nats.conf (3-node cluster)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;cluster {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;my-cluster&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  listen&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;0.0.0.0:6222&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  routes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    nats-route://nats-0:6222&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    nats-route://nats-1:6222&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    nats-route://nats-2:6222&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;jetstream {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  store_dir&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/data/jetstream&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  max_memory_store&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;4GB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  max_file_store&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;100GB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;3 노드 이상&lt;/em&gt; 권장 (Raft 합의)&lt;/li&gt;
&lt;li&gt;JetStream replica = &lt;em&gt;노드 수 이하&lt;/em&gt; 로 설정 (보통 3)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;edge--iot-패턴&quot;&gt;Edge / IoT 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Edge[&quot;Edge devices (수천 ~ 수백만)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        E1[Sensor 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        E2[Sensor 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        EN[Sensor N]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Edge --&gt; NEdge[&quot;NATS edge cluster&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NEdge --&gt; Leaf[&quot;Leaf node&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Leaf --&gt; NCloud[&quot;NATS central cluster&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NCloud --&gt; Apps[&quot;Apps / Analytics&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Leaf node&lt;/em&gt; = &lt;em&gt;edge → central&lt;/em&gt; 의 경량 게이트웨이.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;bandwidth 최적&lt;/em&gt;: 같은 메시지 한 번만 전송.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;오프라인 buffer&lt;/em&gt; + &lt;em&gt;재연결 시 sync&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;nats-vs-kafka-vs-rabbitmq&quot;&gt;NATS vs Kafka vs RabbitMQ&lt;/h2&gt;

































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;NATS&lt;/th&gt;&lt;th&gt;Kafka&lt;/th&gt;&lt;th&gt;RabbitMQ&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;바이너리 크기&lt;/td&gt;&lt;td&gt;&lt;em&gt;10 MB&lt;/em&gt;&lt;/td&gt;&lt;td&gt;수백 MB&lt;/td&gt;&lt;td&gt;수십 MB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;운영 복잡도&lt;/td&gt;&lt;td&gt;&lt;em&gt;낮음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;처리량&lt;/td&gt;&lt;td&gt;매우 높음&lt;/td&gt;&lt;td&gt;매우 높음&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Latency&lt;/td&gt;&lt;td&gt;&lt;em&gt;최저&lt;/em&gt;&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;영속&lt;/td&gt;&lt;td&gt;옵션 (JS)&lt;/td&gt;&lt;td&gt;항상&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Replication&lt;/td&gt;&lt;td&gt;빌트인 (JS)&lt;/td&gt;&lt;td&gt;빌트인&lt;/td&gt;&lt;td&gt;mirror queue&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;학습 곡선&lt;/td&gt;&lt;td&gt;&lt;em&gt;낮음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Request-Reply&lt;/td&gt;&lt;td&gt;&lt;em&gt;네이티브 지원&lt;/em&gt;&lt;/td&gt;&lt;td&gt;어색&lt;/td&gt;&lt;td&gt;가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Edge/IoT&lt;/td&gt;&lt;td&gt;&lt;em&gt;최적&lt;/em&gt;&lt;/td&gt;&lt;td&gt;부적합&lt;/td&gt;&lt;td&gt;가능&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Kafka 와 NATS 의 &lt;em&gt;핵심 차이&lt;/em&gt;:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;-&lt;/th&gt;&lt;th&gt;NATS JetStream&lt;/th&gt;&lt;th&gt;Kafka&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;설계 목적&lt;/td&gt;&lt;td&gt;빠른 메시징 + 선택적 영속&lt;/td&gt;&lt;td&gt;대용량 영속 로그&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;장기 retention&lt;/td&gt;&lt;td&gt;가능하지만 Kafka 만큼 최적화 안 됨&lt;/td&gt;&lt;td&gt;최적 (수개월)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Offset 관리&lt;/td&gt;&lt;td&gt;consumer 별 server-side&lt;/td&gt;&lt;td&gt;consumer 가 broker 에 commit&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Subject 라우팅&lt;/td&gt;&lt;td&gt;와일드카드 (&lt;code&gt;*&lt;/code&gt;, &lt;code&gt;&gt;&lt;/code&gt;)&lt;/td&gt;&lt;td&gt;없음 (topic = exact match)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;적합--부적합&quot;&gt;적합 / 부적합&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good[적합]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G1[마이크로서비스 사이 메시징]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G2[&quot;Edge / IoT&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G3[Real-time telemetry]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G4[&quot;작은 영속 큐 (JS)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad[부적합]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; B1[&quot;복잡한 라우팅 (Topic exchange 같은)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; B2[수개월 retention 의 거대 로그]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Core NATS 의 손실 가능성 무시&lt;/strong&gt; = subscriber 없으면 &lt;em&gt;조용히 사라짐&lt;/em&gt;. JS 로 가야.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JS replica 1&lt;/strong&gt; = 노드 다운 시 손실. RF 3 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wildcard 과도 사용&lt;/strong&gt; = &lt;code&gt;&gt;&lt;/code&gt; 가 &lt;em&gt;모든 메시지&lt;/em&gt; 받음 → 클라이언트 부하.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subject 표준화 없음&lt;/strong&gt; = 작은 팀에서는 OK, 큰 팀에서는 &lt;em&gt;충돌 / 중복&lt;/em&gt;. 네임스페이스 가이드 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JetStream max_age 미설정&lt;/strong&gt; = 디스크 무한 증가. retention 정책 명시 필수.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[kafka]], [[rabbitmq]]&lt;/li&gt;
&lt;li&gt;[[Redis Pub Sub vs Streams]]&lt;/li&gt;
&lt;li&gt;[[grpc]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>nats</category><category>message-broker</category><category>lightweight</category><category>edge</category><category>distributed</category><author>koa (김신건)</author></item><item><title>[Distributed] RabbitMQ: exchange, queue, routing key</title><link>https://shinkeonkim.com/wiki/distributed-systems/rabbitmq/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/rabbitmq/</guid><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt; = &lt;em&gt;AMQP 0.9.1&lt;/em&gt; 기반 &lt;em&gt;traditional message broker&lt;/em&gt;. &lt;em&gt;exchange → queue 라우팅&lt;/em&gt;, &lt;em&gt;workload distribution&lt;/em&gt;, &lt;em&gt;publish-subscribe&lt;/em&gt;. Kafka 보다 &lt;em&gt;작은 단위 메시지 라우팅에 강함&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;메시지는 반드시 &lt;em&gt;exchange&lt;/em&gt; 를 경유해 &lt;em&gt;binding&lt;/em&gt; 규칙에 따라 &lt;em&gt;queue&lt;/em&gt; 로 전달된다. Producer 는 queue 를 직접 알지 못하고 exchange 에만 발행한다.&lt;/p&gt;
&lt;h2 id=&quot;언제-쓰이나&quot;&gt;언제 쓰이나&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Task queue&lt;/strong&gt;: 백그라운드 작업 배포 (이메일 발송, 이미지 처리)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request-Reply&lt;/strong&gt;: RPC 패턴 (&lt;code&gt;reply_to&lt;/code&gt; + &lt;code&gt;correlation_id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복잡한 라우팅&lt;/strong&gt;: topic/headers exchange 로 세밀한 메시지 필터링&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마이크로서비스 이벤트&lt;/strong&gt;: 서비스 간 비동기 통신&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;작은 메시지, 낮은 지연&lt;/strong&gt;: Kafka 보다 단순하고 빠른 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;구조&quot;&gt;구조&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Producer] --&gt; Exch[Exchange]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Exch --&gt;|&quot;routing key 매칭&quot;| Q1[Queue 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Exch --&gt;|&quot;routing key 매칭&quot;| Q2[Queue 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Exch --&gt;|&quot;routing key 매칭&quot;| Q3[Queue 3]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt; C1[Consumer 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt; C2[Consumer 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3 --&gt; C3[Consumer 3]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;컴포넌트&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Producer&lt;/td&gt;&lt;td&gt;메시지 발행자&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Exchange&lt;/td&gt;&lt;td&gt;&lt;em&gt;routing 규칙&lt;/em&gt;. 4 종류&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Queue&lt;/td&gt;&lt;td&gt;메시지 저장소&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Binding&lt;/td&gt;&lt;td&gt;exchange ↔ queue 연결 + key&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Consumer&lt;/td&gt;&lt;td&gt;소비자&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;exchange-4-종류&quot;&gt;Exchange 4 종류&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Direct[&quot;Direct\n(routing_key 정확 일치)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Topic[&quot;Topic\n(*.error.* 와일드카드)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Fanout[&quot;Fanout\n(모든 queue 로 broadcast)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Headers[&quot;Headers\n(header attribute 매칭)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;1-direct&quot;&gt;1. Direct&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;publish key=ORDER.PAID → queue:paid (binding key=ORDER.PAID)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;publish key=ORDER.SHIPPED → queue:shipped (binding key=ORDER.SHIPPED)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하나의 exchange 에 여러 binding key 를 달아 다른 queue 로 보낼 수 있다. 같은 key 에 여러 queue 를 binding 하면 round-robin 분배.&lt;/p&gt;
&lt;h3 id=&quot;2-topic&quot;&gt;2. Topic&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;publish key=order.paid.us → matches &quot;order.*.us&quot;, &quot;order.paid.*&quot;, &quot;#&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;와일드카드&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;&lt;td&gt;단어 1개&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;&lt;td&gt;0+ 단어&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;3-fanout&quot;&gt;3. Fanout&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;모든 binding 된 queue 로 무조건 복사&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Pub/Sub fan-out&lt;/em&gt; 의 정통. routing key 를 무시한다.&lt;/p&gt;
&lt;h3 id=&quot;4-headers&quot;&gt;4. Headers&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;header { type: &quot;order&quot;, region: &quot;us&quot; } 매칭&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;routing key 대신 메시지 헤더 attribute 로 라우팅. &lt;code&gt;x-match: all&lt;/code&gt; (AND) 또는 &lt;code&gt;x-match: any&lt;/code&gt; (OR).&lt;/p&gt;
&lt;h2 id=&quot;실전-python-pika-로-topic-exchange&quot;&gt;실전: Python pika 로 Topic Exchange&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pika&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;connection &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pika.BlockingConnection(pika.ConnectionParameters(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;localhost&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; connection.channel()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Exchange 선언&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.exchange_declare(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;exchange&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;exchange_type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;topic&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;durable&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Queue 선언 (DLX 포함)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.queue_declare(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    queue&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders.paid&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    durable&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    arguments&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;x-dead-letter-exchange&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders.dlx&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;x-message-ttl&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;60000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 60초 미처리 시 DLQ&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;x-max-length&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Binding: order.*.paid 패턴&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.queue_bind(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;exchange&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;queue&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders.paid&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;routing_key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;order.*.paid&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Publish (persistent 메시지)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.basic_publish(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    exchange&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    routing_key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;order.kr.paid&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    body&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;json.dumps({&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;order_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;42&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;9900&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}).encode(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    properties&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;pika.BasicProperties(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        delivery_mode&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;pika.DeliveryMode.Persistent,  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 디스크 저장&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        content_type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;print&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Published order.kr.paid&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;connection.close()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;consumer-ack--nack&quot;&gt;Consumer: ack / nack&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_message&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(channel, method, properties, body):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        data &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; json.loads(body)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        process_order(data)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        channel.basic_ack(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;delivery_tag&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;method.delivery_tag)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    except&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; e:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # requeue=False → DLQ 로 라우팅&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        channel.basic_nack(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;delivery_tag&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;method.delivery_tag, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;requeue&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.basic_qos(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;prefetch_count&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 한 번에 10개만 가져옴&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.basic_consume(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;queue&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders.paid&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;on_message_callback&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;on_message)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.start_consuming()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;메시지-보장&quot;&gt;메시지 보장&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P-&gt;&gt;Broker: publish&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Broker-&gt;&gt;Disk: persistent? → disk write&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Broker-&gt;&gt;P: confirm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P-&gt;&gt;P: confirm 받음 → 안전&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Broker-&gt;&gt;C: deliver&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;C: 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;Broker: ack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Broker-&gt;&gt;Broker: ack 받음 → 제거&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;설정&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;persistent&lt;/code&gt; 메시지&lt;/td&gt;&lt;td&gt;broker 재시작 후에도 보존&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;durable&lt;/code&gt; queue&lt;/td&gt;&lt;td&gt;broker 재시작 후 queue 유지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;publisher confirms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;broker 의 &lt;em&gt;받음&lt;/em&gt; 확인&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;consumer ack&lt;/code&gt;&lt;/td&gt;&lt;td&gt;처리 완료 확인&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mandatory&lt;/code&gt; flag&lt;/td&gt;&lt;td&gt;routing 안 되면 &lt;em&gt;unroutable&lt;/em&gt; 반환&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
모든 &lt;em&gt;4가지 켜야&lt;/em&gt; 실제 &lt;em&gt;at-least-once&lt;/em&gt; 보장. 하나라도 빠지면 &lt;em&gt;손실 가능&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;queue-속성-x-arguments&quot;&gt;Queue 속성 (x-arguments)&lt;/h2&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;속성&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;th&gt;예시 값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;x-message-ttl&lt;/code&gt;&lt;/td&gt;&lt;td&gt;큐 내 메시지 최대 대기 시간 (ms)&lt;/td&gt;&lt;td&gt;&lt;code&gt;60000&lt;/code&gt; (60초)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;x-max-length&lt;/code&gt;&lt;/td&gt;&lt;td&gt;큐 최대 메시지 수&lt;/td&gt;&lt;td&gt;&lt;code&gt;10000&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;x-max-length-bytes&lt;/code&gt;&lt;/td&gt;&lt;td&gt;큐 최대 바이트&lt;/td&gt;&lt;td&gt;&lt;code&gt;104857600&lt;/code&gt; (100MB)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;x-dead-letter-exchange&lt;/code&gt;&lt;/td&gt;&lt;td&gt;TTL/reject 시 라우팅할 DLX&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;orders.dlx&quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;x-dead-letter-routing-key&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DLX 로 보낼 때 routing key&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;failed&quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;x-queue-type&lt;/code&gt;&lt;/td&gt;&lt;td&gt;큐 타입&lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;quorum&quot;&lt;/code&gt;, &lt;code&gt;&quot;stream&quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;x-priority&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Priority Queue 최대 우선순위&lt;/td&gt;&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;dlq-dead-letter-queue&quot;&gt;DLQ (Dead Letter Queue)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1[&quot;primary queue&quot;] --&gt;|&quot;reject 또는 ttl 만료&quot;| DLX[&quot;Dead Letter Exchange&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DLX --&gt; DLQ[&quot;DLQ&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DLQ --&gt; Inspect[&quot;운영자 검사&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;trigger&lt;/th&gt;&lt;th&gt;동작&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;reject (requeue=false)&lt;/td&gt;&lt;td&gt;DLQ 로&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TTL 만료&lt;/td&gt;&lt;td&gt;DLQ 로&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;queue 길이 한도 초과&lt;/td&gt;&lt;td&gt;DLQ 로&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;자동 retry 패턴:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1[&quot;primary&quot;] --&gt;|&quot;fail&quot;| DLQ[&quot;DLQ&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DLQ --&gt;|&quot;ttl 5s&quot;| Retry1[&quot;retry-1&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Retry1 --&gt;|&quot;fail&quot;| DLQ2[&quot;DLQ 2&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DLQ2 --&gt;|&quot;ttl 30s&quot;| Retry2[&quot;retry-2&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Retry2 --&gt;|&quot;fail&quot;| Final[&quot;parking lot&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;quorum-queue-ha&quot;&gt;Quorum Queue (HA)&lt;/h2&gt;
&lt;p&gt;RabbitMQ 3.8+ 의 &lt;strong&gt;고가용성 큐 타입&lt;/strong&gt;. Raft 합의 알고리즘으로 데이터 복제.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Quorum Queue 선언&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;channel.queue_declare(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    queue&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;orders.quorum&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    durable&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    arguments&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;x-queue-type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;quorum&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Classic Queue&lt;/th&gt;&lt;th&gt;Quorum Queue&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;고가용성&lt;/td&gt;&lt;td&gt;Mirrored (구식)&lt;/td&gt;&lt;td&gt;Raft 기반&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;메시지 보장&lt;/td&gt;&lt;td&gt;브로커 장애 시 손실 가능&lt;/td&gt;&lt;td&gt;과반 노드 생존 시 보장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;성능&lt;/td&gt;&lt;td&gt;빠름&lt;/td&gt;&lt;td&gt;약간 느림 (consensus overhead)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;권장&lt;/td&gt;&lt;td&gt;비중요 로컬&lt;/td&gt;&lt;td&gt;프로덕션 중요 메시지&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
Classic mirrored queue 는 RabbitMQ 3.12+ 에서 deprecated. 새 프로젝트는 Quorum Queue 를 사용하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;rabbitmq-vs-kafka&quot;&gt;RabbitMQ vs Kafka&lt;/h2&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;RabbitMQ&lt;/th&gt;&lt;th&gt;Kafka&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;모델&lt;/td&gt;&lt;td&gt;큐 + exchange 라우팅&lt;/td&gt;&lt;td&gt;append-only log&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;처리량&lt;/td&gt;&lt;td&gt;&lt;em&gt;수만 ~ 수십만/s&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;수백만/s&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Latency&lt;/td&gt;&lt;td&gt;&lt;em&gt;낮음 (ms)&lt;/em&gt;&lt;/td&gt;&lt;td&gt;좀 더 큼&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;영속&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;td&gt;항상&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;재처리&lt;/td&gt;&lt;td&gt;한 번 소비 후 &lt;em&gt;사라짐&lt;/em&gt;&lt;/td&gt;&lt;td&gt;offset 으로 &lt;em&gt;임의 재생&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;라우팅&lt;/td&gt;&lt;td&gt;&lt;em&gt;유연 (exchange 패턴)&lt;/em&gt;&lt;/td&gt;&lt;td&gt;단순 (topic + partition)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;사용&lt;/td&gt;&lt;td&gt;task queue, request-reply&lt;/td&gt;&lt;td&gt;event log, stream&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;rabbitmq-streams-kafka-흉내&quot;&gt;RabbitMQ Streams (Kafka 흉내)&lt;/h2&gt;
&lt;p&gt;3.9+ 의 &lt;em&gt;Stream&lt;/em&gt; 큐 타입. &lt;em&gt;append-only log&lt;/em&gt; + replica + 재생 가능.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# declare 시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;queue.declare(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;&quot;my-stream&quot;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; arguments={&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;x-queue-type&quot;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;stream&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Kafka 와 비슷한 흐름&lt;/em&gt;. RabbitMQ 안에서 &lt;em&gt;durable log + fan-out 임의 시점&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;persistent&lt;/code&gt; 없이 신뢰&lt;/strong&gt; = broker 재시작 시 모든 메시지 손실.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;prefetch 너무 큼&lt;/strong&gt; = 한 consumer 가 &lt;em&gt;수천 메시지 lock&lt;/em&gt; → 다른 consumer 가 굶음. &lt;code&gt;prefetch=10~100&lt;/code&gt; 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DLQ 없이 reject&lt;/strong&gt; = 메시지 영구 손실 또는 무한 재시도.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;auto_ack=true&lt;/code&gt;&lt;/strong&gt; = consumer 다운 시 처리 안 한 메시지가 &lt;em&gt;ack 됨&lt;/em&gt; (손실).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Classic mirrored queue 신규 사용&lt;/strong&gt; = deprecated. Quorum Queue 로 전환.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[kafka]] (event log)&lt;/li&gt;
&lt;li&gt;[[nats]] (가벼움)&lt;/li&gt;
&lt;li&gt;[[Redis Pub Sub vs Streams]]&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]]&lt;/li&gt;
&lt;li&gt;[[idempotency-keys]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>rabbitmq</category><category>amqp</category><category>message-broker</category><category>queue</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Pattern] Microservices vs Monolith: 언제 분리, 언제 통합</title><link>https://shinkeonkim.com/wiki/distributed-systems/microservices-vs-monolith/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/microservices-vs-monolith/</guid><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;-&lt;/th&gt;&lt;th&gt;Monolith&lt;/th&gt;&lt;th&gt;Modular Monolith&lt;/th&gt;&lt;th&gt;Microservices&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;배포 단위&lt;/td&gt;&lt;td&gt;1개&lt;/td&gt;&lt;td&gt;1개 (모듈 명확)&lt;/td&gt;&lt;td&gt;N개&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;데이터&lt;/td&gt;&lt;td&gt;단일 DB&lt;/td&gt;&lt;td&gt;단일 DB (스키마 분리)&lt;/td&gt;&lt;td&gt;DB per service&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;통신&lt;/td&gt;&lt;td&gt;함수 호출&lt;/td&gt;&lt;td&gt;함수 호출&lt;/td&gt;&lt;td&gt;네트워크 (HTTP/gRPC)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;운영 복잡도&lt;/td&gt;&lt;td&gt;&lt;em&gt;낮음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;&lt;em&gt;높음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;스케일링&lt;/td&gt;&lt;td&gt;전체 함께&lt;/td&gt;&lt;td&gt;전체 함께&lt;/td&gt;&lt;td&gt;service 별 독립&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;장애 격리&lt;/td&gt;&lt;td&gt;약함&lt;/td&gt;&lt;td&gt;약함&lt;/td&gt;&lt;td&gt;강함 (이상적)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;적합 팀&lt;/td&gt;&lt;td&gt;&amp;#x3C; 30&lt;/td&gt;&lt;td&gt;&amp;#x3C; 100&lt;/td&gt;&lt;td&gt;100+&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;결정-트리&quot;&gt;결정 트리&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{팀 크기}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;&amp;#x3C; 10&quot;| Mono[Monolith]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;10-50&quot;| Q2{성장 속도?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|느림| Mono&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|빠름| MM[Modular Monolith]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;50-200&quot;| MM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;200+&quot;| MS[Microservices]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    MM --&gt;|&quot;진짜 필요해지면&quot;| MS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;시작은-monolith-fowler-의-monolith-first&quot;&gt;시작은 Monolith (Fowler 의 “Monolith First”)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Start[새 프로젝트] --&gt; Mono[&quot;Monolith 로 시작&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Mono --&gt; Grow[&quot;성장 + 도메인 명확&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Grow --&gt; MM[Modular Monolith]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    MM --&gt; Carve[&quot;필요시 service 분리&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Carve --&gt; MS[&quot;Microservices 일부&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;대부분의 새 프로젝트는 monolith 로 시작&lt;/em&gt; 이 정답. &lt;em&gt;마이크로서비스로 시작&lt;/em&gt; 한 팀이 &lt;em&gt;2년 후 distributed monolith&lt;/em&gt; 가 되는 사례가 &lt;em&gt;압도적&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;microservices-가-진짜-도움될-때&quot;&gt;Microservices 가 &lt;em&gt;진짜&lt;/em&gt; 도움될 때&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{독립 배포 절실?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|예| C1[조건 1 ✓]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2{팀 별 자율성 절실?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|예| C2[조건 2 ✓]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3{스케일 차이 큼?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3 --&gt;|예| C3[조건 3 ✓]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C1 &amp;#x26; C2 &amp;#x26; C3 --&gt;|모두 ✓| Go[Microservices 도입]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;distributed-monolith-안티-패턴&quot;&gt;Distributed Monolith (안티 패턴)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A[Service A] --&gt;|동기 호출| B[Service B]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    B --&gt;|동기 호출| C[Service C]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|동기 호출| D[Service D]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note1[하나 다운 = 전체 다운]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;증상:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여러 service 가 &lt;em&gt;함께 배포&lt;/em&gt; 되어야 함 (calling contract 변경 시)&lt;/li&gt;
&lt;li&gt;한 service 다운 = &lt;em&gt;전체 다운&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;분산의 비용 + monolith 의 결합&lt;/em&gt; 동시&lt;/li&gt;
&lt;li&gt;&lt;em&gt;코드 변경 영향이 service 경계 넘김&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;strong&gt;마이크로서비스의 최악의 결과&lt;/strong&gt;. &lt;em&gt;monolith 보다 모든 면에서 나쁨&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;service-boundary-가르는-기준&quot;&gt;Service Boundary 가르는 기준&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Best[좋은 boundary]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Best --&gt; B1[&quot;Domain (DDD bounded context)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Best --&gt; B2[&quot;Team ownership (Conway&apos;s Law)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Best --&gt; B3[데이터 모델 자연 분리]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad[나쁜 boundary]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; X1[&quot;기술 계층 (API / DB / UI)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; X2[&quot;유사 데이터 기준 (DB 테이블 1:1)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; X3[CRUD 단위 분리]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;ddd-bounded-context&quot;&gt;DDD Bounded Context&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Domain-Driven Design&lt;/em&gt; 에서 &lt;em&gt;Bounded Context&lt;/em&gt; = &lt;em&gt;독립적인 도메인 모델이 유효한 경계&lt;/em&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Order[&quot;Order Context&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        O_Order[Order]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        O_Item[OrderItem]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        O_Price[Price]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Inventory[&quot;Inventory Context&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        I_Product[Product]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        I_Stock[Stock]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Payment[&quot;Payment Context&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        P_Payment[Payment]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        P_Invoice[Invoice]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Order -.-&gt;|&quot;ProductId (ref only)&quot;| Inventory&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Order -.-&gt;|&quot;PaymentId (ref only)&quot;| Payment&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;각 context 안에서 &lt;em&gt;같은 이름이라도 의미가 다름&lt;/em&gt; (Order 의 &lt;code&gt;Product&lt;/code&gt; vs Inventory 의 &lt;code&gt;Product&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Bounded Context 경계 = &lt;em&gt;Microservice 분리 경계의 자연스러운 후보&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Context Map&lt;/em&gt; 으로 context 간 관계 (ACL, Shared Kernel, Partnership) 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conways-law&quot;&gt;Conway’s Law&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“시스템 구조는 그것을 만든 조직의 의사소통 구조를 닮는다”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Team1[&quot;Team A&amp;#x3C;br/&gt;(독립 의사소통)&quot;] --&gt; Service1[Service A]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Team2[&quot;Team B&amp;#x3C;br/&gt;(독립 의사소통)&quot;] --&gt; Service2[Service B]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Team3[&quot;Team C&amp;#x3C;br/&gt;(독립 의사소통)&quot;] --&gt; Service3[Service C]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Team1 -.협업.-&gt; Team2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Service1 -.API.-&gt; Service2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;팀 경계와 service 경계의 일치&lt;/em&gt; 가 &lt;em&gt;Conway’s Law 의 실용적 활용&lt;/em&gt;. 한 service 가 &lt;em&gt;두 팀에 걸치면&lt;/em&gt; 운영 사고.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;inverse-conway-maneuver&quot;&gt;Inverse Conway Maneuver&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;반대로 service 구조부터 정해 → 팀 구조 맞추기&lt;/em&gt;. &lt;em&gt;큰 조직 재편&lt;/em&gt; 에서 사용.&lt;/p&gt;
&lt;h2 id=&quot;api-통신-패턴-동기-vs-비동기&quot;&gt;API 통신 패턴: 동기 vs 비동기&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sync[&quot;동기 (HTTP / gRPC)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sync --&gt; S1[즉각 응답 필요]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sync --&gt; S2[결과 바로 사용]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sync --&gt; S3[&quot;단점: 결합, 장애 전파&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Async[&quot;비동기 (Kafka / NATS / RabbitMQ)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Async --&gt; A1[결과 나중에 처리]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Async --&gt; A2[&quot;단점: eventual consistency&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Async --&gt; A3[&quot;장점: 결합 없음, 장애 격리&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;패턴&lt;/th&gt;&lt;th&gt;사용처&lt;/th&gt;&lt;th&gt;예시&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;동기 REST/gRPC&lt;/td&gt;&lt;td&gt;즉각 응답, 읽기&lt;/td&gt;&lt;td&gt;상품 조회, 인증&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;비동기 이벤트&lt;/td&gt;&lt;td&gt;후속 처리, 팬아웃&lt;/td&gt;&lt;td&gt;결제 완료 후 알림 발송&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Saga&lt;/td&gt;&lt;td&gt;분산 트랜잭션&lt;/td&gt;&lt;td&gt;주문 생성 + 재고 감소 + 결제&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;데이터-일관성-전략&quot;&gt;데이터 일관성 전략&lt;/h2&gt;
&lt;p&gt;분산 트랜잭션 (2PC) 은 &lt;em&gt;마이크로서비스에서 피해야 할 패턴&lt;/em&gt;. 대안:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant O as Order Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant E as Event Bus&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant I as Inventory Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant P as Payment Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;E: OrderCreated (event)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    E-&gt;&gt;I: OrderCreated&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    I-&gt;&gt;I: 재고 감소&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    I-&gt;&gt;E: StockReserved&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    E-&gt;&gt;P: StockReserved&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P-&gt;&gt;P: 결제 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P-&gt;&gt;E: PaymentCompleted&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    E-&gt;&gt;O: PaymentCompleted&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;O: 주문 확정&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Choreography-based Saga&lt;/em&gt;: 각 서비스가 이벤트를 듣고 &lt;em&gt;다음 단계를 직접 실행&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;분리-비용-카탈로그&quot;&gt;분리 비용 카탈로그&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost[Microservices 의 추가 비용]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost --&gt; Net[네트워크 호출]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost --&gt; Obs[&quot;관측성 (Tracing 필수)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost --&gt; Auth[&quot;mTLS / Service Mesh&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost --&gt; Data[&quot;데이터 일관성 (Saga)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost --&gt; Deploy[&quot;K8s, CI/CD 복잡도&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost --&gt; Test[&quot;Contract / E2E 테스트&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cost --&gt; Skill[엔지니어 학습 비용]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;service-mesh-개요&quot;&gt;Service Mesh 개요&lt;/h2&gt;
&lt;p&gt;마이크로서비스 간 &lt;em&gt;네트워크 복잡성&lt;/em&gt; 을 인프라 레벨에서 해결:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;기능&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;mTLS&lt;/td&gt;&lt;td&gt;service 간 자동 암호화 + 인증&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Load balancing&lt;/td&gt;&lt;td&gt;L7 수준, 헬스체크 기반&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Circuit breaker&lt;/td&gt;&lt;td&gt;연쇄 장애 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Observability&lt;/td&gt;&lt;td&gt;자동 trace / metric 수집&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Traffic management&lt;/td&gt;&lt;td&gt;canary, retry, timeout&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;대표 구현: &lt;em&gt;Istio&lt;/em&gt; (풍부한 기능), &lt;em&gt;Linkerd&lt;/em&gt; (경량), &lt;em&gt;Cilium&lt;/em&gt; (eBPF 기반).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
Service Mesh 는 &lt;em&gt;마이크로서비스의 운영 복잡도 비용&lt;/em&gt; 의 일부를 낮춰주지만, &lt;em&gt;도입 자체도 학습 비용이 크다&lt;/em&gt;. 팀 규모와 트래픽 수준에 맞게 도입.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;모듈-모놀리스-modular-monolith&quot;&gt;모듈 모놀리스 (Modular Monolith)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph App[Single deployable]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        U[User module]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        O[Order module]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        P[Payment module]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    U -.interface only.-&gt; O&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O -.interface only.-&gt; P&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App --&gt; DB[(&quot;단일 DB&amp;#x3C;br/&gt;스키마 분리&quot;)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;모듈 간 직접 호출 금지&lt;/em&gt;. &lt;em&gt;interface만&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;같은 DB, &lt;em&gt;다른 스키마&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;나중에 service 분리 쉬움&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대부분의 회사가 결국 여기서 멈춤&lt;/strong&gt; (충분).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;너무 일찍 분리&lt;/strong&gt; = 도메인 잘못 잡고 &lt;em&gt;분리 후 재경계&lt;/em&gt;. 가장 큰 후회.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DB 공유&lt;/strong&gt; = “Service 단위 분리” 가 &lt;em&gt;DB 결합&lt;/em&gt; 으로 &lt;em&gt;무의미&lt;/em&gt;. 각 service 자기 DB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;분산 트랜잭션 (2PC)&lt;/strong&gt; = 마이크로서비스의 &lt;em&gt;영혼&lt;/em&gt;. Saga 패턴으로 대체.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shared library 의 영원한 결합&lt;/strong&gt; = service A 가 라이브러리 v2 로 못 올라가면 &lt;em&gt;모든 service&lt;/em&gt; 정체.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bounded Context 무시&lt;/strong&gt; = 잘못된 경계로 분리 후 service 간 &lt;em&gt;결합이 더 강해짐&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[api-gateway]]&lt;/li&gt;
&lt;li&gt;[[saga-pattern]]&lt;/li&gt;
&lt;li&gt;[[cqrs]], [[event-sourcing]]&lt;/li&gt;
&lt;li&gt;[[Service Mesh]]&lt;/li&gt;
&lt;li&gt;[[distributed-systems-distributed-transaction]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>microservices</category><category>monolith</category><category>architecture</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Pattern] API Gateway: BFF, 라우팅, auth, rate limit</title><link>https://shinkeonkim.com/wiki/distributed-systems/api-gateway/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/api-gateway/</guid><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;API Gateway&lt;/strong&gt; = &lt;em&gt;클라이언트와 마이크로서비스 사이의 단일 진입점&lt;/em&gt;. 라우팅, auth, rate limit, transformation, monitoring 의 &lt;em&gt;cross-cutting 책임&lt;/em&gt; 을 모음.&lt;/p&gt;
&lt;h2 id=&quot;역할&quot;&gt;역할&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client[Mobile / Web / IoT] --&gt; GW[API Gateway]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW --&gt;|/users/*| US[User Service]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW --&gt;|/orders/*| OS[Order Service]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW --&gt;|/payments/*| PS[Payment Service]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW -.cross-cutting.-&gt; Auth[Auth]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW -.cross-cutting.-&gt; RL[Rate Limit]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW -.cross-cutting.-&gt; Log[Logging]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW -.cross-cutting.-&gt; Cache[Cache]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW -.cross-cutting.-&gt; Transform[Request/Response transform]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;요청-처리-파이프라인&quot;&gt;요청 처리 파이프라인&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Req[Request] --&gt; Auth[Auth 검증]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth --&gt;|fail| E401[401]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth --&gt;|pass| RL[Rate Limit]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    RL --&gt;|limit| E429[429]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    RL --&gt;|pass| Route[라우팅]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Route --&gt; ReqT[Request Transform]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ReqT --&gt; Back[Backend]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Back --&gt; RespT[Response Transform]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    RespT --&gt; Log[로깅]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Log --&gt; Res[Response]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;모든 요청이 이 파이프라인을 통과한다. 각 단계는 plugin 또는 middleware 로 구현된다. 실패 시 해당 단계에서 즉시 응답을 반환한다.&lt;/p&gt;
&lt;h2 id=&quot;책임-카탈로그&quot;&gt;책임 카탈로그&lt;/h2&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;책임&lt;/th&gt;&lt;th&gt;예시&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;라우팅&lt;/td&gt;&lt;td&gt;path / header / host 기준&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;인증 / 인가&lt;/td&gt;&lt;td&gt;JWT 검증, OAuth, API key&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Rate limiting&lt;/td&gt;&lt;td&gt;per-API key, per-IP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Transformation&lt;/td&gt;&lt;td&gt;request/response 변환 (gRPC, REST)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;캐싱&lt;/td&gt;&lt;td&gt;응답 캐시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Load balancing&lt;/td&gt;&lt;td&gt;백엔드 분산&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Circuit breaker&lt;/td&gt;&lt;td&gt;백엔드 다운 시 차단&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Logging / metrics&lt;/td&gt;&lt;td&gt;중앙 집계&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;WAF&lt;/td&gt;&lt;td&gt;SQLi, XSS 차단&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TLS termination&lt;/td&gt;&lt;td&gt;인증서 관리&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Versioning&lt;/td&gt;&lt;td&gt;URL / 헤더 기반 분기&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;인증-플로우&quot;&gt;인증 플로우&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant GW as Gateway&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Auth as AuthSvc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Back as Backend&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;GW: GET /api/orders, Authorization Bearer JWT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW-&gt;&gt;Auth: POST /verify, token&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Auth--&gt;&gt;GW: 200 OK, user_id and roles&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW-&gt;&gt;Back: GET /orders, X-User-Id header&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Back--&gt;&gt;GW: 200 OK, order list&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GW--&gt;&gt;C: 200 OK, order list&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gateway 가 JWT 를 검증하고 &lt;code&gt;X-User-Id&lt;/code&gt; 헤더를 추가해 백엔드로 전달한다. 백엔드는 JWT 를 다시 검증하지 않아도 된다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
Zero-trust 환경에서는 백엔드도 mTLS 또는 별도 인증 토큰으로 검증한다. Gateway 신뢰만으로는 lateral movement 공격에 취약하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;bff-backend-for-frontend&quot;&gt;BFF (Backend For Frontend)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    iOS[iOS App] --&gt; iOSGW[iOS BFF]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Web[Web App] --&gt; WebGW[Web BFF]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Android[Android App] --&gt; AnGW[Android BFF]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    iOSGW &amp;#x26; WebGW &amp;#x26; AnGW --&gt; Services[(공통 services)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;각 클라이언트 종류마다 별도 gateway&lt;/em&gt;. 클라이언트 별 &lt;em&gt;응답 형식 / 페이지네이션 / 압축&lt;/em&gt; 차이 흡수. &lt;em&gt;모바일 = 작은 응답, 웹 = 큰 응답&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;bff-패턴의-장점&quot;&gt;BFF 패턴의 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;클라이언트 팀이 BFF 를 직접 소유하고 배포 가능&lt;/li&gt;
&lt;li&gt;공통 백엔드를 수정하지 않고 클라이언트별 최적화 가능&lt;/li&gt;
&lt;li&gt;응답 데이터 형식, GraphQL/REST 혼용도 BFF 레벨에서 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;kong-설정-예시&quot;&gt;Kong 설정 예시&lt;/h2&gt;
&lt;p&gt;Kong 은 Nginx 기반 오픈소스 API Gateway. 선언적 YAML 설정이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;_format_version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;3.0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;services&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;order-service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    url&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;http://order-svc:8080&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    routes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orders-route&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        paths&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/api/orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        methods&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;GET&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;POST&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    plugins&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;jwt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        config&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          secret_is_base64&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          key_claim_name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;kid&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;rate-limiting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        config&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          minute&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          policy&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;redis&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          redis_host&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;redis-host&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          redis_port&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;6379&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;request-transformer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        config&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          add&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;            headers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;              - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;X-Gateway-Version:2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Kong 적용&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;deck&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; gateway&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; sync&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; kong.yaml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;aws-api-gateway&quot;&gt;AWS API Gateway&lt;/h2&gt;
&lt;p&gt;AWS 에서 관리되는 fully managed gateway. Lambda 통합이 강점이다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# SAM template 예시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Resources&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  ApiGateway&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    Type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;AWS::Serverless::Api&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    Properties&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      StageName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;prod&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      Auth&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        DefaultAuthorizer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;CognitoAuthorizer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        Authorizers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          CognitoAuthorizer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;            UserPoolArn&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!GetAtt&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; UserPool.Arn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      Cors&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        AllowMethods&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&apos;GET,POST,PUT,DELETE&apos;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        AllowHeaders&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&apos;Content-Type,Authorization&apos;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        AllowOrigin&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&apos;*&apos;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  OrderFunction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    Type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;AWS::Serverless::Function&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    Properties&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      Handler&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;index.handler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      Runtime&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;nodejs20.x&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      Events&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        GetOrders&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          Type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Api&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          Properties&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;            RestApiId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!Ref&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ApiGateway&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;            Path&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;            Method&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;GET&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;HTTP API&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;REST API&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;지연 시간&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;낮음&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;높음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;기능&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;기본&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;완전 (usage plan, caching)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;비용&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;저렴&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;비쌈&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;gRPC&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;미지원&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;미지원&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;envoy--grpc-트랜스코딩&quot;&gt;Envoy + gRPC 트랜스코딩&lt;/h2&gt;
&lt;p&gt;Envoy 는 Lyft 가 만든 C++ proxy. Service mesh 의 data plane 이자 standalone gateway 로도 쓰인다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# envoy.yaml: gRPC-JSON transcoding&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;http_filters&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;envoy.filters.http.grpc_json_transcoder&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    typed_config&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;@type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      proto_descriptor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/etc/envoy/api.pb&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      services&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;mypackage.UserService&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      print_options&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        add_whitespace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        always_print_primitive_fields&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTP JSON 요청이 들어오면 Envoy 가 gRPC 로 변환해 백엔드로 전달한다. 클라이언트는 REST 로, 내부는 gRPC 로 통신할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;rate-limiting-at-gateway&quot;&gt;Rate Limiting at Gateway&lt;/h2&gt;
&lt;p&gt;Gateway 레벨 rate limit 은 Redis 로 분산 카운터를 관리해야 한다. 단일 노드 in-memory 카운터는 인스턴스가 여럿이면 N배 한도가 허용된다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Redis Lua atom: sliding window counter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SLIDING_WINDOW_LUA&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;local key = KEYS[1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;local now = tonumber(ARGV[1])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;local window = tonumber(ARGV[2])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;local limit = tonumber(ARGV[3])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;redis.call(&apos;ZREMRANGEBYSCORE&apos;, key, 0, now - window)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;local count = redis.call(&apos;ZCARD&apos;, key)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;if count &amp;#x3C; limit then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    redis.call(&apos;ZADD&apos;, key, now, now)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    redis.call(&apos;EXPIRE&apos;, key, window)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    return 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;return 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kong / Envoy / AWS API Gateway 모두 Redis 기반 rate limit 을 기본 지원한다.&lt;/p&gt;
&lt;h2 id=&quot;api-gateway-vs-service-mesh&quot;&gt;API Gateway vs Service Mesh&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;API Gateway&lt;/th&gt;&lt;th&gt;Service Mesh&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;위치&lt;/td&gt;&lt;td&gt;&lt;em&gt;외부 ↔ 내부 경계&lt;/em&gt; (north-south)&lt;/td&gt;&lt;td&gt;&lt;em&gt;내부 ↔ 내부&lt;/em&gt; (east-west)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;책임&lt;/td&gt;&lt;td&gt;인증, transformation, public API&lt;/td&gt;&lt;td&gt;mTLS, retry, circuit, observability&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;클라이언트 인지&lt;/td&gt;&lt;td&gt;직접 호출&lt;/td&gt;&lt;td&gt;sidecar 자동&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;예&lt;/td&gt;&lt;td&gt;Kong, AWS API Gateway, Envoy&lt;/td&gt;&lt;td&gt;Istio, Linkerd&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;둘 다 운영하는 경우가 흔하다. &lt;em&gt;gateway 가 north-south&lt;/em&gt;, &lt;em&gt;mesh 가 east-west&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;도구-비교&quot;&gt;도구 비교&lt;/h2&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;도구&lt;/th&gt;&lt;th&gt;종류&lt;/th&gt;&lt;th&gt;강점&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Kong&lt;/td&gt;&lt;td&gt;OSS gateway&lt;/td&gt;&lt;td&gt;plugin 생태계&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Tyk&lt;/td&gt;&lt;td&gt;OSS gateway&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Envoy&lt;/td&gt;&lt;td&gt;Proxy&lt;/td&gt;&lt;td&gt;gRPC, L7, mesh 토대&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AWS API Gateway&lt;/td&gt;&lt;td&gt;Managed&lt;/td&gt;&lt;td&gt;Lambda 통합&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cloudflare Workers&lt;/td&gt;&lt;td&gt;Edge&lt;/td&gt;&lt;td&gt;글로벌&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Apigee&lt;/td&gt;&lt;td&gt;Enterprise&lt;/td&gt;&lt;td&gt;엔터프라이즈&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Apollo Router&lt;/td&gt;&lt;td&gt;GraphQL&lt;/td&gt;&lt;td&gt;federation&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;패턴-graphql-federation-gateway&quot;&gt;패턴: GraphQL Federation Gateway&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client --&gt; Router[Apollo Router]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Router --&gt; A[User subgraph]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Router --&gt; B[Order subgraph]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Router --&gt; C[Payment subgraph]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 건 [[graphql]] 참고.&lt;/p&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Gateway 가 &lt;em&gt;모든 비즈니스 로직&lt;/em&gt; 흡수&lt;/strong&gt; = monolith 의 &lt;em&gt;재림&lt;/em&gt;. cross-cutting 만.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단일 gateway 의 &lt;em&gt;bottleneck&lt;/em&gt;&lt;/strong&gt; = HA + auto-scaling 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth 검증 &lt;em&gt;백엔드 마다&lt;/em&gt; 다시&lt;/strong&gt; = gateway 가 검증 → 백엔드는 &lt;em&gt;trust&lt;/em&gt;. 단 &lt;em&gt;zero-trust&lt;/em&gt; 환경에서는 백엔드도 검증.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limit 의 &lt;em&gt;분산 상태&lt;/em&gt;&lt;/strong&gt; = 단일 노드 카운터 = race. 중앙 store (Redis) + sliding window.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[microservices-vs-monolith]]&lt;/li&gt;
&lt;li&gt;[[Service Mesh]], mTLS, sidecar&lt;/li&gt;
&lt;li&gt;[[Load Balancer]]&lt;/li&gt;
&lt;li&gt;[[JWT]], [[OAuth2]]&lt;/li&gt;
&lt;li&gt;[[graphql]], federation&lt;/li&gt;
&lt;li&gt;[[rate-limiting]], gateway 레벨 rate limit 구현&lt;/li&gt;
&lt;li&gt;[[circuit-breaker]], 백엔드 장애 전파 차단&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>api-gateway</category><category>microservices</category><category>backend</category><category>infrastructure</category><author>koa (김신건)</author></item><item><title>[Distributed Systems] CAP Theorem과 PACELC</title><link>https://shinkeonkim.com/wiki/distributed-systems/cap-theorem/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/cap-theorem/</guid><pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CAP Theorem&lt;/strong&gt; (Eric Brewer, 2000): 분산 시스템에서 &lt;strong&gt;3가지 중 2개만 동시에 보장 가능&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C (Consistency)&lt;/strong&gt;: 모든 노드가 같은 시점에 같은 데이터 반환 (강한 일관성)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A (Availability)&lt;/strong&gt;: 모든 요청이 응답 받음 (실패도 응답)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P (Partition Tolerance)&lt;/strong&gt;: 네트워크 분할이 있어도 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현실: &lt;strong&gt;P는 선택지 아님&lt;/strong&gt; (네트워크는 항상 깨질 수 있음). 따라서 실질적 선택은:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CP&lt;/strong&gt;: 분할 시 일관성 우선 → 일부 노드 응답 안 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AP&lt;/strong&gt;: 분할 시 가용성 우선 → 일관성 일시 깨짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;cp-vs-ap-선택-다이어그램&quot;&gt;CP vs AP 선택 다이어그램&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P{&quot;네트워크 분할 발생&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt;|&quot;CP 선택&quot;| CP[&quot;일관성 우선\n일부 노드 응답 거부\n(minority partition 정지)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt;|&quot;AP 선택&quot;| AP[&quot;가용성 우선\n모든 노드 응답\n(일시적 불일치 허용)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CP --&gt; CPex[&quot;etcd, Consul, ZooKeeper\nHBase, Spanner\nPostgreSQL (single)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    AP --&gt; APex[&quot;Cassandra, DynamoDB\nCouchDB, Riak\nDNS&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;실제-시스템-매핑&quot;&gt;실제 시스템 매핑&lt;/h2&gt;






































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;시스템&lt;/th&gt;&lt;th&gt;CAP 분류&lt;/th&gt;&lt;th&gt;이유&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;etcd&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;CP&lt;/td&gt;&lt;td&gt;Raft 합의, minority 정지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Consul&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;CP&lt;/td&gt;&lt;td&gt;Raft 기반, quorum 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ZooKeeper&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;CP&lt;/td&gt;&lt;td&gt;ZAB 프로토콜, leader 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Cassandra&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AP&lt;/td&gt;&lt;td&gt;모든 노드 응답, eventual consistency&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DynamoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AP (기본)&lt;/td&gt;&lt;td&gt;가용성 우선, 강한 일관성 옵션&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Riak&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AP&lt;/td&gt;&lt;td&gt;vector clock 기반 충돌 해결&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CouchDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AP&lt;/td&gt;&lt;td&gt;MVCC, eventual consistency&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;HBase&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;CP&lt;/td&gt;&lt;td&gt;HDFS + ZooKeeper&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Spanner&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;CP&lt;/td&gt;&lt;td&gt;TrueTime, global consistency&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MongoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;CP (기본)&lt;/td&gt;&lt;td&gt;primary-only write&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Redis Cluster&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AP&lt;/td&gt;&lt;td&gt;분할 시 일부 write 손실 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DNS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AP&lt;/td&gt;&lt;td&gt;캐시로 stale 응답 허용&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
이 분류는 단순화. 실제 시스템은 설정에 따라 달라진다. DynamoDB는 &lt;code&gt;ConsistentRead=true&lt;/code&gt;로 CP에 가깝게, Cassandra는 &lt;code&gt;CONSISTENCY ALL&lt;/code&gt;로 CP에 가깝게 동작 가능.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;etcd--consul-cp-상세&quot;&gt;etcd / Consul: CP 상세&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as &quot;Client&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant L as &quot;Leader (etcd)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant F1 as &quot;Follower 1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant F2 as &quot;Follower 2 (분리됨)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;L: write key=value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L-&gt;&gt;F1: AppendEntries (Raft)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L-&gt;&gt;F2: AppendEntries (Raft)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    F1--&gt;&gt;L: ack&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over F2: 네트워크 분할&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L-&gt;&gt;L: quorum (2/3) 달성 → commit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L--&gt;&gt;C: OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;F2: read (분리된 노드)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    F2--&gt;&gt;C: 오류 또는 stale 거부&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Raft quorum (N/2 + 1)이 없으면 write 거부&lt;/li&gt;
&lt;li&gt;minority partition은 read도 거부 (stale 방지)&lt;/li&gt;
&lt;li&gt;분할 복구 후 자동 동기화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;cassandra-ap-상세&quot;&gt;Cassandra: AP 상세&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph DC1[&quot;DC1 (분할됨)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        N1[&quot;Node A\nwrite: x=1&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph DC2[&quot;DC2 (분할됨)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        N2[&quot;Node B\nwrite: x=2&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N1 -.분할 복구.-&gt; N2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N2 -.충돌 해결 LWW.-&gt; Result[&quot;x=2 (최신 timestamp 승리)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;분할 중에도 양쪽 모두 write 허용&lt;/li&gt;
&lt;li&gt;복구 후 &lt;em&gt;Last Write Wins (LWW)&lt;/em&gt; 또는 &lt;em&gt;vector clock&lt;/em&gt;으로 충돌 해결&lt;/li&gt;
&lt;li&gt;일시적으로 다른 사용자가 다른 결과를 볼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;cap의-한계와-pacelc&quot;&gt;CAP의 한계와 PACELC&lt;/h2&gt;
&lt;p&gt;CAP는 “분할 시” 시나리오만 다룸. &lt;strong&gt;정상 작동 시의 trade-off&lt;/strong&gt;는?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PACELC&lt;/strong&gt; (Daniel Abadi, 2010):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P&lt;/strong&gt;artition 시: &lt;strong&gt;A&lt;/strong&gt;vailability vs &lt;strong&gt;C&lt;/strong&gt;onsistency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E&lt;/strong&gt;lse (정상): &lt;strong&gt;L&lt;/strong&gt;atency vs &lt;strong&gt;C&lt;/strong&gt;onsistency&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{&quot;네트워크 상태&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;분할 발생 (P)&quot;| PQ{&quot;A vs C&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;정상 (E)&quot;| EQ{&quot;L vs C&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    PQ --&gt;|&quot;A 선택&quot;| PA[&quot;PA: Cassandra, DynamoDB, Riak&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    PQ --&gt;|&quot;C 선택&quot;| PC[&quot;PC: etcd, HBase, Spanner&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    EQ --&gt;|&quot;L 선택&quot;| EL[&quot;EL: Cassandra, DynamoDB&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    EQ --&gt;|&quot;C 선택&quot;| EC[&quot;EC: Spanner, VoltDB&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;시스템&lt;/th&gt;&lt;th&gt;PACELC&lt;/th&gt;&lt;th&gt;해석&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;MongoDB&lt;/td&gt;&lt;td&gt;PA / EC&lt;/td&gt;&lt;td&gt;분할 시 A, 정상 시 C&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cassandra&lt;/td&gt;&lt;td&gt;PA / EL&lt;/td&gt;&lt;td&gt;분할 시 A, 정상 시 L (낮은 latency)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;HBase&lt;/td&gt;&lt;td&gt;PC / EC&lt;/td&gt;&lt;td&gt;항상 C&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DynamoDB&lt;/td&gt;&lt;td&gt;PA / EL&lt;/td&gt;&lt;td&gt;항상 가용성 / latency 우선&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PostgreSQL&lt;/td&gt;&lt;td&gt;PC / EC&lt;/td&gt;&lt;td&gt;단일 노드 강한 일관성&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Spanner&lt;/td&gt;&lt;td&gt;PC / EC&lt;/td&gt;&lt;td&gt;글로벌 강한 일관성&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;PACELC가 더 실제적: 정상 시간이 훨씬 길기 때문.&lt;/p&gt;
&lt;h2 id=&quot;conflict-resolution-충돌-해결&quot;&gt;Conflict Resolution (충돌 해결)&lt;/h2&gt;
&lt;p&gt;AP 시스템에서 분할 후 복구 시 충돌 해결 방법:&lt;/p&gt;
&lt;h3 id=&quot;last-write-wins-lww&quot;&gt;Last Write Wins (LWW)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Node A: x=1 at t=100&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Node B: x=2 at t=101&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ x=2 승리 (최신 timestamp)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;단순하지만 &lt;em&gt;clock skew&lt;/em&gt; 위험 (NTP 오차로 잘못된 승리)&lt;/li&gt;
&lt;li&gt;Cassandra 기본 전략&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;vector-clock&quot;&gt;Vector Clock&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Node A: x=1, vc=[A:1, B:0]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Node B: x=2, vc=[A:0, B:1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 충돌 감지 (두 vc가 비교 불가)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 애플리케이션이 해결 (또는 두 값 모두 보존)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;인과 관계 추적 가능&lt;/li&gt;
&lt;li&gt;Riak, Amazon Dynamo 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;crdt-conflict-free-replicated-data-type&quot;&gt;CRDT (Conflict-free Replicated Data Type)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G-Counter: 각 노드가 자기 카운터만 증가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;merge: max(A.count, B.count) per node&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 충돌 없이 자동 병합&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;특정 데이터 타입 (counter, set, map)에서 자동 병합&lt;/li&gt;
&lt;li&gt;Redis CRDT, Riak 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;consistency-spectrum&quot;&gt;Consistency Spectrum&lt;/h2&gt;
&lt;p&gt;CAP의 “C”는 strong consistency. 실무는 더 세밀한 spectrum:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S[&quot;Strong Consistency\n가장 엄격&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S --&gt; L[&quot;Linearizability\n실시간 순서 보장&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L --&gt; Seq[&quot;Sequential Consistency&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Seq --&gt; Cau[&quot;Causal Consistency\n인과 관계만 보장&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cau --&gt; E[&quot;Eventual Consistency\n가장 느슨&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;수준&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;th&gt;예시&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Linearizability&lt;/td&gt;&lt;td&gt;실시간 순서 보장, 가장 강함&lt;/td&gt;&lt;td&gt;etcd, Spanner&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sequential&lt;/td&gt;&lt;td&gt;모든 노드가 같은 순서로 봄&lt;/td&gt;&lt;td&gt;ZooKeeper&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Causal&lt;/td&gt;&lt;td&gt;인과 관계 있는 연산만 순서 보장&lt;/td&gt;&lt;td&gt;MongoDB causal sessions&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Eventual&lt;/td&gt;&lt;td&gt;결국 수렴, 시간 보장 없음&lt;/td&gt;&lt;td&gt;Cassandra (ONE), DNS&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;대부분 AP 시스템 = eventual. CP 시스템 = strong (or linearizable).&lt;/p&gt;
&lt;h3 id=&quot;tunable-consistency-cassandra-패턴&quot;&gt;Tunable consistency (Cassandra 패턴)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;CONSISTENCY LEVEL = ONE / QUORUM / ALL / EACH_QUORUM&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;ONE: 1개 노드 응답이면 OK (빠르지만 stale 위험)&lt;/li&gt;
&lt;li&gt;QUORUM: 과반수 (N/2 + 1)&lt;/li&gt;
&lt;li&gt;ALL: 모든 replica 응답 (느리지만 가장 일관)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;write QUORUM + read QUORUM = strong consistency 보장 (N=3이면 2+2 = 4 &gt; 3).&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- Cassandra CQL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;CONSISTENCY QUORUM;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ?;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;실무-선택-기준&quot;&gt;실무 선택 기준&lt;/h2&gt;
&lt;h3 id=&quot;금융--결제-cp&quot;&gt;금융 / 결제: CP&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;계좌 잔액은 절대 stale 안 됨&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ HBase, PostgreSQL (single instance), Spanner&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 분할 시 거래 차단이 안전&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;sns--feed-ap&quot;&gt;SNS / Feed: AP&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;타임라인이 일시적으로 다르게 보여도 됨&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ Cassandra, DynamoDB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 분할이라도 게시 / 조회 가능&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;검색-인덱스-ap-eventual&quot;&gt;검색 인덱스: AP (eventual)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;검색 결과가 몇 초 지연돼도 OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ Elasticsearch (near real-time)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;서비스-디스커버리-cp&quot;&gt;서비스 디스커버리: CP&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;잘못된 서비스 주소 = 장애&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ etcd, Consul (Raft 기반)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 분할 시 등록 거부가 안전&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;메시지-큐-두-모드&quot;&gt;메시지 큐: 두 모드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Kafka: AP (high throughput)&lt;/li&gt;
&lt;li&gt;RabbitMQ: 모드별 선택 (mirror queue는 CP)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;cap의-흔한-오해&quot;&gt;CAP의 흔한 오해&lt;/h2&gt;
&lt;h3 id=&quot;오해-1-ca-시스템도-가능&quot;&gt;오해 1: CA 시스템도 가능?&lt;/h3&gt;
&lt;p&gt;NO. 분산 환경에서 P는 선택지 아님 (네트워크는 항상 깨질 수 있음). “CA”는 single-node 시스템 (MySQL standalone).&lt;/p&gt;
&lt;h3 id=&quot;오해-2-ap--일관성-없음&quot;&gt;오해 2: AP = 일관성 없음?&lt;/h3&gt;
&lt;p&gt;NO. AP는 eventual consistency 또는 weaker. 결국엔 수렴.&lt;/p&gt;
&lt;h3 id=&quot;오해-3-cap가-영구-선택&quot;&gt;오해 3: CAP가 영구 선택?&lt;/h3&gt;
&lt;p&gt;NO. 분할 발생 시점에만 trade-off. 정상 시간엔 PACELC의 E (latency vs consistency).&lt;/p&gt;
&lt;h3 id=&quot;오해-4-p는-datacenter-분할만&quot;&gt;오해 4: P는 datacenter 분할만?&lt;/h3&gt;
&lt;p&gt;분할 = network가 잠시라도 깨짐. 짧은 packet loss / latency spike도 포함. 빈번히 발생.&lt;/p&gt;
&lt;h2 id=&quot;consensus와의-관계&quot;&gt;consensus와의 관계&lt;/h2&gt;
&lt;p&gt;CAP의 “C”를 보장하려면 합의 (consensus) 필요. Raft, Paxos 같은 알고리즘이 분할 시 majority만 진행:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;5-node cluster&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── 3-node majority (가능, 진행)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── 2-node minority (정지)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;majority가 사라지면 (3+ 노드 분할) cluster 전체 정지. [[distributed-systems-consensus]] 참고.&lt;/p&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CAP는 정상 시 trade-off 안 다룸&lt;/strong&gt;: PACELC가 더 현실적&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P는 항상&lt;/strong&gt;: CA 시스템은 single-node에서만&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;eventual consistency도 정확한 정의&lt;/strong&gt;: 결국 수렴, 시간 보장 X&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tunable consistency 활용&lt;/strong&gt;: Cassandra의 QUORUM 패턴&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;금융은 CP, SNS는 AP&lt;/strong&gt;: 도메인별 명확&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단순화하지 말 것&lt;/strong&gt;: 모든 read가 strong 필요 X, 모든 write가 eventual OK X&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;monitoring&lt;/strong&gt;: stale read 비율, partition 빈도 추적&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[distributed-systems-consensus]]&lt;/li&gt;
&lt;li&gt;[[distributed-systems-distributed-transaction]]&lt;/li&gt;
&lt;li&gt;[[kafka]] (AP 시스템)&lt;/li&gt;
&lt;li&gt;[[idempotency-keys]] (AP 환경에서 중복 방지)&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]] (eventual consistency 패턴)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>distributed-systems</category><category>cap</category><category>consistency</category><category>availability</category><category>partition</category><category>pacelc</category><author>koa (김신건)</author></item><item><title>[Distributed] Kafka Consumer Group: rebalancing, offset, lag</title><link>https://shinkeonkim.com/wiki/distributed-systems/kafka-consumer-group/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/kafka-consumer-group/</guid><pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Consumer Group&lt;/strong&gt; = 같은 &lt;code&gt;group.id&lt;/code&gt;를 가진 consumer들이 &lt;em&gt;함께 한 topic을 분담 소비&lt;/em&gt;. &lt;em&gt;partition 단위로 분배&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;핵심 특성:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;한 partition → 한 consumer&lt;/em&gt; (group 안에서)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;여러 Consumer Group&lt;/em&gt;은 같은 topic을 &lt;em&gt;독립적으로&lt;/em&gt; 소비 가능&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Group Coordinator&lt;/em&gt; (Kafka broker)가 멤버십과 partition 할당 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;partition-할당&quot;&gt;Partition 할당&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph T[&quot;Topic: orders (6 partitions)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        P0[p0] &amp;#x26; P1[p1] &amp;#x26; P2[p2] &amp;#x26; P3[p3] &amp;#x26; P4[p4] &amp;#x26; P5[p5]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph CG[&quot;Consumer Group&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C1[&quot;Consumer 1&quot;] --&gt; P0 &amp;#x26; P1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C2[&quot;Consumer 2&quot;] --&gt; P2 &amp;#x26; P3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C3[&quot;Consumer 3&quot;] --&gt; P4 &amp;#x26; P5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;규칙:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;partition 수 &gt;= consumer 수&lt;/em&gt;면 모두 분배&lt;/li&gt;
&lt;li&gt;&lt;em&gt;partition 수 &amp;#x3C; consumer 수&lt;/em&gt;면 &lt;em&gt;일부 consumer idle&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;한 partition → 한 consumer&lt;/em&gt; (group 안)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;group-coordinator&quot;&gt;Group Coordinator&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Group Coordinator&lt;/strong&gt; = consumer group의 멤버십과 rebalancing을 관리하는 Kafka broker.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C as &quot;Consumer&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant GC as &quot;Group Coordinator (Broker)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant ZK as &quot;__consumer_offsets topic&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;GC: FindCoordinator (group.id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GC--&gt;&gt;C: Coordinator broker 주소&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;GC: JoinGroup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GC-&gt;&gt;GC: 모든 멤버 대기 (sync barrier)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GC--&gt;&gt;C: JoinGroup Response (leader 선출)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over C: Group Leader가 partition 할당 계산&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;GC: SyncGroup (할당 결과)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GC--&gt;&gt;C: SyncGroup Response (내 할당)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;ZK: offset commit&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__consumer_offsets&lt;/code&gt; 내부 topic에 offset 저장&lt;/li&gt;
&lt;li&gt;Group Leader (consumer 중 하나)가 실제 partition 할당 계산&lt;/li&gt;
&lt;li&gt;Coordinator는 할당 결과를 모든 멤버에게 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;rebalancing&quot;&gt;Rebalancing&lt;/h2&gt;
&lt;p&gt;consumer 추가 / 제거 / 죽음 → &lt;em&gt;partition 재할당&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C1 as &quot;Consumer 1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant C2 as &quot;Consumer 2 (new)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Coord as &quot;Group Coordinator&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C1-&gt;&gt;Coord: heartbeat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C2-&gt;&gt;Coord: join group&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over Coord: rebalance 시작&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord-&gt;&gt;C1: stop fetching (revoke)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C1-&gt;&gt;Coord: ack + commit offset&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord-&gt;&gt;C1: &quot;new assignment (p0, p1, p2)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord-&gt;&gt;C2: &quot;new assignment (p3, p4, p5)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C1-&gt;&gt;C1: resume fetching&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C2-&gt;&gt;C2: start fetching&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;eager-vs-cooperative&quot;&gt;Eager vs Cooperative&lt;/h3&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Eager (옛 기본)&lt;/th&gt;&lt;th&gt;Cooperative (Kafka 2.4+)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Rebalance 시&lt;/td&gt;&lt;td&gt;&lt;em&gt;모든 consumer 정지&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;영향받는 partition만&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;처리 중단&lt;/td&gt;&lt;td&gt;길음 (수십 초 가능)&lt;/td&gt;&lt;td&gt;&lt;em&gt;짧음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Throughput&lt;/td&gt;&lt;td&gt;하락 큼&lt;/td&gt;&lt;td&gt;적음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;구현&lt;/td&gt;&lt;td&gt;RangeAssignor&lt;/td&gt;&lt;td&gt;CooperativeStickyAssignor&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;Cooperative rebalancing&lt;/em&gt;이 &lt;em&gt;2026 권장 기본&lt;/em&gt;. &lt;code&gt;partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;cooperative-sticky-assignor-설정&quot;&gt;Cooperative Sticky Assignor 설정&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;properties&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# consumer.properties&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;group.id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=my-consumer-group&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;partition.assignment.strategy&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=org.apache.kafka.clients.consumer.CooperativeStickyAssignor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 또는 여러 전략 조합 (마이그레이션 시)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;partition.assignment.strategy&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  org.apache.kafka.clients.consumer.CooperativeStickyAssignor,\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  org.apache.kafka.clients.consumer.RangeAssignor&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Java 코드&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;Properties props &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Properties&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;props.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;put&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ConsumerConfig.GROUP_ID_CONFIG, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;my-group&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;props.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;put&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CooperativeStickyAssignor.class.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getName&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;KafkaConsumer&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; consumer &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; KafkaConsumer&amp;#x3C;&gt;(props);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;consumer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;subscribe&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(List.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;of&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;orders&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;payments&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;static-membership-kip-345&quot;&gt;Static Membership (KIP-345)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;properties&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;group.instance.id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=consumer-1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;consumer가 &lt;em&gt;동일 ID로 재접속&lt;/em&gt;하면 &lt;em&gt;partition 유지&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;배포 / 재시작&lt;/em&gt; 시 rebalance 회피&lt;/li&gt;
&lt;li&gt;보통 &lt;em&gt;수십 초 ~ 수 분 안&lt;/em&gt;의 짧은 재시작 가정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C1[&quot;Consumer 1\ngroup.instance.id=c1&quot;] --&gt;|&quot;재시작&quot;| C1b[&quot;Consumer 1 (재시작)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C1b --&gt;|&quot;동일 ID로 재접속&quot;| GC[&quot;Group Coordinator&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    GC --&gt;|&quot;partition 유지 (rebalance 없음)&quot;| P[&quot;p0, p1 유지&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;offset-관리&quot;&gt;Offset 관리&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C[Consumer] --&gt;|fetch| K[(Kafka)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|commit| Offset[&quot;__consumer_offsets\n(internal topic)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note1[&quot;정기 commit 또는 명시적&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;모드&lt;/th&gt;&lt;th&gt;안전성&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;enable.auto.commit=true&lt;/code&gt; (기본)&lt;/td&gt;&lt;td&gt;&lt;em&gt;중복 또는 손실&lt;/em&gt; 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;명시적 sync commit&lt;/td&gt;&lt;td&gt;안전&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;명시적 async commit + 마지막 sync&lt;/td&gt;&lt;td&gt;balanced&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;auto-commit-함정&quot;&gt;Auto commit 함정&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. fetch [100, 101, 102]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 처리 중 ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. 5초 후 auto commit (103까지 commit)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;4. consumer crash (102 처리 미완)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;5. 재시작 → 103부터 fetch → 102 손실!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;명시적-commit-패턴&quot;&gt;명시적 commit 패턴&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ConsumerRecords&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; records &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; consumer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;poll&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(Duration.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ofMillis&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (ConsumerRecord&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; record &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; records) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        process&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(record);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // 배치 처리 완료 후 명시적 commit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    consumer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;commitSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;strong&gt;Auto commit은 손실 또는 중복&lt;/strong&gt;의 함정. &lt;em&gt;exactly-once&lt;/em&gt;가 필요하면 명시적 commit + idempotent 처리.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;exactly-once-처리-패턴&quot;&gt;Exactly-once 처리 패턴&lt;/h2&gt;
&lt;p&gt;Kafka 자체의 EOS (Exactly-Once Semantics)는 &lt;em&gt;Kafka → Kafka&lt;/em&gt; 구간만 보장. 외부 DB까지 포함하려면 추가 패턴 필요.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K[(Kafka)] --&gt;|&quot;fetch&quot;| C[Consumer]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|&quot;처리&quot;| DB[(Database)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|&quot;offset commit&quot;| K&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DB --&gt;|&quot;idempotency key 체크&quot;| DB&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Transactional producer (Kafka → Kafka exactly-once)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;producer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;initTransactions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    producer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;beginTransaction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    producer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;send&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ProducerRecord&amp;#x3C;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;output-topic&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, key, value));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // offset commit을 트랜잭션에 포함&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    producer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;sendOffsetsToTransaction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(offsets, consumerGroupMetadata);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    producer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;commitTransaction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (Exception &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;e&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    producer.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;abortTransaction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// DB까지 exactly-once: idempotency key 패턴&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Transactional&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; processRecord&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ConsumerRecord&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;String, Order&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; record) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    String idempotencyKey &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; record.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;topic&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;-&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; record.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;partition&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;-&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; record.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;offset&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (processedOffsets.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;existsByKey&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(idempotencyKey)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 이미 처리됨&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    orderService.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(record.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    processedOffsets.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(idempotencyKey);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // DB 트랜잭션 커밋 후 offset commit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;consumer-lag&quot;&gt;Consumer Lag&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Last[&quot;partition의 last offset (LEO): 1000&quot;] --&gt; Lag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Curr[&quot;consumer current offset: 800&quot;] --&gt; Lag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Lag[&quot;Lag = 200\nprocessing 못 따라옴&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;도구&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;kafka-consumer-groups.sh --describe&lt;/code&gt;&lt;/td&gt;&lt;td&gt;즉시 확인&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Kafka Lag Exporter&lt;/td&gt;&lt;td&gt;Prometheus 메트릭&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Burrow (LinkedIn)&lt;/td&gt;&lt;td&gt;더 세련된 lag 분석&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# lag 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kafka-consumer-groups.sh&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  --bootstrap-server&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; localhost:9092&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  --group&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; my-consumer-group&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  --describe&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 출력 예시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GROUP&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;           TOPIC&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;     PARTITION&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  CURRENT-OFFSET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  LOG-END-OFFSET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  LAG&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;my-group&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        orders&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    0&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;          800&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;             1000&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;            200&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;my-group&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        orders&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    1&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;          950&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;             1000&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;            50&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;lag-기반-auto-scaling&quot;&gt;Lag 기반 Auto-Scaling&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Lag[&quot;Consumer Lag&quot;] --&gt;|KEDA| HPA[&quot;Kubernetes HPA&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    HPA --&gt; Scale[&quot;Consumer pod 수 증가&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://keda.sh/&quot;&gt;KEDA&lt;/a&gt;가 Kafka lag을 &lt;em&gt;Kubernetes HPA 지표&lt;/em&gt;로 사용. &lt;em&gt;spike 자동 흡수&lt;/em&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# KEDA ScaledObject&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;apiVersion&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;keda.sh/v1alpha1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;ScaledObject&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;metadata&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;kafka-consumer-scaler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;spec&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  scaleTargetRef&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;order-consumer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  minReplicaCount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  maxReplicaCount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   # partition 수 이하로&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  triggers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;kafka&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      metadata&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        bootstrapServers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;kafka:9092&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        consumerGroup&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;my-consumer-group&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        topic&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        lagThreshold&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;100&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   # lag 100 이상이면 scale out&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;heartbeat--session&quot;&gt;Heartbeat / Session&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;설정&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;th&gt;기본&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.timeout.ms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;이 시간 heartbeat 없으면 &lt;em&gt;죽었다고 판단&lt;/em&gt;&lt;/td&gt;&lt;td&gt;45s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;heartbeat.interval.ms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;heartbeat 보내는 주기&lt;/td&gt;&lt;td&gt;3s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;max.poll.interval.ms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;poll 사이 최대 시간&lt;/td&gt;&lt;td&gt;5min&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;max.poll.interval.ms&lt;/code&gt; 초과 = consumer &lt;em&gt;추방&lt;/em&gt;. 처리 시간이 길면 &lt;em&gt;명시적 늘림&lt;/em&gt; 또는 &lt;em&gt;워커 별도 스레드&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;consumer-group-성능-튜닝&quot;&gt;Consumer Group 성능 튜닝&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;설정&lt;/th&gt;&lt;th&gt;기본&lt;/th&gt;&lt;th&gt;권장 (고처리량)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;fetch.min.bytes&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1024 (배치 효율)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;fetch.max.wait.ms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;500ms&lt;/td&gt;&lt;td&gt;500ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;max.poll.records&lt;/code&gt;&lt;/td&gt;&lt;td&gt;500&lt;/td&gt;&lt;td&gt;100~500 (처리 시간에 따라)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;max.partition.fetch.bytes&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1MB&lt;/td&gt;&lt;td&gt;1MB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;enable.auto.commit&lt;/code&gt;&lt;/td&gt;&lt;td&gt;true&lt;/td&gt;&lt;td&gt;false (명시적 commit)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;properties&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 고처리량 consumer 설정&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;fetch.min.bytes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=1024&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;fetch.max.wait.ms&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=500&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;max.poll.records&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=200&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;enable.auto.commit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;auto.offset.reset&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=earliest&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rebalancing 폭주&lt;/strong&gt; = consumer 자주 추가/제거 → 처리 정지 반복. cooperative + static membership.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;auto.offset.reset=latest&lt;/code&gt; + 다운&lt;/strong&gt; = 다운 사이 메시지 &lt;em&gt;손실&lt;/em&gt;. 보통 &lt;code&gt;earliest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;너무 큰 batch&lt;/strong&gt; = &lt;code&gt;max.poll.records&lt;/code&gt; 너무 큼 → 처리 시간 길어 추방.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상태 있는 consumer + rebalance&lt;/strong&gt; = partition이 다른 consumer로 가면서 &lt;em&gt;처리 진행 정보 손실&lt;/em&gt;. 상태는 &lt;em&gt;외부 store&lt;/em&gt;에.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;partition 수 &amp;#x3C; consumer 수&lt;/strong&gt; = 초과 consumer는 idle. partition 수를 consumer 최대 수 이상으로 설정.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[kafka]]&lt;/li&gt;
&lt;li&gt;[[Redis Pub Sub vs Streams]] (consumer group 유사)&lt;/li&gt;
&lt;li&gt;[[message-broker-comparison]]&lt;/li&gt;
&lt;li&gt;[[idempotency-keys]]&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]] (exactly-once 패턴)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>kafka</category><category>consumer-group</category><category>rebalancing</category><category>backend</category><category>distributed</category><author>koa (김신건)</author></item><item><title>[Distributed] Message Broker 비교: Kafka / RabbitMQ / NATS / SQS / Redis Streams</title><link>https://shinkeonkim.com/wiki/distributed-systems/message-broker-comparison/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/message-broker-comparison/</guid><pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Message Broker&lt;/strong&gt; = 생산자(Producer)와 소비자(Consumer) 사이에서 메시지를 중계하는 미들웨어. 비동기 통신, 부하 분산, 시스템 디커플링의 핵심.&lt;/p&gt;
&lt;h2 id=&quot;결정-트리&quot;&gt;결정 트리&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{&quot;사용 패턴&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;event log + replay&quot;| Kafka[Kafka]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;복잡 라우팅 + task queue&quot;| Rabbit[RabbitMQ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;마이크로서비스 가벼움&quot;| NATS[NATS]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;AWS managed + 단순 큐&quot;| SQS[SQS]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;Redis 위 fan-out / 짧은 큐&quot;| RedisS[Redis Streams]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;레거시 JMS / 엔터프라이즈&quot;| AMQ[ActiveMQ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;클라우드 native pub-sub&quot;| GCP[&quot;Google Pub/Sub&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;6가지-broker-매트릭스&quot;&gt;6가지 broker 매트릭스&lt;/h2&gt;































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Kafka&lt;/th&gt;&lt;th&gt;RabbitMQ&lt;/th&gt;&lt;th&gt;NATS&lt;/th&gt;&lt;th&gt;SQS&lt;/th&gt;&lt;th&gt;Redis Streams&lt;/th&gt;&lt;th&gt;ActiveMQ&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;모델&lt;/td&gt;&lt;td&gt;log&lt;/td&gt;&lt;td&gt;exchange-queue&lt;/td&gt;&lt;td&gt;subject pub-sub&lt;/td&gt;&lt;td&gt;queue&lt;/td&gt;&lt;td&gt;log&lt;/td&gt;&lt;td&gt;queue/topic&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;영속&lt;/td&gt;&lt;td&gt;&lt;em&gt;항상&lt;/em&gt;&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;td&gt;옵션 (JS)&lt;/td&gt;&lt;td&gt;항상&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;td&gt;항상&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Throughput&lt;/td&gt;&lt;td&gt;&lt;em&gt;수백만/s&lt;/em&gt;&lt;/td&gt;&lt;td&gt;수만/s&lt;/td&gt;&lt;td&gt;수백만/s&lt;/td&gt;&lt;td&gt;거의 무한&lt;/td&gt;&lt;td&gt;수십만/s&lt;/td&gt;&lt;td&gt;수만/s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Latency&lt;/td&gt;&lt;td&gt;중간 (ms)&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;&lt;em&gt;마이크로초&lt;/em&gt;&lt;/td&gt;&lt;td&gt;수십 ms&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;운영 복잡도&lt;/td&gt;&lt;td&gt;&lt;em&gt;높음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;td&gt;&lt;em&gt;낮음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;없음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;라우팅&lt;/td&gt;&lt;td&gt;partition by key&lt;/td&gt;&lt;td&gt;exchange 패턴&lt;/td&gt;&lt;td&gt;subject 계층&lt;/td&gt;&lt;td&gt;FIFO/standard&lt;/td&gt;&lt;td&gt;consumer group&lt;/td&gt;&lt;td&gt;topic/queue&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Re-play&lt;/td&gt;&lt;td&gt;&lt;em&gt;자유&lt;/em&gt; (offset)&lt;/td&gt;&lt;td&gt;한 번만&lt;/td&gt;&lt;td&gt;JS 가능&lt;/td&gt;&lt;td&gt;DLQ만&lt;/td&gt;&lt;td&gt;가능 (XREAD)&lt;/td&gt;&lt;td&gt;제한적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;학습 곡선&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;프로토콜&lt;/td&gt;&lt;td&gt;자체&lt;/td&gt;&lt;td&gt;AMQP 0-9-1&lt;/td&gt;&lt;td&gt;NATS&lt;/td&gt;&lt;td&gt;HTTP/SQS&lt;/td&gt;&lt;td&gt;RESP&lt;/td&gt;&lt;td&gt;AMQP/OpenWire&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;브로커별-아키텍처&quot;&gt;브로커별 아키텍처&lt;/h2&gt;
&lt;h3 id=&quot;kafka-log-모델&quot;&gt;Kafka: Log 모델&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P1[Producer 1] --&gt; T[&quot;Topic: orders\n(6 partitions)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P2[Producer 2] --&gt; T&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt; P0[&quot;Partition 0\noffset 0,1,2...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt; P1b[&quot;Partition 1\noffset 0,1,2...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt; P2b[&quot;Partition 2\noffset 0,1,2...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P0 --&gt; CG1[&quot;Consumer Group A\nConsumer 1&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P1b --&gt; CG1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P2b --&gt; CG2[&quot;Consumer Group B\nConsumer 1&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P0 --&gt; CG2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;메시지는 &lt;em&gt;삭제되지 않음&lt;/em&gt; (retention 기간 동안)&lt;/li&gt;
&lt;li&gt;여러 Consumer Group이 &lt;em&gt;독립적으로&lt;/em&gt; 같은 topic 소비&lt;/li&gt;
&lt;li&gt;offset으로 &lt;em&gt;임의 시점 replay&lt;/em&gt; 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;rabbitmq-exchange-queue-모델&quot;&gt;RabbitMQ: Exchange-Queue 모델&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Producer] --&gt; EX[&quot;Exchange\n(direct/fanout/topic/headers)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    EX --&gt;|&quot;routing key: order.created&quot;| Q1[&quot;Queue: order-processor&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    EX --&gt;|&quot;routing key: order.*&quot;| Q2[&quot;Queue: audit-log&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    EX --&gt;|&quot;fanout&quot;| Q3[&quot;Queue: notification&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt; C1[Consumer 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt; C2[Consumer 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3 --&gt; C3[Consumer 3]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Exchange 타입: &lt;code&gt;direct&lt;/code&gt; (정확 매칭), &lt;code&gt;fanout&lt;/code&gt; (브로드캐스트), &lt;code&gt;topic&lt;/code&gt; (패턴), &lt;code&gt;headers&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;메시지는 &lt;em&gt;소비 후 삭제&lt;/em&gt; (기본)&lt;/li&gt;
&lt;li&gt;복잡한 라우팅 로직을 브로커가 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;처리량--지연--운영비용-직관&quot;&gt;처리량 / 지연 / 운영비용 (직관)&lt;/h2&gt;
&lt;!-- mdx-component: ChartJs --&gt;
&lt;h2 id=&quot;activemq-상세&quot;&gt;ActiveMQ 상세&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;ActiveMQ&lt;/strong&gt; (Apache)는 JMS(Java Message Service) 표준을 구현한 전통적인 엔터프라이즈 메시지 브로커.&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;ActiveMQ Classic&lt;/th&gt;&lt;th&gt;ActiveMQ Artemis&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;프로토콜&lt;/td&gt;&lt;td&gt;OpenWire, AMQP, STOMP, MQTT&lt;/td&gt;&lt;td&gt;AMQP, STOMP, MQTT, OpenWire&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;성능&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;td&gt;&lt;em&gt;Classic보다 높음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;아키텍처&lt;/td&gt;&lt;td&gt;단일 브로커&lt;/td&gt;&lt;td&gt;&lt;em&gt;고성능 비동기 코어&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;권장&lt;/td&gt;&lt;td&gt;레거시 유지&lt;/td&gt;&lt;td&gt;&lt;em&gt;신규 프로젝트&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;xml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;&amp;#x3C;!-- Spring Boot + ActiveMQ Artemis --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;dependency&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;groupId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;org.springframework.boot&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;groupId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;artifactId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;spring-boot-starter-artemis&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;artifactId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;dependency&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;spring&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  artemis&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    mode&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;native&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    host&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;localhost&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    port&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;61616&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    user&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;admin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    password&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;admin&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
ActiveMQ는 JMS 기반 레거시 시스템 통합에 적합. 신규 마이크로서비스 아키텍처에서는 Kafka (이벤트 스트리밍) 또는 NATS (경량 pub-sub)를 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;시나리오별-추천&quot;&gt;시나리오별 추천&lt;/h2&gt;




























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;시나리오&lt;/th&gt;&lt;th&gt;추천&lt;/th&gt;&lt;th&gt;이유&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Event sourcing (큰 로그)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;retention + replay&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;마이크로서비스 commands&lt;/td&gt;&lt;td&gt;&lt;strong&gt;NATS&lt;/strong&gt; 또는 RabbitMQ&lt;/td&gt;&lt;td&gt;낮은 지연, 라우팅&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AWS Lambda + 큐&lt;/td&gt;&lt;td&gt;&lt;strong&gt;SQS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;managed, Lambda trigger&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sidekiq 같은 Ruby job queue&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt; (list 기반)&lt;/td&gt;&lt;td&gt;단순, 빠름&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;실시간 채팅 fan-out&lt;/td&gt;&lt;td&gt;&lt;strong&gt;NATS&lt;/strong&gt; 또는 Redis Pub/Sub&lt;/td&gt;&lt;td&gt;마이크로초 지연&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CDC (DB → search)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt; + Debezium&lt;/td&gt;&lt;td&gt;log 기반 변경 감지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;트래픽 spike 자동 흡수&lt;/td&gt;&lt;td&gt;&lt;strong&gt;SQS&lt;/strong&gt; (managed)&lt;/td&gt;&lt;td&gt;무한 확장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;빠른 MVP / startup&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Redis Streams&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;이미 Redis 쓰면 추가 비용 없음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;레거시 JMS 통합&lt;/td&gt;&lt;td&gt;&lt;strong&gt;ActiveMQ&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;JMS 표준 준수&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;복잡한 라우팅 규칙&lt;/td&gt;&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;exchange 패턴&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;메시지-보장-비교&quot;&gt;메시지 보장 비교&lt;/h2&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Broker&lt;/th&gt;&lt;th&gt;At-most-once&lt;/th&gt;&lt;th&gt;At-least-once&lt;/th&gt;&lt;th&gt;Exactly-once&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;td&gt;기본&lt;/td&gt;&lt;td&gt;EOS 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RabbitMQ&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;td&gt;persistent + ack&lt;/td&gt;&lt;td&gt;외부 idempotency 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NATS Core&lt;/td&gt;&lt;td&gt;기본&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NATS JS&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;기본&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SQS&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;기본&lt;/td&gt;&lt;td&gt;FIFO + 5분 dedup&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Redis Streams&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;XACK로&lt;/td&gt;&lt;td&gt;외부 idempotency&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ActiveMQ&lt;/td&gt;&lt;td&gt;옵션&lt;/td&gt;&lt;td&gt;persistent + ack&lt;/td&gt;&lt;td&gt;XA 트랜잭션&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;Exactly-once는 broker 내부&lt;/em&gt;에서만 의미 있음. *외부 시스템 (DB, API)*까지 &lt;em&gt;완벽한 exactly-once&lt;/em&gt;는 불가능. &lt;em&gt;idempotency + outbox&lt;/em&gt;가 현실적 정답. 자세한 건 [[outbox-pattern]], [[idempotency-keys]].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;영속--재처리-비교&quot;&gt;영속 / 재처리 비교&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Kafka[&quot;Kafka\ndays~weeks retention\n임의 offset replay&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Rabbit[&quot;RabbitMQ\nqueue 안의 메시지\n소비 = 사라짐&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Nats[&quot;NATS JS\nretention 정책\n임의 시점 replay&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sqs[&quot;SQS\n최대 14일\n한 번 소비 = 사라짐&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Stream[&quot;Redis Streams\nMAXLEN만큼\n임의 offset&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    AMQ[&quot;ActiveMQ\nKahaDB / journal\n재처리 제한적&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;마이그레이션-비용&quot;&gt;마이그레이션 비용&lt;/h2&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;출발&lt;/th&gt;&lt;th&gt;도착&lt;/th&gt;&lt;th&gt;비용&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Sidekiq (Redis)&lt;/td&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;큼 (consumer group, exactly-once 재설계)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RabbitMQ&lt;/td&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;중간 (라우팅 → topic 재설계)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;NATS&lt;/td&gt;&lt;td&gt;작음 (둘 다 log)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ActiveMQ&lt;/td&gt;&lt;td&gt;RabbitMQ&lt;/td&gt;&lt;td&gt;작음 (둘 다 AMQP)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ActiveMQ&lt;/td&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;큼 (JMS → Kafka API 전면 교체)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;자체 호스팅&lt;/td&gt;&lt;td&gt;Managed&lt;/td&gt;&lt;td&gt;작음 (운영 측면 큰 이득)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;운영자-시점-체크리스트&quot;&gt;운영자 시점 체크리스트&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;체크 항목                          관련 설정&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;----------------------------------------------&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;메시지 손실 허용?              acks, persistent, ack 정책&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;순서 보장 필요?                partition key, FIFO&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;재처리 가능?                   offset / position&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DLQ + parking lot             실패 메시지 격리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Backpressure                  prefetch, max-in-flight&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Monitoring                    lag, dead messages, throughput&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Consumer auto-scaling         lag 기반 (KEDA)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Schema registry               Avro/Protobuf 스키마 진화&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dead-letter-queue-dlq-패턴&quot;&gt;Dead Letter Queue (DLQ) 패턴&lt;/h2&gt;
&lt;p&gt;처리 실패 메시지를 격리해 재처리하거나 분석하는 패턴.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Producer] --&gt; Q[&quot;Main Queue / Topic&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; C[Consumer]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|&quot;처리 실패 (N회)&quot;| DLQ[&quot;Dead Letter Queue&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DLQ --&gt; Alert[&quot;알림 / 모니터링&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DLQ --&gt; Replay[&quot;수동 재처리&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Broker&lt;/th&gt;&lt;th&gt;DLQ 지원&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;별도 topic 수동 구현 또는 Kafka Streams&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;RabbitMQ&lt;/td&gt;&lt;td&gt;&lt;code&gt;x-dead-letter-exchange&lt;/code&gt; 설정&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SQS&lt;/td&gt;&lt;td&gt;자동 DLQ 연결 (maxReceiveCount 설정)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;NATS JS&lt;/td&gt;&lt;td&gt;&lt;code&gt;MaxDeliver&lt;/code&gt; 초과 시 별도 subject&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# SQS DLQ 설정 예시 (AWS CDK)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;const dlq = new sqs.Queue(this, &apos;DLQ&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;const mainQueue = new sqs.Queue(this, &apos;MainQueue&apos;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  deadLetterQueue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    queue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;dlq&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    maxReceiveCount&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 3회 실패 후 DLQ로&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;schema-registry와-메시지-진화&quot;&gt;Schema Registry와 메시지 진화&lt;/h2&gt;
&lt;p&gt;Kafka 등에서 메시지 스키마를 중앙 관리:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Producer] --&gt;|&quot;Avro/Protobuf 직렬화&quot;| SR[&quot;Schema Registry&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SR --&gt;|&quot;schema_id 포함&quot;| K[(Kafka)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K --&gt; C[Consumer]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|&quot;schema_id로 역직렬화&quot;| SR&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Confluent Schema Registry&lt;/strong&gt;: Kafka 생태계 표준&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS Glue Schema Registry&lt;/strong&gt;: AWS 관리형&lt;/li&gt;
&lt;li&gt;스키마 진화 규칙: backward / forward / full compatibility&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Kafka를 단순 큐로 쓰기&lt;/strong&gt;: Kafka는 log 기반. 단순 task queue라면 RabbitMQ나 SQS가 더 적합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ에서 replay 기대&lt;/strong&gt;: 소비된 메시지는 사라짐. replay가 필요하면 Kafka.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NATS Core에서 영속성 기대&lt;/strong&gt;: NATS Core는 fire-and-forget. 영속성이 필요하면 NATS JetStream.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;partition 수 부족&lt;/strong&gt;: Kafka에서 consumer 수 &gt; partition 수면 일부 consumer idle. 처음부터 충분히 설정.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메시지 크기 과대&lt;/strong&gt;: Kafka 기본 최대 1MB. 큰 payload는 S3에 저장 후 reference만 전송.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DLQ 모니터링 없음&lt;/strong&gt;: DLQ에 메시지가 쌓여도 알림 없으면 데이터 손실 인지 불가.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[kafka]], [[rabbitmq]], [[nats]]&lt;/li&gt;
&lt;li&gt;[[Redis Pub Sub vs Streams]]&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]]&lt;/li&gt;
&lt;li&gt;[[idempotency-keys]]&lt;/li&gt;
&lt;li&gt;[[kafka-consumer-group]]&lt;/li&gt;
&lt;li&gt;[[aws-sqs]] (별도)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>message-broker</category><category>kafka</category><category>rabbitmq</category><category>nats</category><category>sqs</category><category>comparison</category><category>distributed</category><author>koa (김신건)</author></item><item><title>[Distributed] Kafka: 분산 로그, partition, consumer group</title><link>https://shinkeonkim.com/wiki/distributed-systems/kafka/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/kafka/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Apache Kafka&lt;/strong&gt; = &lt;em&gt;분산 commit log&lt;/em&gt;. &lt;em&gt;고처리량 (수백만 msg/s), 영속, 수평 확장&lt;/em&gt;. &lt;em&gt;event-driven 아키텍처&lt;/em&gt; 의 &lt;em&gt;de facto&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;핵심 개념: &lt;strong&gt;topic = partition 들 = 노드에 분산된 append-only log&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;토폴로지&quot;&gt;토폴로지&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P1[Producer 1] --&gt; T1[Topic: events]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P2[Producer 2] --&gt; T1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph T1[Topic: events]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      Part0[&quot;partition 0&amp;#x3C;br/&gt;(broker A)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      Part1[&quot;partition 1&amp;#x3C;br/&gt;(broker B)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      Part2[&quot;partition 2&amp;#x3C;br/&gt;(broker C)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T1 --&gt; CG1[Consumer Group: analytics]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T1 --&gt; CG2[Consumer Group: search]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CG1 --&gt; C11[Consumer 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CG1 --&gt; C12[Consumer 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CG1 --&gt; C13[Consumer 3]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CG2 --&gt; C21[Consumer 1]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;partition-핵심-단위&quot;&gt;Partition: 핵심 단위&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;속성&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Append-only&lt;/td&gt;&lt;td&gt;끝에만 추가&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Immutable&lt;/td&gt;&lt;td&gt;변경 불가&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ordered&lt;/td&gt;&lt;td&gt;partition 안에서 순서 보장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Distributed&lt;/td&gt;&lt;td&gt;노드 간 분산&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Replicated&lt;/td&gt;&lt;td&gt;RF (Replication Factor) 만큼 복제&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Offset&lt;/td&gt;&lt;td&gt;각 메시지의 &lt;em&gt;partition 내 ID&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;partition-수-선택&quot;&gt;Partition 수 선택&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{Partition 수}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;적음 (1-3)&quot;| Low[병렬성 낮음&amp;#x3C;br/&gt;순서 보장 쉬움]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;중간 (10-50)&quot;| Med[일반적]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;많음 (100+)&quot;| High[오버헤드 증가&amp;#x3C;br/&gt;리밸런싱 비용]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;Partition 수 = 한 consumer group 의 최대 병렬성&lt;/strong&gt;. 늘리는 건 가능하지만 &lt;em&gt;줄이는 건 불가&lt;/em&gt;. 보수적으로 시작.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;consumer-group&quot;&gt;Consumer Group&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph CG1[Consumer Group A]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C1[Consumer 1] --&gt; P0[part 0]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C2[Consumer 2] --&gt; P1[part 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C3[Consumer 3] --&gt; P2[part 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph CG2[Consumer Group B]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D1[Consumer 1] --&gt; P0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D1 --&gt; P1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D1 --&gt; P2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;규칙&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;한 partition → 한 consumer (group 안)&lt;/td&gt;&lt;td&gt;순서 보장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;한 consumer → N partitions&lt;/td&gt;&lt;td&gt;OK&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Consumer 수 &gt; Partition 수&lt;/td&gt;&lt;td&gt;남는 consumer는 idle&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;다른 group → 독립 offset&lt;/td&gt;&lt;td&gt;fan-out&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;offset-관리&quot;&gt;Offset 관리&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;K: poll() (offset 100 ~)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K--&gt;&gt;C: msgs [100, 101, 102]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;C: 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C-&gt;&gt;K: commit offset 103&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over C,K: 다음 poll 부터 103 부터&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;모드&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Auto commit&lt;/td&gt;&lt;td&gt;주기적 자동. &lt;em&gt;최대 한 번 손실 또는 중복&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Manual sync commit&lt;/td&gt;&lt;td&gt;처리 후 동기 commit. 정확&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Manual async commit&lt;/td&gt;&lt;td&gt;비동기 commit. &lt;em&gt;최후 commit 실패 가능&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;replication--isr&quot;&gt;Replication + ISR&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Leader[&quot;Leader replica&quot;] --&gt; F1[&quot;Follower 1&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Leader --&gt; F2[&quot;Follower 2&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Leader --&gt; ISR{&quot;ISR&amp;#x3C;br/&gt;In-Sync Replica set&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    F1 --&gt; ISR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    F2 -.지연.-&gt; ISR&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Leader 가 모든 write&lt;/em&gt; 수신.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Follower 가 동기 (또는 비동기) 복제&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;ISR&lt;/em&gt; = &lt;em&gt;충분히 따라온 replica 집합&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Leader 다운 → &lt;em&gt;ISR 중 새 leader&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;설정&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;acks=0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;응답 안 기다림 (손실 가능)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;acks=1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;leader 만 (기본)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;acks=all&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ISR 전체 (가장 안전)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;min.insync.replicas&lt;/code&gt;&lt;/td&gt;&lt;td&gt;최소 ISR 수. 미달 시 write 거절&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas=2&lt;/code&gt; 가 &lt;em&gt;프로덕션 표준&lt;/em&gt;. &lt;em&gt;낮은 settings&lt;/em&gt; 는 운영 사고 단골.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;exactly-once-semantics-eos&quot;&gt;Exactly-once Semantics (EOS)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Producer&amp;#x3C;br/&gt;idempotent + transactional] --&gt; K[Kafka]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K --&gt; C[Consumer&amp;#x3C;br/&gt;read_committed]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt;|transactional.id| TC[Transaction Coordinator]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|isolation.level=read_committed| K&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Producer idempotence&lt;/em&gt;: 재전송 시 &lt;em&gt;중복 제거&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Transactional producer&lt;/em&gt;: 여러 topic write 를 &lt;em&gt;원자적&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Consumer isolation.level=read_committed&lt;/em&gt;: aborted 트랜잭션 무시.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;strong&gt;완벽한 EOS 는 Kafka 안에서만&lt;/strong&gt;. &lt;em&gt;Kafka → DB → Kafka&lt;/em&gt; 등 &lt;em&gt;외부 시스템 포함&lt;/em&gt; 은 &lt;em&gt;outbox + idempotency&lt;/em&gt; 패턴이 추가로 필요. 자세한 건 [[outbox-pattern]].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;kraft-kafka-raft-zookeeper-제거&quot;&gt;KRaft (Kafka Raft, ZooKeeper 제거)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Old[옛 구조]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        K1[Kafka 브로커] --&gt; Zk[(ZooKeeper)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph New[KRaft 구조]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        K2[Kafka 브로커] -.-&gt; Quorum[메타데이터 controller&amp;#x3C;br/&gt;Raft 합의]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;2.8+ preview, 3.5+ production, 4.0+ ZK 완전 제거.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;운영 단순화&lt;/em&gt; + &lt;em&gt;확장성 (수백만 partition)&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;주요-producer-설정&quot;&gt;주요 Producer 설정&lt;/h2&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;설정&lt;/th&gt;&lt;th&gt;기본값&lt;/th&gt;&lt;th&gt;권장 프로덕션&lt;/th&gt;&lt;th&gt;효과&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;acks&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;&lt;td&gt;데이터 유실 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;batch.size&lt;/code&gt;&lt;/td&gt;&lt;td&gt;16KB&lt;/td&gt;&lt;td&gt;64KB ~ 1MB&lt;/td&gt;&lt;td&gt;처리량 향상&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;linger.ms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;5&lt;/code&gt; ~ &lt;code&gt;50&lt;/code&gt;&lt;/td&gt;&lt;td&gt;배치 유리 (latency 조금 증가)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;compression.type&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;none&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;snappy&lt;/code&gt; / &lt;code&gt;lz4&lt;/code&gt;&lt;/td&gt;&lt;td&gt;네트워크 절약&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;enable.idempotence&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;중복 전송 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;retries&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;MAX_INT&lt;/code&gt;&lt;/td&gt;&lt;td&gt;재시도&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;max.in.flight.requests&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt; (순서 중요 시)&lt;/td&gt;&lt;td&gt;순서 보장&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;주요-consumer-설정&quot;&gt;주요 Consumer 설정&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;설정&lt;/th&gt;&lt;th&gt;기본값&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;auto.offset.reset&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;latest&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;earliest&lt;/code&gt; 권장 (재시작 시 손실 방지)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;enable.auto.commit&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;수동 commit 추천 (&lt;code&gt;false&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;max.poll.records&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;&lt;td&gt;한 번에 가져올 최대 수&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;fetch.min.bytes&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;배치 효율 위해 늘림&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;session.timeout.ms&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;45000&lt;/code&gt;&lt;/td&gt;&lt;td&gt;consumer 죽었다고 판단하는 시간&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;처리량-최적화-결정-트리&quot;&gt;처리량 최적화 결정 트리&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q[&quot;처리량 부족?&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; A1{&quot;Producer 측?&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A1 --&gt;|&quot;Yes&quot;| P1[&quot;batch.size 늘리기&amp;#x3C;br/&gt;linger.ms=5~50&amp;#x3C;br/&gt;compression 추가&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A1 --&gt;|&quot;No&quot;| A2{&quot;Consumer 측?&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A2 --&gt;|&quot;Yes&quot;| P2[&quot;partition 수 늘리기&amp;#x3C;br/&gt;consumer 수 늘리기&amp;#x3C;br/&gt;max.poll.records 조정&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A2 --&gt;|&quot;No&quot;| A3{&quot;Broker 측?&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A3 --&gt;|&quot;Yes&quot;| P3[&quot;브로커 추가&amp;#x3C;br/&gt;replication factor 검토&amp;#x3C;br/&gt;log.flush 설정&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;kafka-streams-개요&quot;&gt;Kafka Streams 개요&lt;/h2&gt;
&lt;p&gt;Kafka 위에서 &lt;em&gt;실시간 스트림 처리&lt;/em&gt;. 별도 클러스터 불필요.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;StreamsBuilder builder &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; StreamsBuilder&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;KStream&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; orders &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    builder.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stream&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;orders-topic&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 필터 + 변환&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((key, val) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; val.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;contains&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;status=PAID&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;mapValues&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(val &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; enrich&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(val))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;to&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;paid-orders-topic&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;KafkaStreams streams &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; KafkaStreams&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(builder.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;build&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(), config);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;streams.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;start&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;개념&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;KStream&lt;/td&gt;&lt;td&gt;무한 레코드 스트림&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;KTable&lt;/td&gt;&lt;td&gt;최신 값만 유지 (changelog)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GlobalKTable&lt;/td&gt;&lt;td&gt;전체 파티션 로컬 복사&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Topology&lt;/td&gt;&lt;td&gt;처리 그래프&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;State Store&lt;/td&gt;&lt;td&gt;로컬 RocksDB (조인/집계)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;모니터링-핵심-지표&quot;&gt;모니터링 핵심 지표&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Producer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        PR1[&quot;records-sent-rate&amp;#x3C;br/&gt;초당 전송 건수&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        PR2[&quot;record-error-rate&amp;#x3C;br/&gt;전송 실패율&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Consumer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        CR1[&quot;consumer-lag&amp;#x3C;br/&gt;파티션별 미처리 수&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        CR2[&quot;records-consumed-rate&amp;#x3C;br/&gt;초당 소비 건수&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Broker&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        BR1[&quot;UnderReplicatedPartitions&amp;#x3C;br/&gt;복제 따라가지 못한 수&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        BR2[&quot;ActiveControllerCount&amp;#x3C;br/&gt;1 이어야 함&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;consumer-lag&lt;/strong&gt; 이 지속 증가 = consumer 처리 속도 부족. 파티션/consumer 수 증설 또는 처리 로직 최적화.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;kafka-의-적합--부적합&quot;&gt;Kafka 의 적합 / 부적합&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;적합&lt;/th&gt;&lt;th&gt;부적합&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Event sourcing&lt;/td&gt;&lt;td&gt;작은 데이터 (RabbitMQ 가 단순)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;로그 수집&lt;/td&gt;&lt;td&gt;request-reply (가능하지만 어색)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Stream processing&lt;/td&gt;&lt;td&gt;1-N 즉시 fan-out (Pub/Sub 가 간단)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CDC (Change Data Capture)&lt;/td&gt;&lt;td&gt;짧은 retention task queue (Sidekiq 가 적합)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Partition 수 부족&lt;/strong&gt; = 병렬성 제한. 늘리는 건 가능하지만 &lt;em&gt;기존 키 분포 깨짐&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rebalancing 폭주&lt;/strong&gt; = consumer 가 자주 join/leave → &lt;em&gt;모든 consumer 정지 후 재할당&lt;/em&gt;. cooperative rebalancing (Kafka 2.4+) 으로 완화.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hot partition&lt;/strong&gt; = key 선택 잘못 → 한 partition 만 트래픽. key 디자인 재검토.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;auto.offset.reset=latest&lt;/code&gt;&lt;/strong&gt; = consumer 가 죽었다 살아나면 &lt;em&gt;그동안 메시지 손실&lt;/em&gt;. 보통 &lt;code&gt;earliest&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[rabbitmq]] (대안)&lt;/li&gt;
&lt;li&gt;[[nats]] (대안)&lt;/li&gt;
&lt;li&gt;[[Redis Pub Sub vs Streams]] (Streams 가 Kafka 비슷)&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]]&lt;/li&gt;
&lt;li&gt;[[event-sourcing]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>kafka</category><category>message-broker</category><category>streaming</category><category>distributed</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Pattern] CQRS: Command / Query 분리, read model</title><link>https://shinkeonkim.com/wiki/distributed-systems/cqrs/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/cqrs/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CQRS (Command Query Responsibility Segregation)&lt;/strong&gt; = &lt;em&gt;쓰기 모델&lt;/em&gt; 과 &lt;em&gt;읽기 모델&lt;/em&gt; 을 &lt;em&gt;분리&lt;/em&gt;. 같은 데이터를 &lt;em&gt;다른 형태&lt;/em&gt; 로 저장 / 조회.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
CQRS 는 &lt;em&gt;쓰기 / 읽기 모델 분리&lt;/em&gt; 라는 &lt;em&gt;추상 패턴&lt;/em&gt;. &lt;em&gt;반드시 다른 DB&lt;/em&gt; 일 필요는 없다. &lt;em&gt;대부분 시작 단계는 같은 DB + 다른 모델&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;동기&quot;&gt;동기&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Trad[전통 CRUD]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Trad --&gt; ProbW[복잡한 비즈니스 로직 + 정규화 DB]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Trad --&gt; ProbR[복잡한 join 으로 화면 그리기]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CQRS[CQRS]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CQRS --&gt; Write[Write: 정규화, 트랜잭션, 비즈니스 규칙]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CQRS --&gt; Read[Read: 화면 친화, denormalized, 빠른 조회]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;CRUD 의 &lt;em&gt;읽기 / 쓰기 사용 패턴이 매우 다른&lt;/em&gt; 워크로드 (대시보드, 추천, 검색) 에서 효과적.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;기본-구조&quot;&gt;기본 구조&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cmd[Command&amp;#x3C;br/&gt;CreateOrder, ConfirmOrder] --&gt; CH[Command Handler]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CH --&gt; WM[(Write Model&amp;#x3C;br/&gt;정규화, 트랜잭션)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    WM --&gt;|event| EH[Event Handler / Projection]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    EH --&gt; RM[(Read Model&amp;#x3C;br/&gt;denormalized, optimized)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Query[Query&amp;#x3C;br/&gt;GetOrderSummary] --&gt; QH[Query Handler]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    QH --&gt; RM&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Command Side&lt;/th&gt;&lt;th&gt;Query Side&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;입력&lt;/td&gt;&lt;td&gt;비즈니스 의도 (Command)&lt;/td&gt;&lt;td&gt;정보 요청 (Query)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;검증&lt;/td&gt;&lt;td&gt;비즈니스 규칙&lt;/td&gt;&lt;td&gt;권한만&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;결과&lt;/td&gt;&lt;td&gt;변경 사실 (성공/실패)&lt;/td&gt;&lt;td&gt;데이터&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;모델&lt;/td&gt;&lt;td&gt;정규화 (3NF)&lt;/td&gt;&lt;td&gt;denormalized&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DB&lt;/td&gt;&lt;td&gt;RDB 가 일반&lt;/td&gt;&lt;td&gt;다양 (RDB, ES, Redis)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;일관성&lt;/td&gt;&lt;td&gt;강&lt;/td&gt;&lt;td&gt;보통 &lt;em&gt;eventual&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;예시-전자상거래&quot;&gt;예시: 전자상거래&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- Write Model (정규화)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders (id, user_id, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, ...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_items (id, order_id, product_id, qty, price);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; products (id, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, ...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- Read Model (denormalized, 화면 친화)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_summary_view (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  order_id, user_name, total, item_count,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  status_label, created_at, items_json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;읽기 1 쿼리:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_summary_view &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ?;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;쓰기 - 정상 트랜잭션 + event 발행:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders ...;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_items ...;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;event&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;OrderCreated&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, ...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 &lt;em&gt;event handler&lt;/em&gt; 가 &lt;em&gt;order_summary_view&lt;/em&gt; 갱신.&lt;/p&gt;
&lt;h2 id=&quot;event-sourcing-과-자주-같이&quot;&gt;Event Sourcing 과 자주 같이&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cmd[Command] --&gt; Agg[Aggregate]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agg --&gt;|emit event| ES[(Event Store)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ES --&gt;|project| V1[(Read View 1)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ES --&gt;|project| V2[(Read View 2)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ES --&gt;|project| V3[(Read View 3)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Query --&gt; V1 &amp;#x26; V2 &amp;#x26; V3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 건 [[event-sourcing]].&lt;/p&gt;
&lt;h2 id=&quot;read-model-종류&quot;&gt;Read Model 종류&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Read Model&lt;/th&gt;&lt;th&gt;적합&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Materialized view (DB)&lt;/td&gt;&lt;td&gt;같은 RDB 안&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ElasticSearch / OpenSearch&lt;/td&gt;&lt;td&gt;검색 + aggregation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Redis&lt;/td&gt;&lt;td&gt;작은 hot data&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Cassandra&lt;/td&gt;&lt;td&gt;시계열 / 대용량&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GraphQL cache&lt;/td&gt;&lt;td&gt;API 응답 캐시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;클라이언트 IndexedDB&lt;/td&gt;&lt;td&gt;offline-first&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;eventual-consistency&quot;&gt;Eventual Consistency&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;API: POST /orders (command)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API-&gt;&gt;WriteDB: INSERT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    WriteDB--&gt;&gt;API: OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API--&gt;&gt;User: 200 OK (order_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over API,ReadDB: 비동기 projection 진행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;API: GET /orders/123 (query)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API-&gt;&gt;ReadDB: SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ReadDB--&gt;&gt;API: NOT FOUND? (projection lag)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;em&gt;방금 만든 주문이 안 보이는&lt;/em&gt; UX 함정. 해결책:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;짧은 lag&lt;/em&gt; (수 ms) 보장 + UI 가 &lt;em&gt;낙관적 표시&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;write 후 즉시 같은 transaction 으로 read&lt;/em&gt; 시 &lt;em&gt;write DB 우회&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;작성자 본인은 강 일관성, 다른 사람은 eventual&lt;/em&gt; (read-your-writes)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;언제-cqrs-안-쓸-때가-더-많다&quot;&gt;언제 CQRS? (안 쓸 때가 더 많다)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{비즈니스 도메인 복잡?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|아니오| Simple[CRUD 충분]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|예| Q2{읽기 / 쓰기 패턴 매우 다름?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|아니오| Simple&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|예| Q3{팀이 ES + 분산 시스템 이해?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3 --&gt;|아니오| Risk[학습 비용 큼 → 천천히]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q3 --&gt;|예| CQRS[CQRS 시작]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;대부분의 앱은 CQRS 가 &lt;em&gt;과한&lt;/em&gt; 솔루션&lt;/em&gt;. &lt;em&gt;진짜 도움 되는 곳&lt;/em&gt; (큰 e-commerce, 트레이딩 시스템, 큰 SaaS 대시보드) 가 아니면 &lt;em&gt;모듈 모놀리스 + 일부 read model&lt;/em&gt; 정도면 충분.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;단순-cqrs-가벼운-형태&quot;&gt;단순 CQRS (가벼운 형태)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cmd[Command Handler&amp;#x3C;br/&gt;POST /orders] --&gt; Write[(DB)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Write --&gt;|trigger / event| Mat[Materialized View&amp;#x3C;br/&gt;갱신]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Query[Query Handler&amp;#x3C;br/&gt;GET /orders/dashboard] --&gt; Mat&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;같은 DB.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;materialized view&lt;/em&gt; 또는 &lt;em&gt;summary table&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;별도 인프라 없이 &lt;em&gt;80% 의 효과&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;projection-구현-패턴&quot;&gt;Projection 구현 패턴&lt;/h2&gt;
&lt;p&gt;projection 은 write model 의 event 를 소비해 read model 을 갱신하는 컴포넌트다. 구현 방법은 크게 3 가지.&lt;/p&gt;
&lt;h3 id=&quot;1-synchronous-트랜잭션-내&quot;&gt;1. Synchronous (트랜잭션 내)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders (...) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_items (...) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  -- 같은 트랜잭션에서 read model 도 갱신&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_summary_view (order_id, user_name, total, ...)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; o&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;u&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SUM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;oi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;price&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; oi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;qty&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders o&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    JOIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; users u &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ON&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; o&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; u&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    JOIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_items oi &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ON&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; oi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;order_id&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; o&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    WHERE&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; o&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; NEW&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    GROUP BY&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; o&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;u&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;장점: &lt;em&gt;즉각적 일관성&lt;/em&gt;, read-your-writes 문제 없음.&lt;/li&gt;
&lt;li&gt;단점: write latency 증가, read model 이 여러 개면 트랜잭션 커짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-asynchronous-outbox--event-bus&quot;&gt;2. Asynchronous (outbox + event bus)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;orders 테이블 INSERT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → outbox 에 OrderCreated event INSERT (같은 트랜잭션)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → outbox relay 가 Kafka/NATS 에 발행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → projection consumer 가 구독 → read model 갱신&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;장점: write path 가 가볍다, read model 독립 배포 가능.&lt;/li&gt;
&lt;li&gt;단점: &lt;em&gt;projection lag&lt;/em&gt; 발생, idempotency 필수.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-catch-up-재구성&quot;&gt;3. Catch-up (재구성)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# event store 를 처음부터 재생 → read model 처음부터 재구성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;projection&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; rebuild&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --from-event-store&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --target&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; order_summary_view&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;event sourcing 을 쓸 때 강점.&lt;/li&gt;
&lt;li&gt;새 read model 추가, 버그 수정 후 재계산에 유용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;idempotent-projection&quot;&gt;Idempotent Projection&lt;/h2&gt;
&lt;p&gt;projection 은 반드시 idempotent 이어야 한다. 같은 event 가 두 번 들어와도 결과가 같아야 함.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- upsert 패턴&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order_summary_view (order_id, total, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, updated_at)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (:order_id, :total, :&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, :ts)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; CONFLICT (order_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;DO &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; SET&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  total      &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; EXCLUDED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;total&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  status&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;     =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; EXCLUDED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  updated_at &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; EXCLUDED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;updated_at&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; order_summary_view&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;updated_at&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; EXCLUDED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;updated_at&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 순서 보장&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;updated_at&lt;/code&gt; 비교로 &lt;em&gt;out-of-order event&lt;/em&gt; 도 안전하게 처리.&lt;/li&gt;
&lt;li&gt;자세히는 [[idempotency-keys]].&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;모든 곳에 CQRS&lt;/strong&gt; = 단순 CRUD 도 분리. 복잡도 폭증.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Projection 동기 실행&lt;/strong&gt; = read model 갱신이 &lt;em&gt;write 막음&lt;/em&gt;. async + idempotent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read-your-writes 무시&lt;/strong&gt; = 사용자가 &lt;em&gt;방금 한 일을 못 봄&lt;/em&gt; UX.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read model 재구성 어려움&lt;/strong&gt; = event log 없으면 &lt;em&gt;replay 불가&lt;/em&gt;. ES + CQRS 가 자주 묶이는 이유.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Projection 스키마 변경&lt;/strong&gt; = 기존 event 로 재구성 가능한지 확인 필요.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;테스트-전략&quot;&gt;테스트 전략&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;레이어&lt;/th&gt;&lt;th&gt;테스트 대상&lt;/th&gt;&lt;th&gt;도구&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Command Handler&lt;/td&gt;&lt;td&gt;비즈니스 규칙, 검증, event 발행&lt;/td&gt;&lt;td&gt;단위 테스트&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Projection&lt;/td&gt;&lt;td&gt;idempotency, 특정 event sequence 후 상태&lt;/td&gt;&lt;td&gt;단위 테스트&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Read Model&lt;/td&gt;&lt;td&gt;쿼리 결과, 인덱스&lt;/td&gt;&lt;td&gt;통합 테스트&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;End-to-End&lt;/td&gt;&lt;td&gt;eventual consistency 타이밍&lt;/td&gt;&lt;td&gt;통합/E2E&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;eventual consistency E2E: &lt;code&gt;awaitility&lt;/code&gt; (Java) 같은 polling assertion 사용.&lt;/p&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[event-sourcing]]&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]]&lt;/li&gt;
&lt;li&gt;[[saga-pattern]]&lt;/li&gt;
&lt;li&gt;[[microservices-vs-monolith]]&lt;/li&gt;
&lt;li&gt;[[idempotency-keys]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>cqrs</category><category>architecture</category><category>pattern</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Pattern] Event Sourcing: 상태 대신 이벤트 누적</title><link>https://shinkeonkim.com/wiki/distributed-systems/event-sourcing/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/event-sourcing/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Event Sourcing&lt;/strong&gt; = &lt;em&gt;현재 상태 대신 &lt;em&gt;과거 이벤트 시퀀스&lt;/em&gt; 를 저장&lt;/em&gt;. 현재 상태 = &lt;em&gt;event 모두 replay 한 결과&lt;/em&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;전통: state = { balance: 100 }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ES:   events = [Deposited(50), Deposited(60), Withdrawn(10)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      state = events.reduce(apply) = { balance: 100 }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-약속&quot;&gt;핵심 약속&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;과거 모든 변화 기록&lt;/em&gt;. 감사 / 디버깅 / 시간 여행 자유.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;읽기 모델 자유&lt;/em&gt; (어떻게든 projection).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;replay 로 새 view 생성&lt;/em&gt; (옛 데이터에서 새 통찰).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;비파괴&lt;/em&gt;. 옛 이벤트는 &lt;em&gt;불변&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;구조&quot;&gt;구조&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cmd[Command&amp;#x3C;br/&gt;Deposit 50] --&gt; Agg[Aggregate]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agg --&gt;|emit| Event[&quot;Deposited(50)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Event --&gt; Store[(Event Store&amp;#x3C;br/&gt;append-only)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt;|project| V1[(Balance View)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt;|project| V2[(Audit View)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt;|project| V3[(Search View)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;event-store&quot;&gt;Event Store&lt;/h2&gt;
&lt;p&gt;이벤트 = 불변 사실. 형식:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;event_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;evt_abc&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;aggregate_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;account_42&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;aggregate_type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Account&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;event_type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Deposited&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;version&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;payload&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;USD&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;metadata&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;correlation_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;occurred_at&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;2026-06-25T12:00:00Z&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Store&lt;/th&gt;&lt;th&gt;특징&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Kafka&lt;/td&gt;&lt;td&gt;log + retention&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;EventStoreDB&lt;/td&gt;&lt;td&gt;ES 전용&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PostgreSQL&lt;/td&gt;&lt;td&gt;append-only 테이블 + outbox&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DynamoDB&lt;/td&gt;&lt;td&gt;partition key = aggregate_id&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Axon&lt;/td&gt;&lt;td&gt;Java 프레임워크&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;replay-재계산&quot;&gt;Replay (재계산)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant App&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Store as Event Store&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;Store: load events for aggregate_id=42&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store--&gt;&gt;App: [e1, e2, e3, e4, e5]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;App: state = e1.apply(empty)&amp;#x3C;br/&gt;state = e2.apply(state)&amp;#x3C;br/&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over App: state 복원 완료&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;snapshot&quot;&gt;Snapshot&lt;/h2&gt;
&lt;p&gt;이벤트 수 만이면 &lt;em&gt;replay 가 느림&lt;/em&gt;. 주기적 &lt;em&gt;snapshot&lt;/em&gt; 저장:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store[(Event Store)] --&gt; Snap[(Snapshot Store)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note1[&quot;Snapshot at version 1000:&amp;#x3C;br/&gt;{ balance: 5000 }&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Load[Load aggregate] --&gt; Snap&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Snap --&gt;|version 1000 부터| Replay[event 1001 ~ 현재 replay]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; load_aggregate&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(id):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    snapshot &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; snapshot_store.latest(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    events &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; event_store.from_version(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, snapshot.version)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    state &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; snapshot.state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; e &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; events:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        state &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; e.apply(state)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; state&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;cqrs-와의-자연-결합&quot;&gt;CQRS 와의 자연 결합&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cmd --&gt; Write[Write side]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Write --&gt; Store[(Event Store)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt;|stream| P1[Projection 1] --&gt; View1[(Read View 1)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt;|stream| P2[Projection 2] --&gt; View2[(Read View 2)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt;|stream| P3[Projection 3] --&gt; View3[(Read View 3)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Query --&gt; View1 &amp;#x26; View2 &amp;#x26; View3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 건 [[cqrs]].&lt;/p&gt;
&lt;h2 id=&quot;새-view-추가&quot;&gt;새 View 추가&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;기존 데이터에서 새 인사이트&lt;/em&gt; 가 필요할 때:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Dev-&gt;&gt;Code: 새 projection 함수 추가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Code-&gt;&gt;Store: from version 0 부터 replay&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store--&gt;&gt;Code: 모든 이벤트&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Code-&gt;&gt;NewView: 새 view 채우기 (수 시간 ~ 일)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over Code,NewView: 이후 실시간 stream 으로 유지&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
이게 &lt;em&gt;ES 의 가장 큰 매력&lt;/em&gt;. &lt;em&gt;과거에 안 묻던 질문&lt;/em&gt; 을 &lt;em&gt;나중에 데이터로&lt;/em&gt; 만들 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;적합--부적합&quot;&gt;적합 / 부적합&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good[적합]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G1[&quot;감사 추적 필수 (금융, 의료)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G2[과거 시점 상태 조회]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G3[&quot;복잡 비즈니스 (주문 흐름)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Good --&gt; G4[CQRS 가 자연스러운 도메인]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad[부적합]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; B1[단순 CRUD]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; B2[&quot;GDPR 의 *잊혀질 권리* (이벤트 불변과 충돌)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Bad --&gt; B3[빠른 prototype]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;함정-이벤트-진화&quot;&gt;함정: 이벤트 진화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;v1: Deposited(amount: int)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;v2: Deposited(amount: int, currency: string)   ← currency 추가&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;옛 이벤트는 &lt;em&gt;currency 가 없다&lt;/em&gt;. 처리 시:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;전략&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Default value&lt;/td&gt;&lt;td&gt;옛 이벤트에 currency=“USD” 가정&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Up-cast&lt;/td&gt;&lt;td&gt;load 시 옛 → 새 형태로 변환&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Lazy migration&lt;/td&gt;&lt;td&gt;처리 시점에 보강&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Re-write event (안 권장)&lt;/td&gt;&lt;td&gt;&lt;em&gt;불변 원칙 위반&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;gdpr-의-잊혀질-권리-문제&quot;&gt;GDPR 의 &lt;em&gt;잊혀질 권리&lt;/em&gt; 문제&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q[&quot;GDPR: 사용자 데이터 삭제 요구&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Issue[&quot;ES: 이벤트는 불변 → 어떻게 지움?&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Issue --&gt; S1[&quot;1. Crypto-shredding: 개인정보 암호화, key 만 삭제&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Issue --&gt; S2[&quot;2. Event redaction: 특정 필드 null 처리&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Issue --&gt; S3[&quot;3. 별도 저장: 개인정보는 가변 DB, 이벤트는 reference&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;aggregate-설계-원칙&quot;&gt;Aggregate 설계 원칙&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;원칙&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;단일 트랜잭션 경계&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Aggregate 안에서만 불변식 보장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;작게 유지&lt;/em&gt;&lt;/td&gt;&lt;td&gt;한 개 Aggregate = 한 개 Entity + 직접 소유 ValueObject&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;다른 Aggregate 는 ID 참조&lt;/em&gt;&lt;/td&gt;&lt;td&gt;직접 참조 금지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;em&gt;Eventually consistent&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Aggregate 간 일관성은 eventual&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Order[&quot;Order Aggregate&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        O[Order] --&gt; L1[OrderLine]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        O --&gt; L2[OrderLine]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        O --&gt; Addr[ShippingAddress]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Customer[&quot;Customer Aggregate (별개)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C[Customer]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O -.-&gt;|&quot;customerId (ID 참조)&quot;| C&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;command-vs-event-구분&quot;&gt;Command vs Event 구분&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cmd[&quot;Command&amp;#x3C;br/&gt;(의도, 거절 가능)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Evt[&quot;Event&amp;#x3C;br/&gt;(사실, 불변)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agg[Aggregate]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cmd --&gt;|&quot;validate + apply&quot;| Agg&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agg --&gt;|&quot;emit&quot;| Evt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Evt --&gt;|&quot;append&quot;| Store[(Event Store)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;구분&lt;/th&gt;&lt;th&gt;Command&lt;/th&gt;&lt;th&gt;Event&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;형식&lt;/td&gt;&lt;td&gt;&lt;code&gt;PlaceOrder&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;OrderPlaced&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;시제&lt;/td&gt;&lt;td&gt;현재/미래&lt;/td&gt;&lt;td&gt;과거&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;거절 가능?&lt;/td&gt;&lt;td&gt;Yes (validation)&lt;/td&gt;&lt;td&gt;No (이미 발생)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;저장&lt;/td&gt;&lt;td&gt;저장 안 함 (보통)&lt;/td&gt;&lt;td&gt;영구 저장&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;projection-전략&quot;&gt;Projection 전략&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store[(Event Store)] --&gt;|&quot;event stream&quot;| P1[Projection Worker 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt;|&quot;event stream&quot;| P2[Projection Worker 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P1 --&gt;|&quot;upsert&quot;| V1[(&quot;잔액 View&quot;)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P2 --&gt;|&quot;upsert&quot;| V2[(&quot;거래 내역 View&quot;)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P1 --&gt;|&quot;checkpoint&quot;| CP1[(&quot;checkpoint 1&quot;)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P2 --&gt;|&quot;checkpoint&quot;| CP2[(&quot;checkpoint 2&quot;)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;checkpoint&lt;/em&gt;: 마지막으로 처리한 event version. 재시작 시 여기서 재개.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;eventual consistency&lt;/em&gt;: projection 이 약간 뒤처짐 허용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;이벤트-스키마-진화-전략&quot;&gt;이벤트 스키마 진화 전략&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;전략&lt;/th&gt;&lt;th&gt;설명&lt;/th&gt;&lt;th&gt;추천&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Default value&lt;/td&gt;&lt;td&gt;신규 필드에 기본값 삽입&lt;/td&gt;&lt;td&gt;단순 추가 시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Up-cast&lt;/td&gt;&lt;td&gt;로드 시 구버전 → 신버전 변환&lt;/td&gt;&lt;td&gt;필드 이름 변경&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Weak schema&lt;/td&gt;&lt;td&gt;JSON + 필드 검사 코드에서&lt;/td&gt;&lt;td&gt;스키마 불안정&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Re-write (비권장)&lt;/td&gt;&lt;td&gt;옛 이벤트 수정&lt;/td&gt;&lt;td&gt;불변 원칙 위반&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Up-cast 예시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; DepositedV1ToV2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; upcast&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, event: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;dict&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;dict&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; event[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;version&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            event[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;payload&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;currency&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;USD&apos;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   # 기본값 보강&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            event[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;version&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; event&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;테스트-전략&quot;&gt;테스트 전략&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Given-When-Then 패턴 (이벤트 기반)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_withdraw_sufficient_funds&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # Given: 과거 이벤트로 상태 세팅&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    account &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Account.from_events([&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Deposited(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;amount&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Deposited(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;amount&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # When: 커맨드 실행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    events &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; account.withdraw(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # Then: 발행 이벤트 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(events) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; isinstance&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(events[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], Withdrawn)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; events[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;].amount &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 50&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_withdraw_insufficient_funds&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    account &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Account.from_events([Deposited(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;amount&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pytest.raises(InsufficientFundsError):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        account.withdraw(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
ES 테스트의 핵심: &lt;em&gt;DB 없이 이벤트 리스트만으로 Aggregate 를 만들고 검증&lt;/em&gt;. 매우 빠른 단위 테스트 가능.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;모든 도메인에 ES&lt;/strong&gt; = 단순 CRUD 까지 &lt;em&gt;복잡 모델&lt;/em&gt;. 진짜 도메인 가치 있는 곳에만.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이벤트 schema &lt;em&gt;영구 보존&lt;/em&gt; 안 함&lt;/strong&gt; = 1년 뒤 옛 이벤트 &lt;em&gt;해석 불가&lt;/em&gt;. 스키마 진화 정책.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이벤트 크기 폭증&lt;/strong&gt; = 작은 이벤트 + 정기 snapshot. 큰 binary 는 reference.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aggregate 경계 잘못&lt;/strong&gt; = aggregate 가 &lt;em&gt;너무 크면&lt;/em&gt; concurrent write 충돌, 너무 작으면 일관성 사라짐.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[cqrs]]&lt;/li&gt;
&lt;li&gt;[[saga-pattern]]&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]]&lt;/li&gt;
&lt;li&gt;[[kafka]] (이벤트 저장)&lt;/li&gt;
&lt;li&gt;[[Redis Stream]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>event-sourcing</category><category>cqrs</category><category>architecture</category><category>pattern</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Pattern] Idempotency Keys: 중복 요청 안전 처리</title><link>https://shinkeonkim.com/wiki/distributed-systems/idempotency-keys/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/idempotency-keys/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Idempotency&lt;/strong&gt; = &lt;em&gt;같은 요청을 N번 보내도 결과가 1번과 동일&lt;/em&gt;. 분산 시스템 / 결제 / API 의 &lt;em&gt;안전망&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
네트워크는 &lt;em&gt;항상 timeout / retry&lt;/em&gt;. 진짜 &lt;em&gt;exactly-once&lt;/em&gt; 는 불가능 → &lt;em&gt;at-least-once + idempotent&lt;/em&gt; 가 &lt;em&gt;현실적 정답&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;http-의-idempotent-method&quot;&gt;HTTP 의 idempotent method&lt;/h2&gt;





































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Method&lt;/th&gt;&lt;th&gt;Idempotent&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;GET&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;HEAD&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;OPTIONS&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PUT&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DELETE&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;POST&lt;/td&gt;&lt;td&gt;&lt;em&gt;X (기본)&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PATCH&lt;/td&gt;&lt;td&gt;&lt;em&gt;X (대개)&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;POST 의 &lt;em&gt;비-멱등성&lt;/em&gt; 이 &lt;em&gt;결제 중복&lt;/em&gt; 같은 사고의 근원.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;idempotency-key-header&quot;&gt;Idempotency-Key Header&lt;/h2&gt;
&lt;p&gt;Stripe / Square / PayPal 등이 &lt;em&gt;표준화&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;POST&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /v1/charges &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;HTTP&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Authorization&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; Bearer sk_...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Idempotency-Key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; 7f4d8e2a-c-3a-4e-9f-1234567890ab&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Content-Type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; application/json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;5000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;usd&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;customer&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;cus_...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;서버 응답:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1번째 호출: 200 OK (실제 처리)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2번째 호출 (같은 key): 200 OK (옛 응답 그대로)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;서버-구현&quot;&gt;서버 구현&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant API&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Store as Idempotency Store&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant DB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Client-&gt;&gt;API: POST + Idempotency-Key: abc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API-&gt;&gt;Store: GET key abc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    alt 처음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Store--&gt;&gt;API: not found&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        API-&gt;&gt;Store: SET key abc IN_PROGRESS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        API-&gt;&gt;DB: 실제 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        DB--&gt;&gt;API: 결과&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        API-&gt;&gt;Store: SET key abc COMPLETED + 응답&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        API--&gt;&gt;Client: 응답&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    else 옛 응답 있음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Store--&gt;&gt;API: COMPLETED + 옛 응답&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        API--&gt;&gt;Client: 같은 옛 응답 (실제 처리 없음)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    else 진행 중&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Store--&gt;&gt;API: IN_PROGRESS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        API--&gt;&gt;Client: 409 Conflict (같은 key 중복)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; idempotent_post&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(key, request, handler):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    cached &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; store.get(key)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cached &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cached.status &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;completed&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cached.response&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cached &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cached.status &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;in_progress&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        raise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Conflict(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Same key in progress&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    store.set(key, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;in_progress&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ttl&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;24&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3600&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handler(request)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        store.set(key, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;completed&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;response&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ttl&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;24&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3600&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    except&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        store.delete(key)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 또는 &quot;failed&quot; 로 표시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        raise&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;store-선택&quot;&gt;Store 선택&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Store&lt;/th&gt;&lt;th&gt;적합&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Redis&lt;/td&gt;&lt;td&gt;빠름, TTL 자동&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PostgreSQL&lt;/td&gt;&lt;td&gt;트랜잭션 친화&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DynamoDB&lt;/td&gt;&lt;td&gt;분산 + TTL&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;em&gt;PostgreSQL 의 UNIQUE constraint&lt;/em&gt; + ON CONFLICT 가 &lt;em&gt;가장 단순&lt;/em&gt;. &lt;em&gt;결제 같이 강한 일관성&lt;/em&gt; 필요할 때.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;CREATE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; idempotency_keys&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TEXT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; PRIMARY KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  request_hash &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BYTEA&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; NOT NULL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  response JSONB,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  status&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TEXT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  expires_at &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TIMESTAMPTZ&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; idempotency_keys (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, request_hash, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (?, ?, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;in_progress&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; CONFLICT (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) DO NOTHING&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;RETURNING ...;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;request-hashing-재시도-안전&quot;&gt;Request Hashing (재시도 안전)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; request_hash&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(req):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sha256(req.method &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; req.url &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; req.body).hexdigest()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;같은 key + 다른 body = &lt;em&gt;수상&lt;/em&gt;. 거절 (400) 또는 &lt;em&gt;옛 응답&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;같은 key + 같은 body = &lt;em&gt;재시도&lt;/em&gt;. 옛 응답.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ttl-정책&quot;&gt;TTL 정책&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;기간&lt;/th&gt;&lt;th&gt;적합&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;24시간&lt;/td&gt;&lt;td&gt;Stripe 표준&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;7일&lt;/td&gt;&lt;td&gt;더 안전&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;영구&lt;/td&gt;&lt;td&gt;결제처럼 &lt;em&gt;극한 안전&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;짧으면&lt;/em&gt; 재시도 보호 부족. &lt;em&gt;길면&lt;/em&gt; store 부담.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;분산--큐의-idempotency&quot;&gt;분산 / 큐의 idempotency&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Producer] --&gt;|publish + msg_id| Broker&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Broker --&gt; C[Consumer]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|이미 처리?| Inbox[(Inbox table)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Inbox --&gt;|YES| Skip&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Inbox --&gt;|NO| Process[처리 + 기록]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 건 [[outbox-pattern]] 의 &lt;em&gt;Inbox&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;멱등한-작업-만드는-패턴&quot;&gt;멱등한 작업 만드는 패턴&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;작업&lt;/th&gt;&lt;th&gt;멱등으로 만드는 방법&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;이메일 발송&lt;/td&gt;&lt;td&gt;&lt;em&gt;deduplication store&lt;/em&gt; + 발송 전 체크&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DB INSERT&lt;/td&gt;&lt;td&gt;UNIQUE constraint + ON CONFLICT&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;계좌 송금&lt;/td&gt;&lt;td&gt;transaction_id 컬럼 + UNIQUE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Webhook 수신&lt;/td&gt;&lt;td&gt;event_id + 처리 표시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;API 호출&lt;/td&gt;&lt;td&gt;Idempotency-Key 헤더&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;클라이언트가 &lt;em&gt;매번 새 key 생성&lt;/em&gt;&lt;/strong&gt; = idempotency 의미 &lt;em&gt;없음&lt;/em&gt;. retry 시 &lt;em&gt;같은 key 재사용&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;key 충돌&lt;/strong&gt; = UUID v4 사용. 클라이언트 라이브러리가 자동 생성.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL 너무 짧음&lt;/strong&gt; = 24시간 뒤 &lt;em&gt;재시도&lt;/em&gt; 시 &lt;em&gt;중복 처리&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;in-progress 처리&lt;/strong&gt; = 그동안 동일 key 가 또 오면 &lt;em&gt;대기 vs 거절&lt;/em&gt; 정책. 보통 409.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;key-생성-전략&quot;&gt;Key 생성 전략&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 클라이언트에서 생성 (가장 일반적)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; idempotencyKey&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; uuidv4&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 무작위, 충돌 거의 없음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 결정론적 key (UUID v5 = namespace + 입력의 SHA-1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; NAMESPACE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;6ba7b810-9dad-11d1-80b4-00c04fd430c8&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; uuidv5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`payment:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;userId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;orderId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}:${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;amount&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;NAMESPACE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// 같은 입력이면 항상 같은 key = 재시도 시 동일 key 재사용 가능&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;방식&lt;/th&gt;&lt;th&gt;장점&lt;/th&gt;&lt;th&gt;단점&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;UUID v4 (랜덤)&lt;/td&gt;&lt;td&gt;간단, 충돌 거의 없음&lt;/td&gt;&lt;td&gt;재시작 시 새 key 생성됨&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;UUID v5 (해시)&lt;/td&gt;&lt;td&gt;결정론적, 재시도 가능&lt;/td&gt;&lt;td&gt;namespace 관리 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;${requestId}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;직관적&lt;/td&gt;&lt;td&gt;클라이언트 구현 필요&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
클라이언트가 &lt;em&gt;retry 마다 새 key 를 생성&lt;/em&gt; = idempotency 의미 없음. retry 시 반드시 &lt;em&gt;같은 key 재사용&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;클라이언트-구현-패턴&quot;&gt;클라이언트 구현 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Axios interceptor: 자동 idempotency-key 추가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; api&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; axios.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;create&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ baseURL: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;api.interceptors.request.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;config&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; method&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; config.method?.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toLowerCase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ([&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;post&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;patch&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;put&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;].&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(method)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // ??= 로 retry 시 같은 key 유지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        config.headers[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??=&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; uuidv4&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; config;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Python + httpx + tenacity retry&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; httpx, uuid&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tenacity &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; retry, stop_after_attempt, wait_exponential&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@retry&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;stop&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;stop_after_attempt(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;wait&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;wait_exponential(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;min&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; charge&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(amount: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, key: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; None&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    key &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; key &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;or&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uuid.uuid4())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # retry 할 때 외부에서 key 를 넘겨야 같은 key 재사용&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; httpx.post(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;/v1/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: amount},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: key},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        timeout&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response.raise_for_status()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.json()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 올바른 재시도: key 를 먼저 생성 후 함수에 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;key &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(uuid.uuid4())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;result &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; charge(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;5000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;key)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;idempotency-라이프사이클&quot;&gt;Idempotency 라이프사이클&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A[&quot;클라이언트: key 생성 (UUID v4)&quot;] --&gt; B[&quot;요청 전송&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    B --&gt; C{&quot;Store 조회&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|&quot;없음&quot;| D[&quot;IN_PROGRESS 저장&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    D --&gt; E[&quot;비즈니스 로직 실행&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    E --&gt;|&quot;성공&quot;| F[&quot;COMPLETED + 응답 저장&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    E --&gt;|&quot;실패&quot;| G[&quot;DELETE or FAILED 저장&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    F --&gt; H[&quot;응답 반환&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    G --&gt; I[&quot;에러 반환, 재시도 가능&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|&quot;COMPLETED&quot;| J[&quot;캐시된 응답 즉시 반환&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    C --&gt;|&quot;IN_PROGRESS&quot;| K[&quot;409 Conflict 반환&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    B2[&quot;재시도 (같은 key)&quot;] --&gt; C&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;테스트-전략&quot;&gt;테스트 전략&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pytest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; TestIdempotency&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_first_call_processes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, client):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        key &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;test-key-1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        res &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                          headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: key})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 200&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res.json()[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; None&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_retry_returns_same_response&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, client):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        key &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;test-key-2&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        res1 &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                           headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: key})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        res2 &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                           headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: key})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 응답 동일&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res1.json()[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res2.json()[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # DB 에 결제 1건만&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count_charges() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_different_keys_create_different_charges&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, client):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        res1 &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                           headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;key-A&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        res2 &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                           headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;key-B&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res1.json()[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res2.json()[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count_charges() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_body_mismatch_same_key_returns_400&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, client):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        key &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;test-key-3&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                    headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: key})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        res &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/charges&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;amount&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;999&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                          headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Idempotency-Key&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: key})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 같은 key + 다른 body = 거절&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 400&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[outbox-pattern]]&lt;/li&gt;
&lt;li&gt;[[saga-pattern]]&lt;/li&gt;
&lt;li&gt;[[kafka]]&lt;/li&gt;
&lt;li&gt;[[redis-distributed-lock]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>idempotency</category><category>pattern</category><category>api-design</category><category>backend</category><category>distributed</category><author>koa (김신건)</author></item><item><title>[Pattern] Outbox Pattern: DB + 메시지의 원자성</title><link>https://shinkeonkim.com/wiki/distributed-systems/outbox-pattern/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/outbox-pattern/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Outbox Pattern&lt;/strong&gt; = &lt;em&gt;DB 변경 + 메시지 발행&lt;/em&gt; 의 &lt;em&gt;원자성 보장&lt;/em&gt;. &lt;em&gt;이중 쓰기 (dual write) 문제&lt;/em&gt; 의 표준 해결.&lt;/p&gt;
&lt;h2 id=&quot;문제-dual-write&quot;&gt;문제: Dual Write&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant App&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant DB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Broker as Kafka/RabbitMQ&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;DB: BEGIN&amp;#x3C;br/&gt;INSERT order&amp;#x3C;br/&gt;COMMIT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DB--&gt;&gt;App: OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App-&gt;&gt;Broker: publish OrderCreated&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over App,Broker: ✗ App crash, 또는&amp;#x3C;br/&gt;Broker timeout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Broker--xApp: 응답 없음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over DB,Broker: DB 는 INSERT 됨&amp;#x3C;br/&gt;이벤트는 안 감 → *불일치*&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;시나리오&lt;/th&gt;&lt;th&gt;결과&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DB OK + Publish OK&lt;/td&gt;&lt;td&gt;일관성&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DB OK + Publish fail&lt;/td&gt;&lt;td&gt;&lt;em&gt;불일치&lt;/em&gt; (DB 만 변경)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DB fail + Publish OK&lt;/td&gt;&lt;td&gt;&lt;em&gt;불일치&lt;/em&gt; (이벤트만 갔음)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;둘 다 fail&lt;/td&gt;&lt;td&gt;OK (취소)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;→ &lt;em&gt;두 시스템을 한 번에 일관&lt;/em&gt; 시키는 건 &lt;em&gt;불가능&lt;/em&gt;. &lt;em&gt;2PC&lt;/em&gt; 는 &lt;em&gt;비현실적&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;해결-outbox-테이블&quot;&gt;해결: Outbox 테이블&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    App --&gt;|단일 트랜잭션| DB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DB --&gt; Orders[(orders)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DB --&gt; Outbox[(outbox)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Outbox -.relay.-&gt; Broker[(Broker)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders (id, ...) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (?, ...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox (event_type, payload, aggregate_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;OrderCreated&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, ?, ?);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 한 트랜잭션 = 원자성!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이후 &lt;em&gt;별도 프로세스&lt;/em&gt; 가 outbox 를 읽고 &lt;em&gt;broker 로 publish&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;2가지-relay-방식&quot;&gt;2가지 Relay 방식&lt;/h2&gt;
&lt;h3 id=&quot;1-polling-publisher&quot;&gt;1. Polling Publisher&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Outbox[(outbox)] -.SELECT unpublished.-&gt; Relay[Relay process]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Relay --&gt;|publish| Broker&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Relay --&gt;|UPDATE published=true| Outbox&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    rows &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; db.query(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;SELECT * FROM outbox WHERE published=false LIMIT 100&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; row &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rows:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        broker.publish(row.event)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        db.execute(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;UPDATE outbox SET published=true WHERE id=?&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, row.id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    time.sleep(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;구현 간단.&lt;/li&gt;
&lt;li&gt;DB 부담 (polling).&lt;/li&gt;
&lt;li&gt;Latency ≈ polling 주기.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-cdc-change-data-capture&quot;&gt;2. CDC (Change Data Capture)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DB[(DB)] --&gt;|WAL / binlog| CDC[Debezium / Maxwell]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CDC --&gt; Kafka[(Kafka)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Kafka --&gt;|topic| Consumers&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DB 의 &lt;em&gt;transaction log (PostgreSQL WAL / MySQL binlog)&lt;/em&gt; 를 &lt;em&gt;실시간 캡처&lt;/em&gt;.&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;도구&lt;/th&gt;&lt;th&gt;DB&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Debezium&lt;/td&gt;&lt;td&gt;PG, MySQL, MongoDB, Oracle 등&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Maxwell&lt;/td&gt;&lt;td&gt;MySQL&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AWS DMS&lt;/td&gt;&lt;td&gt;다양&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Striim&lt;/td&gt;&lt;td&gt;다양&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;Outbox + CDC&lt;/em&gt; 는 &lt;em&gt;2026 시점 마이크로서비스 메시징의 표준&lt;/em&gt;. 더 이상 &lt;em&gt;애플리케이션 코드에서 publish&lt;/em&gt; 하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;outbox-테이블-디자인&quot;&gt;Outbox 테이블 디자인&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;CREATE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; outbox&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BIGSERIAL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; PRIMARY KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  aggregate_type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TEXT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; NOT NULL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,        &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- &apos;Order&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  aggregate_id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TEXT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; NOT NULL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,           &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- &apos;order_42&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  event_type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TEXT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; NOT NULL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,             &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- &apos;OrderCreated&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  payload JSONB &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;NOT NULL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  occurred_at &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; DEFAULT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; NOW&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  published &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BOOLEAN&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; DEFAULT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  published_at &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TIMESTAMPTZ&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;CREATE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; INDEX&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; idx_outbox_unpublished&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  ON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox(id) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; published &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;em&gt;partial index&lt;/em&gt; 로 &lt;em&gt;unpublished 만 빠른 SELECT&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;처리-후-정리&quot;&gt;처리 후 정리&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P[Outbox 너무 큼?]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt; O1[옵션 1: published row 즉시 DELETE]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt; O2[옵션 2: 별도 archive 테이블]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt; O3[&quot;옵션 3: TTL (며칠 보관)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CDC 가 &lt;em&gt;binlog stream 위주&lt;/em&gt; 라 &lt;em&gt;published 컬럼 자체 필요 없음&lt;/em&gt;. Debezium 이 &lt;em&gt;INSERT 이벤트 그대로&lt;/em&gt; 전송.&lt;/p&gt;
&lt;h2 id=&quot;메시지-순서-보장&quot;&gt;메시지 순서 보장&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Tx1[Tx 1: INSERT outbox id=100] --&gt; Order&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Tx2[&quot;Tx 2: INSERT outbox id=99 (먼저 시작했지만 commit 늦음)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Tx2 --&gt; Order&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note[&quot;DB ID 순 ≠ commit 순&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;em&gt;concurrent transaction&lt;/em&gt; 때문에 &lt;code&gt;id&lt;/code&gt; 순서 ≠ &lt;em&gt;commit 순서&lt;/em&gt;. &lt;em&gt;strict 순서 보장&lt;/em&gt; 이 필요하면 &lt;em&gt;aggregate ID 별 partition&lt;/em&gt; 으로.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;at-least-once--idempotency&quot;&gt;At-least-once + Idempotency&lt;/h2&gt;
&lt;p&gt;Outbox + relay 는 &lt;em&gt;at-least-once&lt;/em&gt;. &lt;em&gt;중복 발행&lt;/em&gt; 가능 (relay 가 publish 후 update 전 crash).&lt;/p&gt;
&lt;p&gt;→ &lt;em&gt;consumer 가 idempotent 처리&lt;/em&gt;. 자세한 건 [[idempotency-keys]].&lt;/p&gt;
&lt;h2 id=&quot;inbox-pattern-반대-방향&quot;&gt;Inbox Pattern (반대 방향)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Broker --&gt;|consume| Inbox[(inbox)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Inbox --&gt;|단일 트랜잭션| DB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DB --&gt;|이미 처리?| Skip[중복 skip]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;수신 측의 &lt;em&gt;멱등 보장&lt;/em&gt;. Outbox 와 &lt;em&gt;대칭&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;사용-상황&quot;&gt;사용 상황&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;상황&lt;/th&gt;&lt;th&gt;권장 방식&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;마이크로서비스 간 이벤트&lt;/td&gt;&lt;td&gt;Outbox + CDC&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;레거시 DB (WAL 접근 불가)&lt;/td&gt;&lt;td&gt;Polling Publisher&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;실시간 latency &amp;#x3C; 100ms&lt;/td&gt;&lt;td&gt;CDC 권장&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;단순 단일 서비스&lt;/td&gt;&lt;td&gt;불필요 (직접 publish)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;이벤트 감사 로그 필요&lt;/td&gt;&lt;td&gt;Outbox + archive 테이블&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;polling-vs-cdc-선택&quot;&gt;Polling vs CDC 선택&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S[Relay 방식 선택]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S --&gt; Q1{&quot;DB WAL/binlog&amp;#x3C;br/&gt;접근 가능?&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;Yes&quot;| Q2{&quot;Latency 요구&amp;#x3C;br/&gt;&amp;#x3C; 500ms?&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;No&quot;| Poll[&quot;Polling Publisher&amp;#x3C;br/&gt;(구현 단순)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|&quot;Yes&quot;| CDC[&quot;CDC (Debezium)&amp;#x3C;br/&gt;(실시간, 저 latency)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2 --&gt;|&quot;No&quot;| Poll2[&quot;Polling Publisher&amp;#x3C;br/&gt;(latency 허용)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
Debezium + PostgreSQL WAL 조합이 &lt;em&gt;2026 표준&lt;/em&gt;. &lt;code&gt;wal_level=logical&lt;/code&gt; 설정 필요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;프레임워크-통합&quot;&gt;프레임워크 통합&lt;/h2&gt;
&lt;h3 id=&quot;spring-boot--spring-modulith&quot;&gt;Spring Boot + Spring Modulith&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// @ApplicationModuleListener 가 Outbox 자동 관리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ApplicationModuleListener&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(OrderCreated event) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Spring Modulith 가 ApplicationEvent 를 Outbox 로 직렬화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;debezium-connector-설정&quot;&gt;Debezium Connector 설정&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;outbox-connector&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;config&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;connector.class&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;io.debezium.connector.postgresql.PostgresConnector&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;database.hostname&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;localhost&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;table.include.list&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;public.outbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;transforms&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;outbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;transforms.outbox.type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;io.debezium.transforms.outbox.EventRouter&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;transforms.outbox.table.field.event.id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;transforms.outbox.table.field.event.key&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;aggregate_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;transforms.outbox.table.field.event.type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;event_type&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;운영-고려사항&quot;&gt;운영 고려사항&lt;/h2&gt;
&lt;h3 id=&quot;1-relay-lag-모니터링&quot;&gt;1. Relay Lag 모니터링&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 발행 안 된 건수 + 최고령 행 나이&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; unpublished_count,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    EXTRACT(EPOCH &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;NOW&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; MIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(occurred_at))) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; oldest_sec&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; published &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;oldest_sec&lt;/code&gt; 이 SLA (예: 30초) 를 넘으면 &lt;em&gt;relay 가 막혔다는 신호&lt;/em&gt;. 알람 설정 필수.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;2-dead-letter-처리&quot;&gt;2. Dead Letter 처리&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ALTER&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ADD&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; COLUMN retry_count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; DEFAULT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ALTER&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ADD&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; COLUMN last_error &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;TEXT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- relay 에서 실패 시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; retry_count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; retry_count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    last_error  &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;Broker timeout&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ?;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- retry_count &gt; 5 → DLQ 이동&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-대용량-정리-정책&quot;&gt;3. 대용량 정리 정책&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Pub[published 행 누적]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Pub --&gt; A{&quot;보관 정책?&quot;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A --&gt;|&quot;즉시&quot;| Del[&quot;DELETE WHERE published=true&amp;#x3C;br/&gt;(relay 직후)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A --&gt;|&quot;TTL&quot;| TTL[&quot;DELETE WHERE published_at &amp;#x26;lt; NOW()-7d&amp;#x3C;br/&gt;(배치 job)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A --&gt;|&quot;Archive&quot;| Arc[&quot;INSERT INTO outbox_archive&amp;#x3C;br/&gt;+ DELETE FROM outbox&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 효율적 배치 삭제 (lock 최소화)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;DELETE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outbox&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; published &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  AND&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; published_at &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; NOW&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; INTERVAL &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;7 days&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;LIMIT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 10000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;테스트-전략&quot;&gt;테스트 전략&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 통합 테스트: outbox 행 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_order_creates_outbox_event&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db, client):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    resp &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/orders&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;item&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Widget&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; resp.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 201&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    rows &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; db.execute(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;SELECT event_type, payload FROM outbox WHERE published=false&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ).fetchall()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rows) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rows[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;event_type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;OrderCreated&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# relay 테스트: 멱등성 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_relay_idempotent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db, broker, relay):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    db.execute(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;INSERT INTO outbox (event_type, payload) VALUES (&apos;E&apos;, &apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;)&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    relay.run_once()  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 첫 번째 relay&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    relay.run_once()  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# relay 가 crash 후 재시작 시나리오&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    msgs &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; broker.consumed_messages(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;events&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(msgs) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # 중복 없음 (consumer idempotent)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;*&lt;em&gt;&lt;code&gt;UPDATE published=true&lt;/code&gt; 와 &lt;em&gt;publish 가 &lt;em&gt;별도 transaction&lt;/em&gt;&lt;/em&gt;&lt;/em&gt; = relay crash 시 중복 발행. &lt;em&gt;at-least-once 인정&lt;/em&gt; + consumer idempotent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbox 의 &lt;em&gt;순서 무시&lt;/em&gt;&lt;/strong&gt; = 다운스트림이 순서 가정. partition / sequence 명시.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbox 가 &lt;em&gt;너무 큼&lt;/em&gt;&lt;/strong&gt; = 정리 필요. 보관 정책.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SELECT FOR UPDATE SKIP LOCKED&lt;/code&gt;&lt;/strong&gt; 없이 &lt;em&gt;multi worker&lt;/em&gt; = &lt;em&gt;같은 row 동시 처리&lt;/em&gt; → 중복.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[kafka]], [[rabbitmq]]&lt;/li&gt;
&lt;li&gt;[[saga-pattern]]&lt;/li&gt;
&lt;li&gt;[[idempotency-keys]]&lt;/li&gt;
&lt;li&gt;[[event-sourcing]]&lt;/li&gt;
&lt;li&gt;[[Redis Pub Sub vs Streams]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>outbox</category><category>messaging</category><category>cdc</category><category>distributed</category><category>pattern</category><author>koa (김신건)</author></item><item><title>[Pattern] Saga: 분산 트랜잭션의 보상 흐름</title><link>https://shinkeonkim.com/wiki/distributed-systems/saga-pattern/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/saga-pattern/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Saga&lt;/strong&gt; = &lt;em&gt;여러 service 의 로컬 트랜잭션&lt;/em&gt; 을 &lt;em&gt;연결한 긴 흐름&lt;/em&gt;. 한 단계 실패 시 &lt;em&gt;이전 단계의 보상 (compensating) 트랜잭션&lt;/em&gt; 으로 &lt;em&gt;논리적 롤백&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
Saga 는 &lt;em&gt;ACID 트랜잭션이 아니다&lt;/em&gt;. 분산 환경에서 &lt;em&gt;2PC (XA)&lt;/em&gt; 가 비현실적이라 &lt;em&gt;eventually consistent + 보상&lt;/em&gt; 으로 대체.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;시나리오-주문&quot;&gt;시나리오: 주문&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant O as Order Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant P as Payment Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant I as Inventory Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant S as Shipping Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;O: T1: 주문 생성 (pending)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;P: T2: 결제 요청&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P--&gt;&gt;O: 결제 완료&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;I: T3: 재고 차감&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    I--&gt;&gt;O: 차감 완료&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;S: T4: 배송 요청&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S--&gt;&gt;O: 배송 시작&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;O: T5: 주문 confirmed&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실패 시 보상:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;P: T2 결제&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P--&gt;&gt;O: ✓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;I: T3 재고&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    I--&gt;&gt;O: 실패 (재고 부족)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;P: C2: 결제 환불 (보상)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O-&gt;&gt;O: C1: 주문 취소 (보상)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;단계&lt;/th&gt;&lt;th&gt;Compensating&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;T1 주문 생성&lt;/td&gt;&lt;td&gt;C1 주문 취소&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;T2 결제&lt;/td&gt;&lt;td&gt;C2 환불&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;T3 재고 차감&lt;/td&gt;&lt;td&gt;C3 재고 복원&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;T4 배송&lt;/td&gt;&lt;td&gt;C4 배송 취소&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;두-스타일-choreography-vs-orchestration&quot;&gt;두 스타일: Choreography vs Orchestration&lt;/h2&gt;
&lt;h3 id=&quot;1-choreography-이벤트-기반&quot;&gt;1. Choreography (이벤트 기반)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O[Order] --&gt;|OrderCreated| P[Payment]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt;|PaymentSucceeded| I[Inventory]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    I --&gt;|StockReserved| S[Shipping]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S --&gt;|Shipped| O&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt;|PaymentFailed| O&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    I --&gt;|StockUnavailable| P&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;각 service 가 &lt;em&gt;event 듣고 자기 일&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;중앙 제어자 없음.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;유연&lt;/em&gt;, &lt;em&gt;낮은 결합&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;흐름 추적 어려움&lt;/em&gt; (어디까지 갔는지).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-orchestration-중앙-코디네이터&quot;&gt;2. Orchestration (중앙 코디네이터)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord[Saga Orchestrator] --&gt; O[Order]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O --&gt;|성공| Coord&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord --&gt; P[Payment]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P --&gt;|성공| Coord&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord --&gt; I[Inventory]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    I --&gt;|실패| Coord&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord --&gt;|보상| P&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Coord --&gt;|보상| O&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;중앙 상태 기계&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;흐름 &lt;em&gt;명확&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;결합도 약간 높음&lt;/em&gt; (orchestrator 가 모든 service 안)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;문제 추적 쉬움&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Choreography&lt;/th&gt;&lt;th&gt;Orchestration&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;결합도&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;약간 높음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;흐름 추적&lt;/td&gt;&lt;td&gt;&lt;em&gt;어려움&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;쉬움&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;디버깅&lt;/td&gt;&lt;td&gt;어려움&lt;/td&gt;&lt;td&gt;쉬움&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;적합&lt;/td&gt;&lt;td&gt;단순 흐름 (3-4 단계)&lt;/td&gt;&lt;td&gt;복잡 흐름 (5+ 단계)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;운영 도구&lt;/td&gt;&lt;td&gt;event tracer&lt;/td&gt;&lt;td&gt;상태 기계 시각화&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;em&gt;3-4 단계까지는 choreography&lt;/em&gt;, &lt;em&gt;그 이상은 orchestration&lt;/em&gt; 이 일반 권장. Netflix 도 &lt;em&gt;Conductor&lt;/em&gt; 라는 orchestrator 사용.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;보상-트랜잭션의-함정&quot;&gt;보상 트랜잭션의 함정&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q[보상이 *항상 가능* 한가?]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; X1[&quot;✗ 이메일 발송 (취소 불가)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; X2[&quot;✗ SMS / push 알림&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; X3[&quot;✗ 외부 API 호출 (paypal 환불 한도)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Sol1[&quot;→ 가능한 단계만 saga 안&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Sol2[&quot;→ 비가역 단계는 saga *마지막* 으로&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;em&gt;비가역 작업&lt;/em&gt; (이메일, push, 외부 API) 은 &lt;em&gt;saga 가 commit 결정 후&lt;/em&gt; 실행. 그 전에 실행하면 &lt;em&gt;보상 불가&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;멱등성--idempotency-key&quot;&gt;멱등성 + Idempotency Key&lt;/h2&gt;
&lt;p&gt;각 단계 / 보상은 &lt;em&gt;멱등 (idempotent)&lt;/em&gt; 해야 한다. 자세한 건 [[idempotency-keys]].&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 보상 시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; compensate_payment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(saga_id, payment_id):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; already_refunded(saga_id):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;                       # 멱등&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; payment_service.refund(payment_id, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;saga_id)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;saga-상태-저장&quot;&gt;Saga 상태 저장&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Orch[Orchestrator] --&gt; Store[(Saga State Store)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt; SQL[(PostgreSQL)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt; Stream[(Kafka)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Store --&gt; SF[(Step Functions)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Store&lt;/th&gt;&lt;th&gt;특징&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;PostgreSQL (DB)&lt;/td&gt;&lt;td&gt;트랜잭션 친화&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Kafka topic&lt;/td&gt;&lt;td&gt;event log 자연 통합&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AWS Step Functions&lt;/td&gt;&lt;td&gt;managed orchestrator&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Temporal&lt;/td&gt;&lt;td&gt;워크플로 엔진&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Camunda&lt;/td&gt;&lt;td&gt;BPMN 표준&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;temporal--step-functions-워크플로-엔진&quot;&gt;Temporal / Step Functions (워크플로 엔진)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SDK[&quot;Application code&amp;#x3C;br/&gt;(workflow + activity)&quot;] --&gt; Temporal[Temporal Server]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Temporal --&gt; StateDB[(상태 DB)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Temporal --&gt; Workers[&quot;Workers&amp;#x3C;br/&gt;(activity 실행)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;워크플로 코드&lt;/em&gt; 가 &lt;em&gt;durable execution&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;retry / timeout / sleep&lt;/em&gt; 이 &lt;em&gt;언어 native&lt;/em&gt; 처럼.&lt;/li&gt;
&lt;li&gt;마치 &lt;em&gt;함수 호출 같은데 분산 + 영속&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;모든 분산 흐름 = saga&lt;/strong&gt; = 단순 &lt;em&gt;async event&lt;/em&gt; 가 충분한데도 복잡한 saga. &lt;em&gt;진짜 보상 필요한 흐름에만&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보상 &lt;em&gt;없거나 미구현&lt;/em&gt;&lt;/strong&gt; = “&lt;em&gt;해피 패스만 만들고 실패는 운영자 처리&lt;/em&gt;”. 1년 뒤 &lt;em&gt;데이터 불일치 폭증&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choreography 의 &lt;em&gt;순환 의존성&lt;/em&gt;&lt;/strong&gt; = A → B → C → A 같은 흐름. 디버깅 지옥.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상태 저장 안 함&lt;/strong&gt; = orchestrator 다운 시 &lt;em&gt;진행 잃음&lt;/em&gt;. event log 또는 DB 필수.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;saga-상태-기계&quot;&gt;Saga 상태 기계&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;stateDiagram-v2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    [*] --&gt; STARTED&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    STARTED --&gt; PROCESSING : 단계 실행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    PROCESSING --&gt; PROCESSING : 다음 단계&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    PROCESSING --&gt; COMPENSATING : 단계 실패&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    COMPENSATING --&gt; COMPENSATING : 보상 단계 실행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    COMPENSATING --&gt; FAILED : 보상 완료&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    PROCESSING --&gt; COMPLETED : 모든 단계 성공&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    FAILED --&gt; [*]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    COMPLETED --&gt; [*]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;상태&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;STARTED&lt;/td&gt;&lt;td&gt;saga 생성, 첫 단계 전&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PROCESSING&lt;/td&gt;&lt;td&gt;정방향 단계 실행 중&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;COMPENSATING&lt;/td&gt;&lt;td&gt;실패 후 보상 단계 실행 중&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;COMPLETED&lt;/td&gt;&lt;td&gt;모든 단계 성공&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FAILED&lt;/td&gt;&lt;td&gt;보상 완료, 논리적 롤백 완료&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;saga-와-2pc-비교&quot;&gt;Saga 와 2PC 비교&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Saga&lt;/th&gt;&lt;th&gt;2PC (XA)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;트랜잭션 격리&lt;/td&gt;&lt;td&gt;약함 (eventually consistent)&lt;/td&gt;&lt;td&gt;강함 (ACID)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;가용성&lt;/td&gt;&lt;td&gt;높음 (부분 실패 허용)&lt;/td&gt;&lt;td&gt;낮음 (코디네이터 블로킹)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;구현 복잡도&lt;/td&gt;&lt;td&gt;보상 로직 필요&lt;/td&gt;&lt;td&gt;분산 코디네이터 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;성능&lt;/td&gt;&lt;td&gt;비동기 가능&lt;/td&gt;&lt;td&gt;동기, 잠금&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;적합&lt;/td&gt;&lt;td&gt;마이크로서비스&lt;/td&gt;&lt;td&gt;단일 조직 DB 간&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;em&gt;2PC 가 필요하다고 느껴진다면&lt;/em&gt; saga 를 먼저 검토. saga 로 해결 안 되는 경우에만 2PC.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;temporal-워크플로-예시&quot;&gt;Temporal 워크플로 예시&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Temporal Python SDK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; temporalio &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflow, activity&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; datetime &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; timedelta&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@workflow.defn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; OrderSaga&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    @workflow.run&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, order_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # T1: 주문 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        order &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflow.execute_activity(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            confirm_order,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            order_id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            start_to_close_timeout&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;timedelta(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # T2: 결제&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            payment &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflow.execute_activity(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                charge_payment,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                order.payment_info,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                start_to_close_timeout&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;timedelta(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # C1: 보상 - 주문 취소&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflow.execute_activity(cancel_order, order_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            raise&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # T3: 재고 차감&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflow.execute_activity(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                deduct_stock,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                order.items,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                start_to_close_timeout&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;timedelta(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        except&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # C2: 결제 환불&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflow.execute_activity(refund_payment, payment.id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; workflow.execute_activity(cancel_order, order_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            raise&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;completed&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Temporal 의 핵심:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;activity 실패 시 자동 retry&lt;/em&gt; (정책 설정 가능)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;workflow state 는 영속 저장&lt;/em&gt;: 서버 재시작 후에도 재개&lt;/li&gt;
&lt;li&gt;언어 native 코드처럼 쓰지만 분산 + 내구성&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;saga-테스트-전략&quot;&gt;Saga 테스트 전략&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;테스트 레벨&lt;/th&gt;&lt;th&gt;목적&lt;/th&gt;&lt;th&gt;도구&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Unit&lt;/td&gt;&lt;td&gt;각 서비스 보상 로직 멱등성&lt;/td&gt;&lt;td&gt;Jest / pytest&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Integration&lt;/td&gt;&lt;td&gt;오케스트레이터 + 스텁 서비스&lt;/td&gt;&lt;td&gt;Testcontainers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Contract&lt;/td&gt;&lt;td&gt;서비스 간 이벤트 스키마&lt;/td&gt;&lt;td&gt;Pact&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;End-to-End&lt;/td&gt;&lt;td&gt;실패 시나리오 전체 흐름&lt;/td&gt;&lt;td&gt;Temporal test env&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 멱등성 단위 테스트 예시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_compensate_payment_idempotent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    saga_id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;test-saga-1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    payment_id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;pay-1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 첫 번째 보상&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    compensate_payment(saga_id, payment_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 두 번째 보상 (중복) 도 에러 없이 통과&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    compensate_payment(saga_id, payment_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # DB 에 환불 기록 1건만&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count_refunds(payment_id) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Saga 테스트 핵심: &lt;em&gt;실패 경로마다 보상이 올바르게 실행되는지&lt;/em&gt;, &lt;em&gt;보상 자체가 실패할 때 어떻게 되는지&lt;/em&gt; 검증.&lt;/p&gt;
&lt;h2 id=&quot;프로덕션-체크리스트&quot;&gt;프로덕션 체크리스트&lt;/h2&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 각 단계에 Idempotency Key 부여 (재시도 안전)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 모든 보상 트랜잭션 구현 및 멱등성 검증&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; Saga 상태 영속 저장 (DB 또는 워크플로 엔진)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 비가역 작업 (이메일, push) saga 마지막에 배치&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 실패 알람 및 Dead Letter Queue 운영&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 각 단계 timeout + retry 정책 설정&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; correlationId / traceid 로 분산 추적 연결&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 보상 단계 실패 시 수동 개입 알람&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[distributed-systems-distributed-transaction]] (2PC)&lt;/li&gt;
&lt;li&gt;[[outbox-pattern]] (event publishing 보장)&lt;/li&gt;
&lt;li&gt;[[idempotency-keys]]&lt;/li&gt;
&lt;li&gt;[[event-sourcing]]&lt;/li&gt;
&lt;li&gt;[[microservices-vs-monolith]]&lt;/li&gt;
&lt;li&gt;[[cqrs]]&lt;/li&gt;
&lt;li&gt;[[cap-theorem]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>saga</category><category>distributed-transaction</category><category>microservices</category><category>backend</category><author>koa (김신건)</author></item><item><title>[Voice AI] 파이프라인 디커플링: Redis Streams / Kafka 적용</title><link>https://shinkeonkim.com/wiki/distributed-systems/voice-pipeline-decoupling/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/voice-pipeline-decoupling/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;대규모 음성 에이전트 = &lt;em&gt;STT/LLM/TTS 를 분리 배포 + 메시지 큐로 디커플링&lt;/em&gt;. 수평 확장 + 독립 실패.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;em&gt;소규모 (수십 동시 통화)&lt;/em&gt; 는 &lt;em&gt;monolith pipeline (Pipecat 한 프로세스)&lt;/em&gt; 으로 충분. &lt;em&gt;수백 ~ 수천 동시&lt;/em&gt; 부터 디커플링 의미.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;monolith-vs-decoupled&quot;&gt;Monolith vs Decoupled&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Mono[&quot;Monolith&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        M1[&quot;Pipecat process&amp;#x3C;br/&gt;(STT + LLM + TTS 한 프로세스)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Decoupled[&quot;Decoupled&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D1[&quot;Orchestrator (stateless)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D2[STT Workers]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D3[LLM Workers]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D4[TTS Workers]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Q1[(Queue: stt-tasks)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Q2[(Queue: llm-tasks)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Q3[(Queue: tts-tasks)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D1 --&gt; Q1 --&gt; D2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D2 --&gt; Q2 --&gt; D3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D3 --&gt; Q3 --&gt; D4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D4 --&gt; D1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Monolith&lt;/th&gt;&lt;th&gt;Decoupled&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;운영 복잡도&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;수평 확장&lt;/td&gt;&lt;td&gt;동일 비율&lt;/td&gt;&lt;td&gt;&lt;em&gt;단계별 독립&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;독립 실패&lt;/td&gt;&lt;td&gt;전체 다운&lt;/td&gt;&lt;td&gt;&lt;em&gt;단계별 격리&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;지연&lt;/td&gt;&lt;td&gt;&lt;em&gt;최저&lt;/em&gt;&lt;/td&gt;&lt;td&gt;+ queue hop (~10ms)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;동시 통화&lt;/td&gt;&lt;td&gt;수십&lt;/td&gt;&lt;td&gt;&lt;em&gt;수천+&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;큐-선택&quot;&gt;큐 선택&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;큐&lt;/th&gt;&lt;th&gt;적합&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Redis Streams&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;가장 단순, &amp;#x3C; 수만 RPS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Redis Pub/Sub&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;손실 OK, 알림만&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;큰 throughput, 영속, replay&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NATS JetStream&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;가벼움, edge&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;AWS SQS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;managed, AWS only&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;음성 에이전트 = &lt;em&gt;Redis Streams&lt;/em&gt; 가 가장 흔함 (latency 낮음, 운영 단순).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;redis-streams-패턴&quot;&gt;Redis Streams 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Producer (STT worker)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; stt_worker&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    while&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # stt-tasks 에서 받음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        msgs &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.xreadgroup(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;stt-group&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;stt-worker-1&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;stt-tasks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            count&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;block&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; stream, items &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; msgs:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; msg_id, data &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; items:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                audio_url &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; data[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;audio_url&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                transcript &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; deepgram.transcribe(audio_url)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;                # 다음 단계로 전달&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.xadd(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;llm-tasks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                    &quot;session_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: data[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;session_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                    &quot;transcript&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: transcript,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.xack(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;stt-tasks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;stt-group&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, msg_id)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 Streams 는 [[Redis Streams]].&lt;/p&gt;
&lt;h2 id=&quot;orchestrator-stateless&quot;&gt;Orchestrator (stateless)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; VoiceOrchestrator&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;&quot;&quot;Session 만 관리. 실제 처리는 worker.&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_call_start&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, call_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # Session 을 Redis 에 저장 (stateless 의 외부 상태)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.hset(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;call:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;call_id&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;mapping&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;started_at&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: time.time(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;active&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_audio_chunk&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, call_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, audio: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;bytes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 큐에 던지고 끝&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.xadd(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;stt-tasks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;call_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: call_id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;audio&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: audio,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_llm_result&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, call_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, text: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # TTS 큐로&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.xadd(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;tts-tasks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;call_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: call_id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;text&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: text,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_tts_audio&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, call_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, audio_chunk: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;bytes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 사용자에게 stream&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.send_to_user(call_id, audio_chunk)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Orchestrator 인스턴스 자체는 &lt;em&gt;상태 없음&lt;/em&gt;. &lt;em&gt;모든 상태 Redis&lt;/em&gt;. 수평 확장 + 무중단 배포 자유.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;수평-확장&quot;&gt;수평 확장&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LB[Load Balancer] --&gt; O1[Orchestrator 1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LB --&gt; O2[Orchestrator 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LB --&gt; O3[Orchestrator 3]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    O1 &amp;#x26; O2 &amp;#x26; O3 --&gt; Redis[(Redis: 상태 + 큐)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Redis --&gt; STT_W[&quot;STT Workers (auto-scale)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Redis --&gt; LLM_W[&quot;LLM Workers (auto-scale)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Redis --&gt; TTS_W[&quot;TTS Workers (auto-scale)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;컴포넌트&lt;/th&gt;&lt;th&gt;스케일링 기준&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Orchestrator&lt;/td&gt;&lt;td&gt;동시 connection 수&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;STT Worker&lt;/td&gt;&lt;td&gt;stt-tasks lag&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LLM Worker&lt;/td&gt;&lt;td&gt;llm-tasks lag, GPU&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TTS Worker&lt;/td&gt;&lt;td&gt;tts-tasks lag&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;각자 다른 자원&lt;/em&gt;. STT 는 CPU, LLM 은 GPU, TTS 는 GPU 또는 API. 분리 = 효율적 자원.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;sticky-vs-stateless&quot;&gt;Sticky vs Stateless&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{Voice agent stateless?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;Yes (모든 state Redis)&quot;| Stateless[수평 확장 자유]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;No (메모리에 conversation)&quot;| Sticky[Sticky session 필수]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Sticky&lt;/em&gt; = 같은 통화 = 같은 orchestrator. SFU + WebRTC 환경에서 &lt;em&gt;불가피할 때 있음&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;failure-recovery&quot;&gt;Failure Recovery&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Crash[Orchestrator 1 죽음]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Crash --&gt; Detect[Health check fail]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Detect --&gt; Drain[기존 통화 drain]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Detect --&gt; NewO[다른 orchestrator 가 이어받음]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NewO --&gt; Restore[Redis 에서 state 복원]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; take_over_call&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(call_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    state &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.hgetall(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;call:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;call_id&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; state:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 이미 다른 orchestrator 처리 중?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    owner &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; redis.set(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;call:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;call_id&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;:owner&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.instance_id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        nx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ex&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; owner:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 이어받음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.resume_call(call_id, state)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; True&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 &lt;em&gt;상태 관리&lt;/em&gt; 는 [[Redis Sorted Sets]] 의 session 패턴.&lt;/p&gt;
&lt;h2 id=&quot;sentinel--cluster&quot;&gt;Sentinel + Cluster&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Voice[Voice agents] --&gt; Sentinel[&quot;Redis Sentinel&amp;#x3C;br/&gt;(자동 failover)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sentinel --&gt; Primary[(Redis Primary)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Primary --&gt; Replica1[(Replica 1)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Primary --&gt; Replica2[(Replica 2)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;음성 = &lt;em&gt;Redis 단일 장애 = 모든 통화 끊김&lt;/em&gt;. Sentinel 또는 Cluster + sticky session 보호 필수.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;외부-api-복원력&quot;&gt;외부 API 복원력&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q[복원력 패턴]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; T1[&quot;Timeout (짧게)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; T2[&quot;Retry + backoff (idempotent only)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; T3[Circuit Breaker]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; T4[&quot;Fallback (다른 vendor)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; T5[Health check]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;단계&lt;/th&gt;&lt;th&gt;timeout&lt;/th&gt;&lt;th&gt;fallback&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;STT&lt;/td&gt;&lt;td&gt;5초&lt;/td&gt;&lt;td&gt;Deepgram → AssemblyAI&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LLM&lt;/td&gt;&lt;td&gt;30초&lt;/td&gt;&lt;td&gt;GPT-4o-mini → Claude Haiku&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TTS&lt;/td&gt;&lt;td&gt;10초&lt;/td&gt;&lt;td&gt;Cartesia → Azure&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;자세한 건 [[retry-with-backoff]], [[circuit-breaker]].&lt;/p&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Monolith → Decouple 전환 너무 일찍&lt;/strong&gt; = 운영 복잡도 폭증. 수십 동시 통화는 monolith.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상태 메모리 보관&lt;/strong&gt; = orchestrator restart = 통화 끊김. Redis 외부 상태.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis Streams MAXLEN 무제한&lt;/strong&gt; = 메모리 폭증. MAXLEN ~ 100K 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue lag 모니터링 없음&lt;/strong&gt; = 어디서 병목인지 모름. Prometheus + alert.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[voice-agent-architecture]]&lt;/li&gt;
&lt;li&gt;[[Redis Streams]]&lt;/li&gt;
&lt;li&gt;[[Redis Pub Sub vs Streams]]&lt;/li&gt;
&lt;li&gt;[[kafka]]&lt;/li&gt;
&lt;li&gt;[[circuit-breaker]]&lt;/li&gt;
&lt;li&gt;[[retry-with-backoff]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>voice-pipeline</category><category>queue</category><category>redis-streams</category><category>kafka</category><category>decoupling</category><category>scaling</category><author>koa (김신건)</author></item><item><title>[Distributed Systems] Consensus: Raft, Paxos</title><link>https://shinkeonkim.com/wiki/distributed-systems/distributed-systems-consensus/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/distributed-systems-consensus/</guid><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Consensus&lt;/strong&gt; (합의): &lt;strong&gt;여러 노드가 같은 값에 동의&lt;/strong&gt; 하는 분산 알고리즘. CAP 의 C 보장의 기반.&lt;/p&gt;
&lt;p&gt;용도:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leader election&lt;/strong&gt;: 1개 leader 선출 (ZooKeeper, etcd, Consul)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replicated log&lt;/strong&gt;: 모든 노드가 같은 명령 순서로 적용 (DB replication, K8s etcd)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed lock&lt;/strong&gt;: 글로벌 락&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration management&lt;/strong&gt;: cluster 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대표 알고리즘:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Paxos&lt;/strong&gt; (1989, Lamport): 이론적 정석, 이해 어려움&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raft&lt;/strong&gt; (2014, Ongaro &amp;#x26; Ousterhout): Paxos 의 이해 가능한 대안. 현재 대세&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PBFT, Byzantine fault tolerance&lt;/strong&gt;: 악의적 노드 가정 (blockchain)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 페이지는 Raft 중심 (가장 보편적).&lt;/p&gt;
&lt;h2 id=&quot;raft-의-3가지-역할&quot;&gt;Raft 의 3가지 역할&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[Leader]   ─── 1명만 존재&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   └── 모든 write 받음, log replication 책임&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[Follower] ─── 나머지 모두&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   └── leader 의 명령 받아 적용&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[Candidate] ─── 전이 상태 (선거 중)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   └── leader 가 사라지면 follower 가 candidate 로 전환, 투표 요청&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;raft-의-핵심-term&quot;&gt;Raft 의 핵심: term&lt;/h2&gt;
&lt;p&gt;시간을 term (선거 회차) 으로 나눔:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;term 1: Leader = A&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;term 2: Leader = B  (A 죽음, B 선출)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;term 3: Leader = A  (B 죽음, A 복귀)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 노드는 자신의 currentTerm 보유. &lt;strong&gt;term 이 낮은 노드의 요청은 거부&lt;/strong&gt; (오래된 leader 격리).&lt;/p&gt;
&lt;h2 id=&quot;leader-election&quot;&gt;Leader Election&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;follower 가 randomized timeout (150~300ms) 동안 leader heartbeat 못 받으면:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  1. 자신을 candidate 로 전환&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  2. currentTerm++&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  3. 자기 자신에 투표&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  4. 다른 노드에 RequestVote RPC 전송&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;응답 시나리오:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - 과반수 (N/2 + 1) 표 받으면 → Leader 됨&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - 다른 candidate 의 더 높은 term 보면 → follower 로 복귀&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  - 표 안 모이면 → 새 election (다른 timeout 후)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;randomized timeout&lt;/code&gt; 이 split vote 회피의 핵심.&lt;/p&gt;
&lt;h2 id=&quot;log-replication&quot;&gt;Log Replication&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[Leader] receives &quot;x = 5&quot; from client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. Leader appends to its log:  [..., (term=3, x=5)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. Leader sends AppendEntries RPC to all followers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. Each follower appends to its log + replies OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;4. Once majority replied → Leader commits (apply to state machine)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;5. Leader notifies followers in next AppendEntries&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;6. Followers apply committed entries to their state machine&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심: &lt;strong&gt;commit = majority 가 log 에 기록&lt;/strong&gt;. apply 는 그 후.&lt;/p&gt;
&lt;h3 id=&quot;log-matching-property&quot;&gt;Log Matching Property&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;같은 index + 같은 term 의 entry = 같은 명령 (강력한 invariant)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 leader 의 log 가 truth. follower 가 mismatch 면 leader 의 것으로 덮어쓰기.&lt;/p&gt;
&lt;h2 id=&quot;safety-어떤-leader-가-선출되나&quot;&gt;Safety: 어떤 leader 가 선출되나?&lt;/h2&gt;
&lt;p&gt;새 leader 는 &lt;strong&gt;모든 committed entry 를 가진 노드&lt;/strong&gt; 만 가능.&lt;/p&gt;
&lt;p&gt;RequestVote 시 candidate 가 자신의 last log term, index 보냄. follower 는 &lt;strong&gt;자기보다 stale 한 candidate 에게 투표 안 함&lt;/strong&gt; (rejects vote if candidate’s log is less up-to-date).&lt;/p&gt;
&lt;p&gt;majority vote 보장 + commit 조건 = “어느 commit 된 entry 는 영구히 보존”.&lt;/p&gt;
&lt;h2 id=&quot;membership-changes&quot;&gt;Membership Changes&lt;/h2&gt;
&lt;p&gt;cluster 멤버 추가 / 제거는 위험 (두 majority 존재 시 split brain):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;old: {A, B, C} → majority = 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;new: {A, B, C, D, E} → majority = 3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;전환 중에 둘 다 자기를 majority 라고 주장 가능&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Raft 의 해결:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Joint consensus&lt;/strong&gt; (논문): old 와 new config 의 majority 모두 필요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single server changes&lt;/strong&gt; (실용): 한 번에 1명만 추가 / 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;snapshot--log-compaction&quot;&gt;Snapshot + Log Compaction&lt;/h2&gt;
&lt;p&gt;log 가 무한히 커지면 안 됨. 주기적으로 snapshot:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. State machine 의 현재 상태를 disk 에 저장&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 그 시점 이전 log entry 삭제&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. Snapshot 에 last included index, term 기록&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;복구 시 snapshot 적용 + 그 이후 log replay.&lt;/p&gt;
&lt;h2 id=&quot;raft-구현체&quot;&gt;Raft 구현체&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;시스템&lt;/th&gt;&lt;th&gt;사용처&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;etcd&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Kubernetes 의 control plane&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Consul&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;service discovery&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CockroachDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;distributed SQL&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;TiKV&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;TiDB 의 storage&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MongoDB (3.2+)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;replication&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Hazelcast (4.0+)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;distributed cache&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;대부분 시스템이 직접 구현하거나 hashicorp/raft 라이브러리 사용.&lt;/p&gt;
&lt;h2 id=&quot;paxos-와의-비교&quot;&gt;Paxos 와의 비교&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Paxos&lt;/th&gt;&lt;th&gt;Raft&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;발표&lt;/td&gt;&lt;td&gt;1989&lt;/td&gt;&lt;td&gt;2014&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;이해도&lt;/td&gt;&lt;td&gt;매우 어려움&lt;/td&gt;&lt;td&gt;비교적 쉬움&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Leader 개념&lt;/td&gt;&lt;td&gt;명시적 X (Multi-Paxos 추가)&lt;/td&gt;&lt;td&gt;명시적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Log replication&lt;/td&gt;&lt;td&gt;명시적 X&lt;/td&gt;&lt;td&gt;명시적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;산업 채택&lt;/td&gt;&lt;td&gt;Spanner, Chubby&lt;/td&gt;&lt;td&gt;etcd, Consul, …&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Raft 가 “이해 가능한 alternative” 를 목표로 설계. 안전성은 동등.&lt;/p&gt;
&lt;h3 id=&quot;paxos-의-단계&quot;&gt;Paxos 의 단계&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Prepare&lt;/strong&gt; (round): proposer 가 proposal number 보내 majority 의 promise 받음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accept&lt;/strong&gt;: 가장 높은 proposal number 의 value 를 majority 에 보냄&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learn&lt;/strong&gt;: 결정된 값 전파&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Multi-Paxos: leader 가 prepare 한 번 + 여러 accept (Raft 의 leader 와 유사).&lt;/p&gt;
&lt;h2 id=&quot;zookeeper-와-zab&quot;&gt;ZooKeeper 와 Zab&lt;/h2&gt;
&lt;p&gt;ZooKeeper 의 합의 알고리즘 &lt;strong&gt;Zab&lt;/strong&gt; (ZooKeeper Atomic Broadcast):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paxos / Raft 와 유사하지만 &lt;strong&gt;total order&lt;/strong&gt; 강제&lt;/li&gt;
&lt;li&gt;Leader 가 모든 update 를 ordered broadcast&lt;/li&gt;
&lt;li&gt;ZooKeeper 의 &lt;code&gt;zxid&lt;/code&gt; (zookeeper transaction id) 가 순서 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ZooKeeper 는 분산 락 / leader election 의 standard. ephemeral node + watch 가 강력.&lt;/p&gt;
&lt;h2 id=&quot;cap-와-합의의-관계&quot;&gt;CAP 와 합의의 관계&lt;/h2&gt;
&lt;p&gt;합의 알고리즘 = CP (강한 일관성).&lt;/p&gt;
&lt;p&gt;분할 시 minority partition 의 노드들은:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;leader 가 없음 → write 불가&lt;/li&gt;
&lt;li&gt;또는 old leader 였다면 commit 못 함 → 정지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;majority partition 만 진행. &lt;strong&gt;가용성 손실 + 일관성 보장&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;자주-보는-함정&quot;&gt;자주 보는 함정&lt;/h2&gt;
&lt;h3 id=&quot;함정-1-split-brain&quot;&gt;함정 1: split brain&lt;/h3&gt;
&lt;p&gt;분할 후 양쪽이 각자 leader 선출 → 둘 다 update → 복구 후 충돌.&lt;/p&gt;
&lt;p&gt;Raft 의 term + majority 가 방어. minority 의 “leader” 는 다음 term 의 leader 로 인해 거부됨.&lt;/p&gt;
&lt;h3 id=&quot;함정-2-clock-skew&quot;&gt;함정 2: clock skew&lt;/h3&gt;
&lt;p&gt;Lamport timestamp / Vector clock 같은 logical clock 사용. wall clock 의존 시 NTP drift 로 깨짐.&lt;/p&gt;
&lt;p&gt;Raft 는 wall clock 의존 안 함 (term 이 logical).&lt;/p&gt;
&lt;h3 id=&quot;함정-3-2-노드-cluster-의-위험&quot;&gt;함정 3: 2 노드 cluster 의 위험&lt;/h3&gt;
&lt;p&gt;majority = 2/2. 1 노드 죽으면 cluster 정지. 3 노드가 최소.&lt;/p&gt;
&lt;h3 id=&quot;함정-4-짝수-노드-cluster&quot;&gt;함정 4: 짝수 노드 cluster&lt;/h3&gt;
&lt;p&gt;4 노드 cluster 의 majority = 3. 즉 3 노드 cluster 와 같은 fault tolerance (1 노드 fail). 그러나 메시지 비용 ↑. 홀수 권장.&lt;/p&gt;
&lt;h3 id=&quot;함정-5-cross-dc-합의의-latency&quot;&gt;함정 5: cross-DC 합의의 latency&lt;/h3&gt;
&lt;p&gt;majority 가 다른 DC 면 매 write 가 cross-DC latency. 보통 같은 region 에 3 노드. cross-region 은 별도 consensus (geo-replication).&lt;/p&gt;
&lt;h2 id=&quot;성능-trade-off&quot;&gt;성능 trade-off&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3 node cluster&lt;/strong&gt;: 1 fail tolerance, 빠른 합의&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5 node cluster&lt;/strong&gt;: 2 fail tolerance, 약간 느림&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;7+&lt;/strong&gt;: 거의 안 함 (메시지 비용 폭증)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;write throughput = 1 / RTT (대략). leader 가 majority 응답 기다림.&lt;/p&gt;
&lt;h2 id=&quot;함정과-베스트-프랙티스&quot;&gt;함정과 베스트 프랙티스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;홀수 노드 (3, 5, 7)&lt;/strong&gt;: 짝수는 메시지 비용만 ↑&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;majority 가 같은 region&lt;/strong&gt;: cross-DC 면 latency 폭주&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;clock skew 무관&lt;/strong&gt;: 합의 자체엔 영향 X (logical clock)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;snapshot 주기적 + tunable&lt;/strong&gt;: log 무한 성장 방지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;client retry + idempotent&lt;/strong&gt;: leader 전환 중에도 안전&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;monitoring&lt;/strong&gt;: leader election 빈도, log size, applied vs committed gap&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[cap-theorem]]&lt;/li&gt;
&lt;li&gt;[[distributed-systems-distributed-transaction]]&lt;/li&gt;
&lt;li&gt;[[zero-downtime-deployment]]&lt;/li&gt;
&lt;li&gt;[[concurrency/stateful]]&lt;/li&gt;
&lt;li&gt;[[spring-cloud]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>distributed-systems</category><category>consensus</category><category>raft</category><category>paxos</category><category>leader-election</category><author>koa (김신건)</author></item><item><title>[Distributed Systems] 분산 트랜잭션: 2PC, Saga, Outbox</title><link>https://shinkeonkim.com/wiki/distributed-systems/distributed-systems-distributed-transaction/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/distributed-systems/distributed-systems-distributed-transaction/</guid><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;분산 트랜잭션&lt;/strong&gt; 은 여러 service / DB / 메시지 큐에 걸친 작업을 ACID 처럼 묶는 문제. 마이크로서비스 / 이벤트 기반 아키텍처의 핵심 도전.&lt;/p&gt;
&lt;p&gt;3가지 접근:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;2PC (Two-Phase Commit)&lt;/strong&gt;: 동기 ACID, 전통적, 거의 안 씀&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saga&lt;/strong&gt;: 비동기 compensating action, 마이크로서비스 표준&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbox&lt;/strong&gt;: DB 트랜잭션 + 메시지 큐 일관성 보장 (Saga 의 보조)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;선택 기준: 시스템 복잡도, 일관성 요구, 가용성 trade-off.&lt;/p&gt;
&lt;h2 id=&quot;2pc-two-phase-commit&quot;&gt;2PC (Two-Phase Commit)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1단계 (Prepare)&lt;/strong&gt;: coordinator → 모든 participant 에게 “준비 됐어?”
&lt;strong&gt;2단계 (Commit)&lt;/strong&gt;: 모두 OK 면 → “commit”. 한 명이라도 NO 면 → “rollback”&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       [Coordinator]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            │&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   ┌────────┼────────┐&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   ↓        ↓        ↓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt; [DB A]   [DB B]   [DB C]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Phase 1 (Prepare):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   coordinator → &quot;준비?&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   each DB → write log, lock rows, &quot;yes&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Phase 2 (Commit):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   coordinator → &quot;commit&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   each DB → commit, release lock, &quot;done&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;장점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;강한 ACID 보장&lt;/li&gt;
&lt;li&gt;표준 (XA Transaction)&lt;/li&gt;
&lt;li&gt;동기적 - 결과 즉시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blocking&lt;/strong&gt;: coordinator 죽으면 participant 가 락 잡은 채 영원히 대기&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: 매 트랜잭션 = 2 라운드 + N participant&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single point of failure&lt;/strong&gt;: coordinator&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CP 시스템&lt;/strong&gt;: 가용성 손실&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JTA (Java Transaction API) + XA driver 가 구현. 대부분 modern 시스템은 회피.&lt;/p&gt;
&lt;h2 id=&quot;saga-pattern&quot;&gt;Saga Pattern&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;전체 트랜잭션을 여러 local 트랜잭션의 chain&lt;/strong&gt; 으로:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;T1 (서비스 A) → T2 (서비스 B) → T3 (서비스 C)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                ↓ 실패&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              C2 (B 보상) ← C3 (실패 처리)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 step 이 commit 후 다음 발동. 실패 시 &lt;strong&gt;compensating transaction&lt;/strong&gt; 으로 되돌리기.&lt;/p&gt;
&lt;h3 id=&quot;예-주문-처리&quot;&gt;예: 주문 처리&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Step 1: Order Service → 주문 생성 (status=pending)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Step 2: Payment Service → 결제 시도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   성공 → Step 3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   실패 → Compensating: Order Service → 주문 취소&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Step 3: Inventory Service → 재고 차감&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   성공 → Step 4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   실패 → Compensating: Payment Service → 환불 → Order Service → 취소&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Step 4: Shipping Service → 배송 시작&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   성공 → 완료&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   실패 → Compensating: 재고 복구, 환불, 주문 취소&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 service 가 자기 local 트랜잭션만 책임. cross-service 락 없음.&lt;/p&gt;
&lt;h3 id=&quot;두-가지-구현-방식&quot;&gt;두 가지 구현 방식&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Choreography&lt;/strong&gt;: 각 service 가 이벤트 발행 + 구독&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Order Service → &quot;OrderCreated&quot; 이벤트 → Kafka&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Payment Service ← &quot;OrderCreated&quot; 구독 → 결제 시도 → &quot;PaymentCompleted&quot; 발행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Inventory Service ← &quot;PaymentCompleted&quot; 구독 → ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;장점: 분산, 결합도 낮음.
단점: 흐름 추적 어려움, 디버깅 복잡.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Orchestration&lt;/strong&gt;: 중앙 orchestrator 가 흐름 제어&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Saga Orchestrator&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ├── Order Service: 생성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ├── Payment Service: 결제 (실패 시 → 이전 단계 보상)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ├── Inventory Service: 차감&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── Shipping Service: 시작&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;장점: 흐름 명시적, 디버깅 쉬움.
단점: orchestrator 가 단일 장애점, 복잡한 로직.&lt;/p&gt;
&lt;p&gt;도구: AWS Step Functions, Camunda, Temporal, Conductor.&lt;/p&gt;
&lt;h3 id=&quot;saga-의-제약&quot;&gt;Saga 의 제약&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolation 없음&lt;/strong&gt;: 진행 중인 saga 의 중간 상태가 다른 트랜잭션에 보임 → “dirty read” 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eventually consistent&lt;/strong&gt;: 보상 완료까지 일시 비일관&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compensating transaction 설계 어려움&lt;/strong&gt;: idempotent + 영구 부수효과 없어야 (이메일 발송 후 보상 어려움)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대응 패턴:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Semantic lock&lt;/strong&gt;: 진행 중 자원에 flag 표시 (e.g., &lt;code&gt;order.status=processing&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commutative updates&lt;/strong&gt;: 순서 무관하게 결과 동일하도록&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pessimistic view&lt;/strong&gt;: 보수적 처리 (e.g., 미리 예약 → 확정)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;outbox-pattern&quot;&gt;Outbox Pattern&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;DB 트랜잭션 + 메시지 큐 발행&lt;/strong&gt; 의 atomicity 보장:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[전통 문제]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;INSERT order INTO orders ─ DB transaction ─ OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kafka.publish(OrderPlaced) ─ network call ─ 실패&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ DB 에 order 있지만 알림 X (inconsistent)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;또는 반대로:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;kafka.publish(OrderPlaced) ─ 성공&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;INSERT order ─ rollback&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ 알림 갔지만 DB 에 order 없음 (worse!)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Outbox 패턴&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[같은 DB 트랜잭션 안]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  INSERT INTO orders (...);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  INSERT INTO outbox (event_type, payload, status=&apos;pending&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;COMMIT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[별도 worker]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SELECT * FROM outbox WHERE status=&apos;pending&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → Kafka publish&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → UPDATE outbox SET status=&apos;sent&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;장점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB 트랜잭션 = 단일 commit (atomic)&lt;/li&gt;
&lt;li&gt;Worker 가 죽어도 row 가 outbox 에 살아 있어 재시도&lt;/li&gt;
&lt;li&gt;at-least-once delivery (idempotent consumer 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;도구:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debezium&lt;/strong&gt;: outbox 테이블의 CDC (Change Data Capture)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spring Modulith Events&lt;/strong&gt;: Spring 통합&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;직접 구현&lt;/strong&gt;: 간단한 polling worker&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;spring-boot-의-outbox-예&quot;&gt;Spring Boot 의 outbox 예&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Transactional&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; OrderService&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; final&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OrderRepository orderRepo;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; final&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OutboxRepository outboxRepo;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; final&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ObjectMapper objectMapper;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Order &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;place&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(Cart &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;cart&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Order order &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orderRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Order&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(cart));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        OutboxEvent event &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; OutboxEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        event.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setAggregateType&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Order&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        event.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setAggregateId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(order.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        event.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setEventType&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;OrderPlaced&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        event.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setPayload&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(objectMapper.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;writeValueAsString&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(order)));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        event.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setStatus&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;pending&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        outboxRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(event);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; order;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; OutboxRelay&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; final&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OutboxRepository outboxRepo;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; final&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; KafkaTemplate&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; kafka;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    @&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Scheduled&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;fixedDelay&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; relay&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        List&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;OutboxEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; events &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; outboxRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;findTop100ByStatusOrderById&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;pending&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (OutboxEvent e &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; events) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            kafka.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;send&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;orders&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, e.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getAggregateId&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(), e.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;getPayload&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            e.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;setStatus&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;sent&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            outboxRepo.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;save&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(e);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@TransactionalEventListener(AFTER_COMMIT)&lt;/code&gt; 도 비슷한 효과 ([[spring-events]]) 지만 worker 인스턴스 죽으면 event 손실 → outbox 가 더 안전.&lt;/p&gt;
&lt;h2 id=&quot;outbox-vs-2pc-vs-saga-정리&quot;&gt;Outbox vs 2PC vs Saga 정리&lt;/h2&gt;















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;2PC&lt;/th&gt;&lt;th&gt;Saga&lt;/th&gt;&lt;th&gt;Outbox&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;일관성&lt;/td&gt;&lt;td&gt;Strong&lt;/td&gt;&lt;td&gt;Eventual&lt;/td&gt;&lt;td&gt;Eventual&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Blocking&lt;/td&gt;&lt;td&gt;Yes (락)&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Compensating 필요&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;(개별 step 에 따름)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;운영 복잡도&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;td&gt;중간&lt;/td&gt;&lt;td&gt;낮음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;가용성&lt;/td&gt;&lt;td&gt;낮음 (CP)&lt;/td&gt;&lt;td&gt;높음 (AP)&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;사용처&lt;/td&gt;&lt;td&gt;legacy (XA)&lt;/td&gt;&lt;td&gt;마이크로서비스 표준&lt;/td&gt;&lt;td&gt;DB + MQ 양립&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;대부분 modern: &lt;strong&gt;Saga + Outbox 조합&lt;/strong&gt;. 2PC 회피.&lt;/p&gt;
&lt;h2 id=&quot;idempotency&quot;&gt;Idempotency&lt;/h2&gt;
&lt;p&gt;분산 시스템의 모든 작업은 &lt;strong&gt;idempotent&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;재시도해도 결과 같음&lt;/li&gt;
&lt;li&gt;중복 메시지 처리해도 OK&lt;/li&gt;
&lt;li&gt;network 재시도 안전&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;구현 패턴:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idempotency key&lt;/strong&gt;: 클라이언트가 unique key 동봉 → 서버가 처음 보면 처리, 두 번째 보면 캐시 결과 반환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conditional update&lt;/strong&gt;: &lt;code&gt;WHERE status=&apos;pending&apos;&lt;/code&gt; → 이미 처리된 row 는 변경 안 됨&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed lock + check&lt;/strong&gt;: race 회피&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;자주-보는-함정&quot;&gt;자주 보는 함정&lt;/h2&gt;
&lt;h3 id=&quot;함정-1-보상-트랜잭션의-부수효과&quot;&gt;함정 1: 보상 트랜잭션의 부수효과&lt;/h3&gt;
&lt;p&gt;“이메일 발송” 같은 영구 부수효과는 보상 불가. 패턴:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외부 호출은 saga 의 마지막 단계로&lt;/li&gt;
&lt;li&gt;또는 confirmation 단계 분리 (&lt;code&gt;PaymentPending&lt;/code&gt; → &lt;code&gt;PaymentConfirmed&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;함정-2-at-least-once-delivery-의-중복&quot;&gt;함정 2: at-least-once delivery 의 중복&lt;/h3&gt;
&lt;p&gt;Kafka / SQS 는 at-least-once. consumer 는 idempotent 처리 필수. Outbox 자체도 publish 후 status update 사이 crash 시 중복 가능.&lt;/p&gt;
&lt;h3 id=&quot;함정-3-outbox-의-throughput-한계&quot;&gt;함정 3: outbox 의 throughput 한계&lt;/h3&gt;
&lt;p&gt;polling worker 가 단일 인스턴스면 bottleneck. 해결:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;partition 별 worker&lt;/li&gt;
&lt;li&gt;Debezium 같은 CDC tool 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;함정-4-saga-의-부분-실패-디버깅&quot;&gt;함정 4: Saga 의 부분 실패 디버깅&lt;/h3&gt;
&lt;p&gt;choreography 면 흐름 추적 어려움. distributed tracing (OpenTelemetry) + correlation ID 필수.&lt;/p&gt;
&lt;h3 id=&quot;함정-5-two-generals-문제&quot;&gt;함정 5: Two-Generals 문제&lt;/h3&gt;
&lt;p&gt;DB commit + 메시지 큐 publish 의 atomicity 는 본질적으로 불가 (network 위에선). Outbox 가 best-effort.&lt;/p&gt;
&lt;h2 id=&quot;함정과-베스트-프랙티스&quot;&gt;함정과 베스트 프랙티스&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2PC 회피&lt;/strong&gt;: 모든 modern 시스템&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saga + Orchestration&lt;/strong&gt;: 흐름 추적 / 디버깅 우선이면&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbox 가 DB+MQ 일관성의 표준&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모든 작업 idempotent&lt;/strong&gt;: idempotency key + conditional update&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;distributed tracing 필수&lt;/strong&gt;: traceId + spanId&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compensating action 설계가 핵심&lt;/strong&gt;: 영구 부수효과는 마지막에&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;eventual consistency 명시적 안내&lt;/strong&gt;: UX 에 반영&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outbox 의 worker scaling&lt;/strong&gt;: 단일 worker 가 bottleneck&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[cap-theorem]]&lt;/li&gt;
&lt;li&gt;[[distributed-systems-consensus]]&lt;/li&gt;
&lt;li&gt;[[spring-events]]&lt;/li&gt;
&lt;li&gt;[[spring-kafka]]&lt;/li&gt;
&lt;li&gt;[[spring-amqp-rabbitmq]]&lt;/li&gt;
&lt;li&gt;[[spring-transaction]]&lt;/li&gt;
&lt;li&gt;[[zero-downtime-deployment]]&lt;/li&gt;
&lt;li&gt;[[concurrency/pubsub-bus]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>distributed-systems</category><category>transaction</category><category>2pc</category><category>saga</category><category>outbox</category><category>eventual-consistency</category><author>koa (김신건)</author></item></channel></rss>