- ๋ถ์ผ: MSA, Event-Driven Architecture, Saga Pattern, Idempotency, Kafka Partition Key Routing
- ๊ด๋ จ ํ์ผ: OrderCommandService.java , OrderEventConsumer.java
1. ๋ฐ๋จ: ๋น๋๊ธฐ ํ๊ฒฝ์์ ๋ฐ๊ฒฌ๋ '์๊ฐ์ฐจ ์ฃ์ง ์ผ์ด์ค'
์ฃผ๋ฌธ ์์ฑ ํธ๋์ญ์ ์ ๋น๋๊ธฐ Saga ํจํด(Kafka)์ผ๋ก ๋ถ๋ฆฌํ ํ, ์์คํ ์ ์์ ์ฑ์ ๊ฒ์ฆํ๊ธฐ ์ํด ์ฌ๋ฌ ์์ธ ์๋๋ฆฌ์ค๋ฅผ ์๋ฎฌ๋ ์ด์ ํจ. ๊ทธ์ค "์ ์ ์ ๊ธฐ์ต ์ทจ์" ์ํฉ์์ ์ฃ์ง ์ผ์ด์ค๋ฅผ ๋ฐ๊ฒฌํจ
[์๋๋ฆฌ์ค ํ์๋ผ์ธ]
- ์ ์ ๊ฐ ์ฃผ๋ฌธ์ ์์ฒญํ์ฌ ์ํ๊ฐ PENDING(๋๊ธฐ)์ด ๋จ.
- ์ ์ ๊ฐ ๋ณ์ฌํ์ฌ ์ฆ์ ์ฃผ๋ฌธ ์ทจ์(cancelOrder)๋ฅผ ์์ฒญํจ. ์ํ๋ CANCELED๊ฐ ๋๊ณ , ๊น์์์ง ๋ชจ๋ฅผ ์ฌ๊ณ ๋ฅผ ์๋ณตํ๊ธฐ ์ํด STOCK_RESTORE(์ฌ๊ณ ๋ณต๊ตฌ) ์ด๋ฒคํธ๋ฅผ Kafka๋ก ๋ฐํํจ. (๋ณต๊ตฌ ์ด๋ฒคํธ #1)
- [๊ฒฝํฉ ๋ฐ์] ๊ทธ๋ฐ๋ฐ ์ฐฐ๋์ ์๊ฐ์ฐจ๋ก, ์ธ๋ฒคํ ๋ฆฌ ์๋น์ค๋ ์ ์ ๊ฐ ์ทจ์ํ๊ธฐ ์ง์ ์ ์ด๋ฏธ ์ฌ๊ณ ์ฐจ๊ฐ์ ์ฑ๊ณตํ๊ณ ORDER_APPROVED(์น์ธ) ๋ฉ์์ง๋ฅผ ์ค๋ ์๋น์ค๋ก ๋ ๋ ค๋ ์ํ์๋ค๋ฉด?
- ์ค๋ ์๋น์ค๊ฐ ๋ค๋ฆ๊ฒ ์น์ธ ๋ฉ์์ง๋ฅผ ์์ (approveOrder)ํจ. ์ด๋ฏธ ์ทจ์๋ ์ฃผ๋ฌธ์ด๋ฏ๋ก ์ํ ๋ณ๊ฒฝ์ ๋ฌด์ํ์ง๋ง, ์ธ๋ฒคํ ๋ฆฌ์ ์ฌ๊ณ ๊ฐ ๊น์๋ค๋ ๋ป์ด๋ฏ๋ก ๋กค๋ฐฑ์ ์ํด STOCK_RESTORE๋ฅผ ๋ ๋ฐํํจ. (๋ณต๊ตฌ ์ด๋ฒคํธ #2)
2. ๋ฌธ์ ์ธ์: "์ด๋ฒคํธ๊ฐ 2๋ฒ ๋ ์๊ฐ๋ฉด ์ฌ๊ณ ๊ฐ 2๋ฐฐ๋ก ๋์ด๋๋ ๊ฒ ์๋๊ฐ?" (๋ฉฑ๋ฑ์ฑ ๊ฒฐํจ)
์ฌ์ค ์ด์ ์๋ CodeRabbit ์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ํตํด "์ค๋ณต ์ด๋ฒคํธ๊ฐ ๋ฐํ๋๋ ์ธ๋ฒคํ ๋ฆฌ ์๋น์ค์ ๋ฉฑ๋ฑ์ฑ ์ฒ๋ฆฌ๊ฐ ํ์์ ์ด๋ค"๋ผ๋ ํผ๋๋ฐฑ์ ๋ฐ์์. ๊ทธ๋ ์ธ๋ฒคํ ๋ฆฌ์ ์ด๋ฏธ ProcessedEvent ์ํฐํฐ๋ฅผ ํ์ฉํ ๋ฉฑ๋ฑ์ฑ ๋ฐฉ์ด ๋ก์ง์ด ์์ด์ ์๋ชป๋ ํผ๋๋ฐฑ์ธ ์ค ์์์ผ๋, ๋ค์ ๋ณด๋ ์ค๋ ์๋น์ค์ ์ด๋ฒคํธ ๋ฐํ๋ถ์ ๊ฒฐํจ์ด ์์์.
// [๊ธฐ์กด ์ฝ๋ - OrderCommandService.java]
orderOutboxHelper.append("ORDER", orderId, "STOCK_RESTORE",
// ๋ฌธ์ ์ : ์ด๋ฒคํธ ID๋ฅผ ๋ฌด์กฐ๊ฑด ๋๋ค UUID๋ก ์์ฑํ๊ณ ์์!
new StockRestoreEventPayload(UUID.randomUUID(), item.getOptionId(), item.getQuantity())
);
์ ์ฝ๋์ฒ๋ผ ์ทจ์ํ ๋์๋ ๋๋ค UUID๋ฅผ ์์ฑํ๊ณ , ์น์ธ๋ฐ์ ํ ๋กค๋ฐฑํ ๋์๋ ๋ ๋ค๋ฅธ ๋๋ค UUID๋ฅผ ์์ฑํ๋ฉด, ์ธ๋ฒคํ ๋ฆฌ ์ ์ฅ์์๋ "์๋ก ๋ค๋ฅธ 2๊ฐ์ ๋ ๋ฆฝ์ ์ธ ๋ณต๊ตฌ ์์ฒญ"์ผ๋ก ์ธ์ํ์ฌ ์ฌ๊ณ ๋ฅผ 2๋ฐฐ๋ก ๋๋ ค๋ฒ๋ฆฌ๋ ์ํฉ์ด ๋ฐ์ํจ.
3. 1์ฐจ ํด๊ฒฐ: ๊ฒฐ์ ์ (Deterministic) UUID๋ฅผ ํตํ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ
์ด ๋ฌธ์ ๋ฅผ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํ๊ธฐ ์ํด, ๊ฐ์ ๋น์ฆ๋์ค ํ์(ํน์ ์ฃผ๋ฌธ์ ํน์ ์ต์ ๋ณต๊ตฌ)์ ๋ํด์๋ ํญ์ ๋์ผํ ์ด๋ฒคํธ ID๊ฐ ์์ฑ๋๋๋ก ๊ฒฐ์ ์ (Deterministic) UUID ๋ถ์ฌ ๋ฐฉ์์ผ๋ก ๋ฆฌํฉํฐ๋งํ์.
// [๊ฐ์ ๋ ์ฝ๋ - OrderCommandService.java]
// ํน์ ์ฃผ๋ฌธ๊ณผ ์ต์
์ ๋ํด ํญ์ ๋์ผํ ์ด๋ฒคํธ ID๋ฅผ ๋ณด์ฅํ๋ ์๋ณ์ ์์ฑ
String uniqueKey = "RESTORE_" + order.getId().toString() + "_" + item.getOptionId().toString();
UUID deterministicEventId = UUID.nameUUIDFromBytes(uniqueKey.getBytes());
orderOutboxHelper.append(
"ORDER", order.getId().toString(), "STOCK_RESTORE",
new StockRestoreEventPayload(deterministicEventId, item.getOptionId(), item.getQuantity())
);
๐ก ๊ฒ์ฆ ๋ฐ ์ฑ๊ณผ (์ค์ ์ด์ ๋ก๊ทธ)
ํ ์คํธ ๊ฒฐ๊ณผ, ์ค๋ ์๋น์ค๋ ์๋ฒฝํ ๋์ผํ UUID(7500eaee...)๋ฅผ ๊ฐ์ง ์ด๋ฒคํธ๋ฅผ 2๋ฒ ๋ฐํํ๊ณ , ์ธ๋ฒคํ ๋ฆฌ ์๋น์ค๋ ๋ ๋ฒ์งธ ์ด๋ฒคํธ๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๋ฐฉ์ดํ์.
// [1] ์ค๋ ์๋น์ค: ์ฃ์ง ์ผ์ด์ค ๊ฐ์ง ๋ฐ 2๋ฒ์งธ ๋กค๋ฐฑ ์ด๋ฒคํธ ๋ฐํ
WARN [Order Saga Edge-Case] ์ด๋ฏธ ์ ์ ๊ฐ ์ทจ์ํ ์ฃผ๋ฌธ์ ๋ํด ์น์ธ์ด ๋์ฐฉํ์ต๋๋ค. ์ธ๋ฒคํ ๋ฆฌ ๋กค๋ฐฑ ์ด๋ฒคํธ๋ฅผ ๋ฐํํฉ๋๋ค: 68578038...
// [2] ์ค๋ ์๋น์ค DB (p_order_outbox): ๊ฒฐ์ ์ UUID ์์ฑ ํ์ธ
{"eventId": "7500eaee-df88-3cb9-a9b5-b97dc7d82d84", "optionId": "1a2e7d36...", "quantity": 1}
{"eventId": "7500eaee-df88-3cb9-a9b5-b97dc7d82d84", "optionId": "1a2e7d36...", "quantity": 1}
// [3] ์ธ๋ฒคํ ๋ฆฌ ์๋น์ค: ๋ฉฑ๋ฑ์ฑ ํํฐ ์๋ (๋ ๋ฒ์งธ ์์ฒญ ๋ฐฉ์ด)
INFO [Kafka Consumer] ์ฌ๊ณ ๋ณต๊ตฌ ์ด๋ฒคํธ ์์ : eventId=7500eaee-df88...
WARN [Idempotency] ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์นดํ์นด ๋ฉ์์ง์
๋๋ค. ์ค๋ณต ๋ณต๊ตฌ๋ฅผ ๋ฐฉ์งํ๊ณ ๋ฐํํฉ๋๋ค. eventId=7500eaee...



๊ฒฐ๊ณผ์ ์ผ๋ก ๋์ผํ eventId๋ฅผ ๊ฐ์ง ๋กค๋ฐฑ ๋ฉ์์ง๊ฐ ๋ฐํ๋์๊ณ , ์ธ๋ฒคํ ๋ฆฌ๋ '[Idempotency] ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์นดํ์นด ๋ฉ์์ง์ ๋๋ค.' ๋ผ๋ฉฐ ๋ ๋ฒ์งธ ์์ฒญ์ ์๋ฒฝํ๊ฒ ๋ฐฉ์ดํ์.
์ด๋ฅผ ํตํด At-Least-Once(์ ์ด๋ ํ ๋ฒ) ์ ์ก ๋ณด์ฅ๊ณผ ๋ฉฑ๋ฑ์ฑ์ ๊ฒฐํฉํ์ฌ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ์ง์ผ๋
4. ์๋ก์ด ๋ฐ๊ฒฌ: ๋ถ์ฐ ์์คํ ์ ์ต์ข ๋ณด์ค "์์ ๋ฏธ๋ณด์ฅ(Out-of-Order)"๊ณผ ์ฌ๊ณ ๋์
๋ฉฑ๋ฑ์ฑ์ ๋ณด์ฅํ์์๋ ๋ถ๊ตฌํ๊ณ , DB ๋ฐ์ดํฐ๋ฅผ ๊ต์ฐจ ๊ฒ์ฆํ๋ ๊ณผ์ ์์ ๋ฌธ์ ์ํฉ์ ์ถ๊ฐ๋ก ๋ฐ๊ฒฌํจ. ์ทจ์ ์ฒ๋ฆฌ ํ ํ์ฌ์ ์ผ์ผ ์ฌ๊ณ ๊ฐ 10๊ฐ(์ต๋์น)๋ก ์๋ณต๋์ง ์๊ณ 9๊ฐ๋ก ๋์(Leak) ๋์ด ์์์...

[์์ธ ๋ถ์: ์นดํ์นด ํ ํฝ ๊ฐ ์์ ๋ฏธ๋ณด์ฅ]
- ์ค๋ ์๋น์ค๊ฐ ๊ฑฐ์ ๋์์ ORDER_CREATED (์ฃผ๋ฌธ ํ ํฝ)์ STOCK_RESTORE (๋ณต๊ตฌ ํ ํฝ)๋ฅผ ์นดํ์นด๋ก ๋ฐํํจ.
- ์๋ก ๋ค๋ฅธ ํ ํฝ์ด๋ค ๋ณด๋, ์ธ๋ฒคํ ๋ฆฌ์ ์ปจ์๋จธ๋ค์ด ์ด๋ฅผ ๋์์(ํน์ ์์๊ฐ ์ญ์ ๋์ด) ์๋นํจ.
- ์ฐฐ๋์ ์ฐจ์ด๋ก ๋ณต๊ตฌ(+1)๊ฐ ์ฐจ๊ฐ(-1)๋ณด๋ค ๋จผ์ ์คํ๋จ.
- ์ฌ๊ณ ๊ฐ 10๊ฐ(MAX)์ธ ์ํ์์ ๋ณต๊ตฌ(+1)๊ฐ ๋จผ์ ๋ค์ด์ค๋ ์ํ์ ์ ๋งํ ๊ทธ๋๋ก 10๊ฐ๊ฐ ๋จ.
- ์งํ ์ฐจ๊ฐ(-1) ๋ก์ง์ด ์คํ๋์ด ์ฌ๊ณ ๊ฐ 9๊ฐ๊ฐ ๋จ.
- ์ดํ ์ค๋ ์๋น์ค๊ฐ ์ฃ์ง ์ผ์ด์ค ๋ฐฉ์ด ๋ก์ง์ผ๋ก ์ ๋ ๋ฒ์งธ ๋ณต๊ตฌ(+1) ์ด๋ฒคํธ๊ฐ ๋์ฐฉํ์ง๋ง, ์์ 3๋ฒ ๋จ๊ณ์์ ์์ฑํด ๋ '๋ฉฑ๋ฑ์ฑ ํํฐ'์ ๊ฑธ๋ ค ์ ์์ ์ธ ์ค๋ณต ์์ฒญ์ผ๋ก ๊ฐ์ฃผ๋์ด ๋ฌด์๋นํจ.
- ๊ฒฐ๊ณผ์ ์ผ๋ก ์ฌ๊ณ ๊ฐ 9๊ฐ๋ก ๋์๋จ.
5. ์ต์ข ํด๊ฒฐ ๊ณผ์ : ๋จ์ผ ํ ํฝ ํตํฉ & ํํฐ์ ํค ๋ผ์ฐํ
์ด ํ์์ ๋๊ท๋ชจ ๋ถ์ฐ ์์คํ ์ 'ํ ํฝ ๊ฐ ๋ฉ์์ง ์์ ์ญ์ (Out-of-Order Delivery)' ๋ฌธ์ ์์
1) ๋จ์ผ ์ปค๋งจ๋ ํ ํฝ ํตํฉ (application.yml)
๊ธฐ์กด์ ๋๋์ด ์๋ ์ฐจ๊ฐ/๋ณต๊ตฌ ๋ช ๋ น ํ ํฝ์ inventory.command๋ผ๋ ๋จ์ผ ํ ํฝ์ผ๋ก ํตํฉํจ
2) ํํฐ์ ํค(Partition Key) ์ง์ (OrderCommandService.java)
์นดํ์นด๋ ๋์ผํ Key๋ฅผ ๊ฐ์ง ๋ฉ์์ง๋ฅผ ๋์ผํ ํํฐ์ ์ ์์๋๋ก ๋ฃ์. ์ด๋ฅผ ํ์ฉํ๊ธฐ ์ํด, ๋ฉ์์ง ๋ฐํ ์ Key ๊ฐ์ ๊ธฐ์กด orderId์์ reservationId๋ก ํต์ผํจ. (์ด์ ์ ์์ ํด๋ ORDER_CREATED์ ๋๊ฐ์ ์นดํ์นด ํํฐ์ ์ ๋ค์ด๊ฐ๊ฒ ๋ง๋ค๊ธฐ ์ํด์ ์ ๋ ๊ฒ ๋ฃ์์.)
// ํํฐ์
ํค๋ฅผ reservationId๋ก ํต์ผํ์ฌ In-Order ๋ณด์ฅ
orderOutboxHelper.append(
"ORDER",
order.getReservationId().toString(), // Partition Key ํต์ผ
"STOCK_RESTORE",
payload
);
6. ์ต์ข ๊ฒ์ฆ
ํ ์คํธ ๊ฒฐ๊ณผ, ๋์ผ ์ฃผ๋ฌธ์ ๋ํ ์ฐจ๊ฐ๊ณผ ๋ณต๊ตฌ๊ฐ FIFO ์์๋ก ์ฒ๋ฆฌ๋์์ผ๋ฉฐ ์ฌ๊ณ ๋์๋ ํด๊ฒฐ๋จ.
// [1] In-Order ๋ณด์ฅ (๋ฌด์กฐ๊ฑด ์ฐจ๊ฐ -> ๋ณต๊ตฌ ์์๋ก ์คํ๋จ)
19:22:09.028 [Kafka Consumer] ์ ๊ท ์ฃผ๋ฌธ ์์ฑ ๋ฉ์์ง ์์ -> ์ฌ๊ณ ๋ค์ค ์ฐจ๊ฐ ์๋ (In-Order ์ฒ๋ฆฌ)
19:22:09.102 [Kafka Consumer] ์ฌ๊ณ ๋ณต๊ตฌ ์ด๋ฒคํธ ์์ (In-Order ์ฒ๋ฆฌ)
// [2] ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ (์๊ฐ์ฐจ๋ก ๋ฆ๊ฒ ๋์ฐฉํ 2๋ฒ์งธ ๋ณต๊ตฌ ์์ฒญ ์ฐจ๋จ)
19:22:13.986 [Idempotency] ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์นดํ์นด ๋ฉ์์ง์
๋๋ค. ์ค๋ณต ๋ณต๊ตฌ๋ฅผ ๋ฐฉ์งํ๊ณ ๋ฐํํฉ๋๋ค.
