docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
---
|
||||
id: messaging-exactly-once
|
||||
title: Exactly-once 의미 — Idempotency + Outbox + Dedup
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [messaging, exactly-once, idempotency, vibe-coding]
|
||||
tech_stack: { language: "TS / Kafka / SQL", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [exactly once, EOS, transactional producer, dedupe, effectively-once]
|
||||
---
|
||||
|
||||
# Exactly-once
|
||||
|
||||
> 분산 시스템에서 진정한 exactly-once 는 거의 없다. 실용적 = **at-least-once + idempotent consumer = effectively-once**. Kafka transactional 은 read-process-write 안에서만.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- At-most-once: 잃을 수 있음.
|
||||
- At-least-once: 중복 가능.
|
||||
- Exactly-once: 정확히 1번 — 분산 boundary 까지는 어렵.
|
||||
- Effectively-once = at-least-once + idempotent.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Idempotent consumer (가장 표준)
|
||||
```sql
|
||||
CREATE TABLE processed (
|
||||
event_id TEXT PRIMARY KEY,
|
||||
processed_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
```ts
|
||||
async function consume(msg: Message) {
|
||||
const id = msg.headers['event-id'];
|
||||
return db.transaction(async (tx) => {
|
||||
try {
|
||||
await tx.processed.insert({ eventId: id }); // unique violation = 이미 처리
|
||||
} catch (e) {
|
||||
if (isUniqueViolation(e)) return; // skip
|
||||
throw e;
|
||||
}
|
||||
await handleInTx(tx, msg.body);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Producer idempotency (Kafka)
|
||||
```ts
|
||||
const producer = kafka.producer({
|
||||
idempotent: true,
|
||||
maxInFlightRequests: 5, // <= 5 권장
|
||||
retry: { retries: 10 },
|
||||
});
|
||||
// Kafka 가 자동 PID + sequence number 로 중복 제거 (single producer)
|
||||
```
|
||||
|
||||
### Transactional producer (Kafka, read-process-write)
|
||||
```ts
|
||||
const producer = kafka.producer({ transactionalId: 'projector-1', idempotent: true });
|
||||
const consumer = kafka.consumer({ groupId: 'projector', readUncommitted: false });
|
||||
|
||||
await producer.connect();
|
||||
const tx = await producer.transaction();
|
||||
try {
|
||||
await tx.send({ topic: 'output', messages: [...] });
|
||||
await tx.sendOffsets({ consumerGroupId: 'projector', topics: [{ topic: 'input', partitions: [{ partition: 0, offset: '42' }] }] });
|
||||
await tx.commit();
|
||||
} catch {
|
||||
await tx.abort();
|
||||
}
|
||||
```
|
||||
|
||||
EOS 안 = consumer offset 과 produced messages 가 한 트랜잭션.
|
||||
|
||||
### Outbox + processed-table 조합
|
||||
```
|
||||
1. Producer: outbox 안에 같이 commit (atomic)
|
||||
2. Publisher: outbox → broker (at-least-once)
|
||||
3. Consumer: processed table 검사 + handle in tx
|
||||
```
|
||||
|
||||
이게 분산 시스템에서 정직한 exactly-once.
|
||||
|
||||
### 외부 부수효과 — TX 후
|
||||
```ts
|
||||
// ❌ TX 안 외부 API: TX 롤백 시 부수효과만 남음
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.orders.insert(o);
|
||||
await stripe.charges.create(...); // 안 됨
|
||||
});
|
||||
|
||||
// ✅ outbox + 후속 처리
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.orders.insert(o);
|
||||
await tx.outbox.insert({ type: 'ChargeRequested', ... });
|
||||
});
|
||||
// publisher 가 별도로 stripe 호출 + idempotencyKey
|
||||
```
|
||||
|
||||
### Dedupe 시간 윈도우 (가벼운)
|
||||
```ts
|
||||
const recent = new LRU<string, true>({ max: 100_000, ttl: 5 * 60_000 });
|
||||
|
||||
async function consume(msg) {
|
||||
if (recent.has(msg.id)) return;
|
||||
recent.set(msg.id, true);
|
||||
await handle(msg);
|
||||
}
|
||||
```
|
||||
|
||||
분산 환경에선 Redis 같은 외부 store 필요.
|
||||
|
||||
### Once-and-only-once 가짜 / 진짜
|
||||
가짜 (claim 만): SQS FIFO `MessageDeduplicationId` 5분 / Kafka idempotent producer (single producer).
|
||||
진짜에 가까움: Kafka transactional EOS + processed table.
|
||||
완벽 X: 외부 시스템 boundary (HTTP, email).
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 보장 |
|
||||
|---|---|
|
||||
| Read → process → write Kafka 안 | Kafka EOS transactional |
|
||||
| DB write + 외부 API | Outbox + idempotency key |
|
||||
| 단순 큐 처리 | Idempotent consumer (processed table) |
|
||||
| Stream aggregations | Kafka Streams EOS |
|
||||
| 일회성 알림 | At-least-once + idempotent |
|
||||
| 정확히 한 번 진짜 | 보통 불가 — effectively-once 디자인 |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **EOS 라고 가정 + 외부 부수효과**: 깨짐.
|
||||
- **Idempotency key 없는 dedupe**: 같은 의미 다른 hash 통과.
|
||||
- **Processed table 없음**: 중복 처리.
|
||||
- **Producer idempotent 만 + multiple producer**: producer 간 중복.
|
||||
- **Manual offset commit + before processing**: 메시지 잃음.
|
||||
- **SQS visibility timeout < 처리 시간**: 같은 메시지 두 consumer.
|
||||
- **Distributed lock 으로 dedupe**: 복잡 + 실패 모드.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- 진짜 exactly-once 거의 없음 — effectively-once 디자인.
|
||||
- Idempotent consumer + outbox 가 표준.
|
||||
- Kafka EOS 는 read-process-write 안에서만.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_Idempotency_Keys]]
|
||||
- [[Backend_Outbox_Pattern]]
|
||||
- [[Messaging_Kafka_Patterns]]
|
||||
Reference in New Issue
Block a user