codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›PostgreSQL 깊게 다루기 + Redis · Kafka›7단계

7단계

데이터 파이프라인 — 재시도 · 멱등

0회 조회

데이터 파이프라인 — 재시도 · 멱등

외부 API 호출 · 배치 · 큐 소비. 네트워크 · 장애 · 중복 메시지는 일상. 멱등 (idempotent) 으로 짜면 재시도가 안전합니다.

1. Exactly-once 의 환상

분산 시스템에서 exactly-once 는 원칙적으로 불가능. at-least-once + 멱등 소비자가 실제 해법.

Producer → Kafka → Consumer
           (at-least-once 전달)
                    ↓
              (멱등 처리로 중복 흡수)

2. 자연 키 멱등

CREATE TABLE payments (
  id BIGSERIAL PRIMARY KEY,
  idempotency_key TEXT NOT NULL UNIQUE,   -- 클라이언트 또는 producer 가 발급
  amount DECIMAL NOT NULL,
  status VARCHAR NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

INSERT INTO payments (idempotency_key, amount, status)
VALUES ($1, $2, 'pending')
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING *;

UNIQUE + ON CONFLICT = 두 번 실행해도 한 번만 기록.

3. 상태 전이 멱등

UPDATE orders SET status = 'paid', paid_at = now()
WHERE id = $1 AND status = 'pending';

이미 paid 면 0 rows 업데이트. 재실행 안전.

4. 재시도 패턴

async function withRetry<T>(fn: () => Promise<T>, max = 3): Promise<T> {
  for (let i = 0; i < max; i++) {
    try { return await fn(); }
    catch (e) {
      if (i === max - 1) throw e;
      const wait = 2 ** i * 1000 + Math.random() * 500;   // exp + jitter
      await new Promise(r => setTimeout(r, wait));
    }
  }
  throw new Error("unreachable");
}
  • exponential backoff — 1s · 2s · 4s
  • jitter — 동시 다량 재시도로 재난 방지

5. Circuit Breaker

연속 실패 시 잠시 차단 → 서비스 회복 시간 확보.

let consecutiveFailures = 0;
let openedAt: number | null = null;
const THRESHOLD = 5, COOLDOWN_MS = 30_000;

async function call<T>(fn: () => Promise<T>): Promise<T> {
  if (openedAt && Date.now() - openedAt < COOLDOWN_MS) {
    throw new Error("circuit_open");
  }
  try {
    const r = await fn();
    consecutiveFailures = 0; openedAt = null;
    return r;
  } catch (e) {
    consecutiveFailures++;
    if (consecutiveFailures >= THRESHOLD) openedAt = Date.now();
    throw e;
  }
}

6. Outbox 패턴

트랜잭션 커밋과 이벤트 발행을 원자적으로.

BEGIN;
  UPDATE users SET verified = true WHERE id = $1;
  INSERT INTO outbox_events (type, payload)
  VALUES ('user.verified', jsonb_build_object('userId', $1));
COMMIT;

별도 워커가 outbox_events 를 폴링 → Kafka 발행 → 성공 시 DELETE.

DB 쓰기와 이벤트 발행이 같은 트랜잭션. "DB 는 커밋됐는데 이벤트 발행 실패" 회피.

7. Saga 패턴

여러 서비스 트랜잭션. 한 단계 실패 시 앞 단계 보상.

주문 접수 → 재고 차감 → 결제 → 배송
                      ↓ 실패
                   재고 복구 (보상)

각 단계의 보상 트랜잭션 미리 설계. 단순한 2-3 단계는 saga 오버엔지니어링.

8. dead-letter queue

3 번 재시도 실패 메시지를 DLQ 로 이동. 운영자 수동 확인.

original-topic → retry-topic → dead-letter-topic

DLQ 에 쌓이면 알림 · 조사.

9. 배치 처리 멱등

# 배치 시작 전 체크포인트
last_processed = db.fetchval("SELECT MAX(id) FROM events WHERE processed")

rows = db.fetch("SELECT * FROM events WHERE id > $1 ORDER BY id LIMIT 1000", last_processed)
for row in rows:
    process(row)
    db.execute("UPDATE events SET processed = true WHERE id = $1", row.id)

중간 실패 후 재실행해도 last_processed 부터.

10. 자주 걸리는 자리

  • idempotency_key 없음 — 중복 과금 · 중복 알림
  • backoff 없는 재시도 — thundering herd
  • 트랜잭션 밖 이벤트 발행 — DB 롤백 후에도 이벤트 나감
  • at-most-once 전제 — producer 실패 시 이벤트 누락

11. 관찰

  • 재시도 횟수 · 성공률 대시보드
  • DLQ 사이즈 알람
  • p95 · p99 latency

숫자 없이 튜닝 안 됨.

하고픈 말

"재시도 가능한 코드 = 멱등한 코드" 라는 규칙만 새기고 설계. 대부분의 분산 시스템 문제가 이 규칙 하나로 해결됩니다.

Next

  • 08-backup-restore

← 6단계

Kafka — 언제 · 언제 아닌지

8단계 →

백업 · 복원 리허설