신뢰할 수 있는 API 통합을 위한 PostgreSQL의 아웃박스 패턴
PostgreSQL에서 이벤트를 저장한 뒤 재시도·순서·중복 제거를 적용해 서드파티 API로 안전하게 전달하는 outbox 패턴을 배워보세요.

앱은 정상인데 통합이 실패하는 이유
앱에서는 동작이 "성공"으로 보이지만, 그 뒤의 통합이 조용히 실패하는 경우가 흔합니다. 데이터베이스 쓰기는 빠르고 신뢰할 만하지만, 서드파티 API 호출은 그렇지 않을 수 있습니다. 그 결과 두 개의 다른 세계가 생깁니다: 시스템은 변경이 일어났다고 하지만 외부 시스템은 그 사실을 전혀 알지 못합니다.
일반적인 예: 고객이 주문을 하고 앱이 PostgreSQL에 주문을 저장한 뒤 배송 제공자에게 알리려고 시도합니다. 제공자가 20초 동안 응답하지 않고 요청이 포기되면 주문은 실제로 존재하지만 배송은 생성되지 않습니다.
사용자는 이를 혼란스럽고 일관성 없는 동작으로 경험합니다. 이벤트가 누락되면 "아무 일도 일어나지 않았다"고 보이고, 이벤트가 중복되면 "왜 결제가 두 번 되었나" 같은 문제가 생깁니다. 지원팀도 문제가 앱인지, 네트워크인지, 파트너인지 판단하기 어렵습니다.
재시도는 도움이 되지만, 재시도만으로 정합성이 보장되진 않습니다. 타임아웃 후 재시도하면 파트너가 첫 요청을 받았는지 알 수 없어 같은 이벤트를 두 번 보낼 수 있습니다. 순서가 바뀌어 "Order shipped"가 "Order paid"보다 먼저 도착할 수도 있습니다.
이 문제들은 대개 정상적인 동시성에서 옵니다: 여러 워커의 병렬 처리, 여러 앱 서버의 동시 쓰기, 부하에 따라 타이밍이 변하는 "최선의 노력" 큐. 실패 모드는 예측 가능합니다: API가 느려지거나 다운되고, 네트워크가 요청을 잃어버리고, 프로세스가 잘못된 순간에 크래시하며, 재시도가 멱등성을 강제하지 않으면 중복을 만듭니다.
이런 실패가 정상적이기 때문에 outbox 패턴이 존재합니다.
outbox 패턴을 쉽게 설명하면
outbox 패턴은 간단합니다: 앱에서 중요한 변경을 만들 때(예: 주문 생성) 같은 트랜잭션 안에 작은 "보낼 이벤트" 레코드를 데이터베이스 테이블에 함께 씁니다. 데이터베이스 커밋이 성공하면 비즈니스 데이터와 이벤트 레코드가 함께 존재함을 알 수 있습니다.
그 뒤 별도의 워커가 outbox 테이블을 읽어 그 이벤트들을 서드파티 API로 전달합니다. API가 느리거나 다운되거나 타임아웃이 나도, 메인 사용자 요청은 외부 호출을 기다리지 않기 때문에 여전히 성공합니다.
이 방법은 요청 핸들러 안에서 API를 호출할 때 생기는 어색한 상태들을 피합니다:
- 주문은 저장됐지만 API 호출이 실패했다.
- API 호출은 성공했지만 앱이 주문을 저장하기 전에 크래시했다.
- 사용자가 재시도해서 같은 내용을 두 번 보냈다.
outbox 패턴은 주로 누락된 이벤트, 부분 실패(데이터베이스는 OK지만 외부 API는 실패), 우발적 중복 전송, 그리고 추측 없이 안전하게 재시도할 수 있게 해주는 점에서 도움이 됩니다.
모든 문제를 해결하진 않습니다. 페이로드가 잘못됐거나 비즈니스 규칙이 틀렸거나 서드파티 API가 데이터를 거부하면 여전히 검증, 적절한 오류 처리, 실패한 이벤트를 점검하고 수정하는 방법이 필요합니다.
PostgreSQL에서 outbox 테이블 설계하기
좋은 outbox 테이블은 일부러 단순해야 합니다. 쓰기 쉽고, 읽기 쉽고, 오용하기 어려워야 합니다.
다음은 실무에서 적용 가능한 기본 스키마입니다:
create table outbox_events (
id bigserial primary key,
aggregate_id text not null,
event_type text not null,
payload jsonb not null,
status text not null default 'pending',
created_at timestamptz not null default now(),
available_at timestamptz not null default now(),
attempts int not null default 0,
locked_at timestamptz,
locked_by text,
meta jsonb not null default '{}'::jsonb
);
ID 선택
bigserial(또는 bigint)을 쓰면 정렬이 단순하고 인덱스가 빠릅니다. UUID는 시스템 간 고유성에 좋지만 생성 순서대로 정렬되지 않아 폴링이 덜 예측 가능하고 인덱스 비용이 더 클 수 있습니다.
일반적인 절충안은: id는 정렬용으로 bigint로 유지하고, 시스템 간에 공유할 안정적인 식별자가 필요하면 별도의 event_uuid를 추가하는 것입니다.
중요한 인덱스
워커는 같은 패턴의 쿼리를 계속 실행합니다. 대부분 시스템에는 다음 인덱스가 필요합니다:
- 다음 보낼 이벤트를 순서대로 가져오기 위한
(status, available_at, id)같은 인덱스 - 오래된 잠금을 만료시키려면
(locked_at)인덱스 - 가끔 aggregate별로 전달할 때
(aggregate_id, id)인덱스
페이로드는 안정적으로 유지
페이로드는 작고 예측 가능하게 유지하세요. 수신자가 실제로 필요로 하는 것만 저장하고 전체 행을 넣지 마세요. meta에 명시적 버전을 추가하면 필드를 안전하게 확장할 수 있습니다.
meta에는 테넌트 ID, 상관관계 ID(correlation ID), 트레이스 ID, 중복 제거 키 같은 라우팅 및 디버깅 컨텍스트를 넣으세요. 이런 추가 컨텍스트는 나중에 "이 주문에 무슨 일이 있었나?"를 답할 때 큰 도움이 됩니다.
비즈니스 쓰기와 함께 이벤트를 안전하게 저장하는 방법
가장 중요한 규칙은 간단합니다: 비즈니스 데이터와 outbox 이벤트를 같은 데이터베이스 트랜잭션에서 쓰세요. 트랜잭션이 커밋되면 둘 다 존재합니다. 롤백되면 둘 다 존재하지 않습니다.
예: 고객이 주문을 합니다. 하나의 트랜잭션에서 주문 행, 주문 항목들, 그리고 order.created 같은 outbox 행을 삽입하세요. 어느 단계가 실패하면 "생성됨" 이벤트가 외부로 유출되는 일이 없어야 합니다.
하나의 이벤트 vs 여러 이벤트
가능하면 비즈니스 액션 당 한 이벤트로 시작하세요. 추론하기 쉽고 처리 비용도 적습니다. 서로 다른 소비자가 실제로 다른 타이밍이나 페이로드를 필요로 할 때만 여러 이벤트로 쪼개세요(예: 이행용 order.created와 청구용 payment.requested). 한 번의 클릭으로 많은 이벤트를 생성하면 재시도 수, 순서 문제, 중복 처리 부담이 늘어납니다.
어떤 페이로드를 저장해야 할까?
보통 다음 중에서 선택합니다:
- 스냅샷: 액션 시점의 핵심 필드를 저장(주문 총액, 통화, 고객 ID 등). 나중에 추가 조회를 피하고 메시지를 안정적으로 유지합니다.
- 참조 ID: 주문 ID만 저장하고 워커가 나중에 상세를 조회하게 함. outbox를 작게 유지하지만 추가 조회가 필요하고 주문이 수정되면 달라질 수 있습니다.
실용적인 중간 지점은 식별자들과 함께 주요 값의 작은 스냅샷을 저장하는 것입니다. 수신자가 빠르게 처리할 수 있고 디버깅에도 도움이 됩니다.
트랜잭션 경계는 최대한 짧게 유지하세요. 같은 트랜잭션 안에서 서드파티 API를 호출하지 마십시오.
서드파티 API로 이벤트 전달하기: 워커 루프
이벤트가 outbox에 들어가면 이를 읽어 서드파티 API를 호출하는 워커가 필요합니다. 이 부분이 패턴을 신뢰 가능한 통합으로 바꿉니다.
폴링이 보통 가장 단순한 옵션입니다. LISTEN/NOTIFY는 지연을 줄일 수 있지만 구성 요소가 늘어나고 알림이 누락되거나 워커가 재시작될 때 대비가 필요합니다. 대부분 팀은 작은 배치로 안정적으로 폴링하는 것이 운영과 디버깅 면에서 쉽습니다.
행을 안전하게 점유하기
워커는 두 워커가 같은 이벤트를 동시에 처리하지 않도록 행을 점유해야 합니다. PostgreSQL에서는 로우 잠금과 SKIP LOCKED를 사용해 배치를 선택한 뒤 처리 중으로 표시하는 것이 일반적입니다.
실무에서의 상태 흐름 예시는 다음과 같습니다:
pending: 전송 준비 완료processing: 워커가 점유(이때locked_by와locked_at사용)sent: 성공적으로 전달됨failed: 최대 시도 후 중지(또는 수동 검토용으로 분리)
데이터베이스에 부담을 주지 않도록 배치 크기를 작게 유지하세요. 보통 10100행 배치로 15초마다 실행하는 것이 출발점으로 적당합니다.
호출이 성공하면 행을 sent로 표시하세요. 실패하면 attempts를 증가시키고 available_at을 백오프에 따라 미래 시점으로 설정한 뒤 잠금을 해제하고 다시 pending으로 돌립니다.
비밀을 누설하지 않는 로그
좋은 로그는 실패를 조치 가능하게 만듭니다. outbox의 id, 이벤트 타입, 대상 이름, 시도 횟수, 소요 시간, HTTP 상태나 오류 종류를 기록하세요. 요청 본문, 인증 헤더, 전체 응답 등 민감한 내용은 기록하지 마세요. 연관성을 위해 안전한 요청 ID나 해시를 저장하세요.
현실적인 시스템에서 통용되는 순서 규칙
많은 팀이 "우리가 생성한 순서대로 이벤트를 보낸다"로 시작합니다. 문제는 "같은 순서"가 전역적일 경우가 거의 없다는 점입니다. 전역 큐로 강제하면 한 느린 고객이나 불안정한 API가 모두를 가로막을 수 있습니다.
실용적인 규칙은: 전체 시스템 순서가 아닌 그룹별 순서를 보존하세요. 외부 세계가 데이터에 대해 생각하는 방식과 일치하는 그룹 키(예: customer_id, account_id, 또는 order_id 같은 aggregate_id)를 선택한 뒤 각 그룹 내에서 순서를 보장하되 여러 그룹은 병렬로 처리하세요.
순서를 깨지 않으면서 병렬 워커 운영하기
여러 워커를 운영하되 같은 그룹을 두 워커가 동시에 처리하지 않도록 하세요. 보통의 접근법은 주어진 aggregate_id에 대해 가장 이른 미전송 이벤트만 전달하게 하고, 서로 다른 aggregate 간에는 병렬성을 허용하는 것입니다.
점유 규칙을 단순하게 유지하세요:
- 그룹별로 가장 이른 pending 이벤트만 전달
- 그룹 간 병렬성 허용, 그룹 내에서는 병렬 금지
- 한 번에 하나의 이벤트를 점유·전송·상태 업데이트 후 다음으로 이동
어떤 이벤트가 나머지를 막을 때
언젠가 "poison" 이벤트가 몇 시간 동안 실패하는 경우가 생깁니다(잘못된 페이로드, 토큰 만료, 제공자 장애 등). 그룹별 순서를 엄격히 지키면 그 그룹의 이후 이벤트들은 기다려야 하지만, 다른 그룹은 계속 진행해야 합니다.
실용적인 절충안은 이벤트별 재시도 상한을 두는 것입니다. 상한에 도달하면 failed로 표시하고 해당 그룹만 중지시켜 사람의 개입을 기다리세요. 이렇게 하면 한 고객의 문제로 전체가 느려지는 것을 막을 수 있습니다.
상황을 악화시키지 않는 재시도
재시도는 outbox 설정을 신뢰 가능하게 만들지 아니면 소음으로 가득 채울 수 있는 부분입니다. 목표는 단순합니다: 성공할 가능성이 있을 때 다시 시도하고, 성공 가능성이 낮을 때는 빨리 멈추는 것입니다.
지수적 백오프와 하드 캡을 사용하세요. 예: 1분, 2분, 4분, 8분 후 재시도하고 그다음에는 중단(또는 최대 지연 시간을 15분으로 제한) 같은 방식입니다. 하나의 잘못된 이벤트가 시스템을 영원히 막지 않도록 시도 횟수의 최대치를 항상 설정하세요.
모든 실패를 재시도해서는 안 됩니다. 규칙을 명확히 하세요:
- 재시도: 네트워크 타임아웃, 연결 재설정, DNS 문제, HTTP 429 또는 5xx 응답
- 재시도하지 않음: HTTP 400(잘못된 요청), 401/403(인증 문제), 404(잘못된 엔드포인트), 보내기 전에 감지할 수 있는 검증 오류
재시도 상태는 outbox 행에 저장하세요. attempts를 증가시키고, 다음 시도를 위한 available_at을 설정하며, 짧고 안전한 오류 요약(상태 코드, 오류 종류, 간단한 메시지)을 기록하세요. 오류 필드에 전체 페이로드나 민감한 데이터를 저장하지 마세요.
레이트 리미트는 특별히 처리해야 합니다. HTTP 429를 받으면 Retry-After 헤더가 있을 때는 이를 존중하세요. 없으면 재시도를 더 공격적으로 백오프해서 재시도 폭주를 피하세요.
중복 제거와 멱등성 기본
신뢰성 있는 API 통합을 만든다면 같은 이벤트가 두 번 전송될 수 있음을 항상 가정하세요. 워커가 HTTP 호출 후 성공 기록을 남기기 전에 크래시할 수 있고, 타임아웃으로 성공을 알지 못할 수 있으며, 재시도가 느린 첫 시도와 겹칠 수 있습니다. outbox 패턴은 누락된 이벤트를 줄이지만 그 자체로 중복을 막지는 않습니다.
가장 안전한 접근법은 멱등성입니다: 반복 전송이 한 번 전송과 같은 결과를 만들게 하세요. 서드파티 API를 호출할 때 해당 이벤트와 대상에 대해 안정적인 멱등 키를 포함하세요. 많은 API는 헤더를 지원하고, 지원하지 않으면 본문에 키를 넣으세요.
간단한 키는 대상과 이벤트 ID를 합친 것입니다. 예: 이벤트 ID가 evt_123이면 항상 destA:evt_123 같은 키를 사용하세요.
자신 쪽에서는 전달 로그를 유지하고 (destination, event_id) 같은 고유 규칙을 적용해 중복 전송을 방지하세요. 두 워커가 경합하더라도 "이것을 보내고 있다"는 기록을 하나만 만들 수 있도록 하면 됩니다.
웹훅도 중복될 수 있음
웹훅 콜백(예: "전달 확인" 또는 "상태 업데이트")을 받으면 동일하게 처리하세요. 제공자는 재시도하고 같은 페이로드를 여러 번 보낼 수 있습니다. 처리된 웹훅 ID를 저장하거나 제공자의 메시지 ID에서 안정적인 해시를 만들어 중복을 거부하세요.
데이터를 얼마나 오래 보관할까
성공(또는 수용 가능한 최종 실패)을 기록할 때까지 outbox 행을 보관하세요. 전달 로그는 누가 "우리가 보냈는가?"를 물을 때 감사 기록이 되므로 더 오래 보관하세요.
일반적인 접근법:
- Outbox 행: 성공 후 짧은 안전 기간(며칠)을 두고 삭제하거나 보관
- 전달 로그: 규정 준수 및 지원 필요에 따라 몇 주 또는 몇 달 보관
- 멱등성 키: 재시도가 발생할 수 있는 기간만큼(그리고 웹훅 중복을 고려해 더 길게) 보관
단계별: outbox 패턴 구현하기
어떤 것을 발행할지 결정하세요. 이벤트는 작고 집중적이며 나중에 재실행하기 쉬워야 합니다. 좋은 규칙은 이벤트 당 하나의 비즈니스 사실을 전달하고 수신자가 처리하는 데 충분한 데이터를 포함하는 것입니다.
기본 구성 만들기
명확한 이벤트 이름을 선택하세요(예: order.created, order.paid) 그리고 페이로드 스키마에 버전을 매기세요(예: v1, v2). 버전 관리는 나중에 필드를 추가해도 오래된 소비자를 깨뜨리지 않게 해줍니다.
PostgreSQL outbox 테이블을 만들고 워커 쿼리에 중요한 인덱스(특히 (status, available_at, id))를 추가하세요.
비즈니스 변경과 outbox 삽입이 같은 데이터베이스 트랜잭션에서 일어나도록 쓰기 흐름을 업데이트하세요. 이것이 핵심 보장입니다.
전달과 제어 추가
간단한 구현 계획:
- 장기적으로 지원할 이벤트 타입과 페이로드 버전을 정의
- outbox 테이블과 인덱스 생성
- 주요 데이터 변경과 함께 outbox 행 삽입
- 행을 점유하고 서드파티 API로 전송한 뒤 상태를 업데이트하는 워커 빌드
- 백오프를 사용하는 재시도 스케줄과 시도 종료 후
failed상태 추가
기본 지표를 추가해 문제를 조기에 발견하세요: 지연(lag, 가장 오래된 미전송 이벤트의 나이), 전송률, 실패율 등.
간단한 예: 주문 이벤트를 외부 서비스로 보내기
고객이 앱에서 주문합니다. 앱 밖에서 일어나야 할 두 가지는: 결제 제공자가 카드에 청구하고, 배송 제공자가 배송을 생성하는 것입니다.
outbox 패턴을 쓰면 체크아웃 요청 안에서 그런 API들을 호출하지 않습니다. 대신 주문과 outbox 이벤트를 같은 PostgreSQL 트랜잭션에 저장해 "주문은 저장됐지만 알림이 전송되지 않았다"(또는 반대)의 상태가 생기지 않게 합니다.
일반적인 outbox 행은 aggregate_id(주문 ID), event_type(order.created 같은), 그리고 총액, 항목, 배송지 정보를 담은 JSONB 페이로드를 포함할 수 있습니다.
워커가 보류 중인 행을 가져와 외부 서비스를 호출합니다(정해진 순서로 호출하거나 payment.requested, shipment.requested 같은 별도 이벤트를 내보낼 수 있음). 한 제공자가 다운되면 워커는 시도를 기록하고 available_at을 미래로 밀어 다음 시도를 예약한 뒤 계속 진행합니다. 주문은 여전히 존재하고 이벤트는 차후 재시도되어 새로운 체크아웃을 막지 않습니다.
순서는 보통 "주문별" 또는 "고객별"입니다. 같은 aggregate_id를 가진 이벤트는 한 번에 하나씩 처리되도록 하여 order.paid가 order.created보다 먼저 도착하지 않게 하세요.
중복 제거는 두 번 청구되거나 두 번 배송이 생성되는 일을 막아줍니다. 서드파티가 지원하면 멱등성 키를 보내고, 재시도 후 타임아웃이 있더라도 두 번째 행동이 발생하지 않도록 대상별 전달 기록을 유지하세요.
배포 전 빠른 점검 목록
금전 이동, 고객 알림, 데이터 동기화 같은 통합을 신뢰하려면 가장자리 케이스를 테스트하세요: 크래시, 재시도, 중복, 여러 워커 상황.
자주 잡아내는 체크:
- 비즈니스 변경과 같은 트랜잭션에서 outbox 행이 생성되는지 확인
- 전송자는 여러 인스턴스에서 안전하게 실행 가능한지 검증. 두 워커가 같은 이벤트를 동시에 전송하면 안 됩니다.
- 순서가 중요하면 한 문장으로 규칙을 정의하고 안정적인 키로 강제
- 각 대상마다 중복을 어떻게 막고 "전송했다"를 어떻게 증명할지 결정
- N번 시도 후 이벤트를
failed로 이동시키고 마지막 오류 요약을 보관하며 간단한 재처리 수단을 제공하는 종료 조건 정의
현실적인 예: Stripe가 요청을 받아들였지만 워커가 성공을 기록하기 전에 크래시하면 멱등성이 없으면 재시도가 중복 행동을 유발할 수 있습니다. 멱등성과 전달 기록을 함께 쓰면 재시도는 안전해집니다.
다음 단계: 앱을 방해하지 않고 롤아웃하기
롤아웃은 outbox 프로젝트가 성공하느냐 멈추느냐를 좌우합니다. 처음에는 작게 시작해 전체 통합 계층을 위험에 빠뜨리지 않고 실제 동작을 관찰하세요.
한 통합과 한 이벤트 타입으로 시작하세요. 예: order.created만 하나의 벤더 API로 보내고 나머지는 그대로 두는 방식. 이렇게 하면 처리량, 지연, 실패율에 대한 깨끗한 기준선을 얻을 수 있습니다.
문제를 조기에 가시화하세요. outbox 지연(대기 중 이벤트 수와 가장 오래된 이벤트의 나이)과 실패율(재시도 중인 수)을 위한 대시보드와 알림을 추가하세요. "지금 우리가 뒤처져 있나?"를 10초 내에 답할 수 있으면 사용자가 문제를 느끼기 전에 잡을 수 있습니다.
첫 인시던트 전에 안전한 재처리 계획을 마련하세요. "재처리"가 무엇을 의미하는지 결정하세요: 같은 페이로드로 재시도할지, 현재 데이터로 페이로드를 재구축할지, 수동 검토로 보낼지. 어떤 경우에 다시 보내는 것이 안전하고 어떤 경우 사람이 확인해야 하는지 문서화하세요.
no-code 플랫폼인 AppMaster (appmaster.io) 같은 도구로 이걸 만든다면 동일한 원칙이 적용됩니다: 비즈니스 데이터와 outbox 행을 PostgreSQL에 함께 쓰고, 별도의 백엔드 프로세스로 전달, 재시도, 전송 또는 실패 표시를 수행하세요.
자주 묻는 질문
사용자 작업이 데이터베이스를 업데이트하고 다른 시스템에서 처리되어야 할 때 outbox 패턴을 사용하세요. 타임아웃, 불안정한 네트워크, 또는 서드파티 장애로 인해 “우리 앱에는 저장됐지만 상대 시스템에는 전달되지 않았다”는 상황이 발생할 수 있을 때 특히 유용합니다.
비즈니스 행위(예: 주문 생성)와 outbox 행위를 같은 데이터베이스 트랜잭션 안에서 쓰면 하나의 명확한 보장이 생깁니다. 즉 둘 다 커밋되거나 둘 다 롤백됩니다. 이는 “API 호출은 성공했는데 주문이 저장되지 않았다” 또는 “주문은 저장됐지만 API 호출이 실패했다” 같은 부분 실패를 막아줍니다.
기본적으로 포함하면 좋은 필드는 id, aggregate_id, event_type, payload, status, created_at, available_at, attempts와 같은 재시도와 동시성 처리를 돕는 locked_at, locked_by 등입니다. 이렇게 하면 전송, 재시도 스케줄링, 안전한 동시성 제어가 단순해집니다.
일반적인 기준은 (status, available_at, id) 인덱스입니다. 이는 워커가 보낼 수 있는 다음 이벤트 배치를 빠르게 조회하게 해줍니다. 실제로 자주 조회할 때만 추가 인덱스를 더하세요. 인덱스는 삽입 성능을 떨어뜨립니다.
대부분 팀에는 폴링이 가장 단순하고 예측 가능한 접근법입니다. 작은 배치와 짧은 간격으로 시작한 뒤 부하와 지연에 따라 조정하세요. LISTEN/NOTIFY는 지연을 줄일 수 있지만 추가적인 복잡성이 있고, 알림이 누락되거나 워커가 재시작될 때 대비가 필요합니다.
행 수준 잠금을 이용해 두 워커가 같은 이벤트를 동시에 처리하지 못하게 하세요. 일반적으로 SKIP LOCKED를 사용해 배치를 선택한 다음, processing 상태로 표시하고 locked_at, locked_by를 설정해 전송 후 sent로 업데이트하거나 실패하면 잠금을 해제하고 available_at을 미래로 설정해 재시도 대기 상태로 돌립니다.
지수적 백오프와 시도 횟수의 상한을 두세요. 그리고 일시적일 가능성이 높은 오류만 재시도 대상으로 삼으세요. 예: 타임아웃, 네트워크 오류, HTTP 429 또는 5xx는 재시도, 대부분의 4xx(검증 오류, 401/403 등)는 재시도하지 않는 것이 안전합니다.
완전한 한 번 전송만을 보장할 수는 없다고 가정하세요. 워커가 HTTP 호출 후 성공 기록을 남기기 전에 크래시할 수 있고, 타임아웃으로 성공 여부를 모를 수 있습니다. 각 대상과 이벤트에 대해 안정적인 멱등 키를 포함하고, (destination, event_id) 같은 유니크 제약을 가진 전달 로그를 보관하면 중복 실행을 예방할 수 있습니다.
전체 시스템이 아닌 그룹 단위로 순서를 보존하세요. aggregate_id(예: 주문 ID)나 customer_id 같은 그룹 키를 사용해 그룹 내에서는 순서를 보장하되, 서로 다른 그룹은 병렬 처리하도록 하면 한 사용자의 느린 동작이 전체를 막지 않습니다.
한 이벤트가 수시간 동안 계속 실패하면 해당 이벤트를 N번 시도 후 failed로 표시하고, 같은 그룹의 이후 이벤트 처리는 중지한 뒤 사람이 원인 수정할 때까지 대기시키세요. 이렇게 하면 한 고객의 문제로 전체가 멈추는 것을 막을 수 있습니다.


