쿠폰 발행에 3초 타임아웃을 둔다고 하자. 파트너가 2.8초에 쿠폰을 만들었는데 응답을 잃으면 호출자는 3초 뒤 실패처럼 보이는 신호를 받는다. 새 요청 ID로 다시 발행하면 쿠폰 두 개가 만들어질 수 있다.
이 글의 질문은 결과를 모르는 외부 요청을 어떤 증거로 최종 상태에 수렴시킬까다. 발행·결제처럼 외부 효과가 있는 API에서 PENDING을 저장하고 조회·웹훅·대사로 확인하는 설계를 설명한다.
PENDING은 파트너가 처리 중이라는 확신이 아니다
응답을 받지 못한 구간에는 요청 미도착, 처리 중, 성공 응답 유실, 응답 수신 뒤 로컬 기록 전 중단이 모두 들어간다. 타임아웃은 이 가운데 어느 결과인지 알려주지 않는다. PENDING은 로컬에 확정 증거가 없다는 상태로 정의한다.
READY → REQUESTED → SUCCEEDED / REJECTED / PENDING_CONFIRMATION
PENDING_CONFIRMATION → SUCCEEDED / REJECTED / MANUAL_REVIEW
명시적 성공·거절은 계약에 맞춰 확정하고 불명확한 오류는 확인 상태로 보낸다. 자동 확인 기한이 지난 EXPIRED를 둘 수도 있지만, 그것이 외부 작업 미실행을 뜻하는지 조사 종료를 뜻하는지 구분해야 한다. 조회 보존 기간과 부재 응답 의미가 확정되지 않았다면 시간 경과만으로 실패를 판정하지 않는다.
그림에서는 조회 API가 최종 성공을 확인한다. 공급자가 조회·웹훅을 제공하는지와 어떤 키로 찾는지는 연동 계약에서 결정한다.
호출 전에 추적할 식별자를 커밋한다
외부 성공 뒤 처음 로컬 행을 만들면 중간 종료 시 추적할 기준까지 사라진다. 다음은 호출과 확인 작업을 연결하는 테이블 예제다.
CREATE TABLE partner_operation (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
business_key varchar(128) NOT NULL,
idempotency_key uuid NOT NULL,
request_hash varchar(64) NOT NULL,
partner_request_id varchar(128),
partner_transaction_id varchar(128),
status varchar(32) NOT NULL,
attempt_count integer NOT NULL DEFAULT 0,
next_check_at timestamptz,
lease_until timestamptz,
last_error_code varchar(64),
last_error_message text,
requested_at timestamptz,
confirmed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uq_partner_operation_key UNIQUE (idempotency_key)
);
REQUESTED와 요청 키를 짧은 로컬 트랜잭션으로 확정한 뒤 외부 호출을 시작한다. 동일 키 입력은 hash로 비교하고 외부 거래 ID가 생기면 같은 작업에 연결한다. 호출 전 커밋 뒤 프로세스가 종료되는 경우도 있으므로 REQUESTED에 남은 오래된 작업을 회수하는 경로가 필요하다.
외부 멱등성 키가 없다면 business reference 조회와 중복 판정 규칙을 먼저 합의한다. 로컬 한 행이 외부 한 건을 보장하는 것은 아니다. 안전한 재호출 근거가 없다면 자동 발행보다 수동 확인을 선택할 수 있다.
생성 재시도보다 상태 조회를 먼저 선택한다
거래 ID로 조회하고, 없으면 요청 키나 merchant reference로 찾고, 이미 수신한 웹훅을 확인한다. 같은 키 재호출이 안전하다는 공급자 계약이 있을 때만 생성 API 재시도를 선택한다. 단순한 5xx도 외부 미실행의 증거로 일반화하지 않는다.
조회 간격은 파트너 처리 시간, rate limit과 사용자 완료 약속에 맞춘다. 예를 들어 10초·30초·2분·10분으로 늘리는 정책을 비교할 수 있다. jitter와 전체 횟수·시간 상한을 두고 HTTP 클라이언트, SDK, 서비스와 배치가 중첩 재시도하는지 확인한다. AWS의 재시도 제한 지침은 이 제어 기준을 설명한다.
여러 작업자는 짧게 점유하고 밖에서 조회한다
다음 SQL은 준비된 미확정 작업을 점유하는 핵심 예제다.
WITH picked AS (
SELECT id FROM partner_operation
WHERE status = 'PENDING_CONFIRMATION' AND next_check_at <= now()
ORDER BY next_check_at
FOR UPDATE SKIP LOCKED
LIMIT 100
)
UPDATE partner_operation p
SET status = 'RECONCILING',
attempt_count = attempt_count + 1,
lease_until = now() + interval '30 seconds',
updated_at = now()
FROM picked
WHERE p.id = picked.id
RETURNING p.*;
다른 작업자가 잠근 행을 건너뛰고 점유 상태를 커밋한 뒤 외부 조회를 실행한다. SKIP LOCKED는 큐 형태의 경합 완화에 적합하지만 전체 데이터의 일관된 조회를 제공하는 선택은 아니다. PostgreSQL SELECT의 잠금 옵션.
100행과 30초 lease는 예제 예산이다. 작업자가 종료되면 만료된 점유를 회수하고, 늦게 돌아온 이전 작업자가 새 소유자의 상태를 덮지 않도록 소유 토큰이나 세대 번호를 검사한다. 이 필드는 핵심 SQL에서 생략했지만 복수 작업자 구현에 필요하다.
최종 반영에는 상태 전이 조건도 둔다.
UPDATE partner_operation
SET status = 'SUCCEEDED', partner_transaction_id = :partner_tx_id,
confirmed_at = now(), updated_at = now()
WHERE id = :id
AND status IN ('REQUESTED', 'PENDING_CONFIRMATION', 'RECONCILING');
이미 확정된 상태를 무조건 덮지 않는 예제다. 실제 worker 경로에서는 점유 토큰을 추가 검사한다. 확정 거절 뒤 성공 통지가 오면 충돌 기록을 남기고 공급자의 상태 의미에 따라 재조회·수동 검토를 진행한다.
웹훅도 중복과 순서 역전을 처리한다
Stripe는 이벤트 순서를 보장하지 않고 중복 전달 가능성을 설명한다. 이 계약을 모든 공급자에게 적용하는 대신 수신 측의 방어 기준으로 참고한다. 웹훅 전달·중복 처리.
CREATE TABLE partner_event_inbox (
provider varchar(32) NOT NULL,
event_id varchar(128) NOT NULL,
event_type varchar(64) NOT NULL,
object_id varchar(128),
payload jsonb NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
processed_at timestamptz,
PRIMARY KEY (provider, event_id)
);
서명과 형식을 검사하고 inbox 저장을 확정한 뒤 성공 응답을 반환한다. 도메인 반영은 재처리 가능한 작업으로 분리한다. 다른 event ID가 같은 객체 변화를 가리키는 경우도 있어 객체와 이벤트 종류, 현재 거래 조회를 함께 사용한다. 생성 시각 하나만으로 순서를 판정하지 않는다.
만료 뒤 성공은 사업 정책으로 처리한다
자동 확인 기한 뒤 성공이 확인되면 성공 수용, 성공 후 취소·회수, 수동 검토 중 하나를 선택한다. 대체 거래가 이미 만들어졌는지, 상품 제공과 취소 가능성·수수료가 어떤지에 따라 선택이 갈린다. 보상은 성공을 덮어쓰는 대신 별도 명령·결과로 남긴다. 보상 실패에도 재확인 경로가 필요하다.
관측에서는 현재 PENDING 수뿐 아니라 머문 시간의 분포, 파트너별 확인 시간, 지연 성공·자동 조정·수동 검토 비율과 양쪽 거래 불일치를 본다. 예를 들어 타임아웃 0.4% 중 70%가 5분 안에 성공으로 확인되는 조건이라면 타임아웃 실패 확정 정책과 결과 확인 정책의 차이를 계산할 수 있다. 이 값으로 실제 임계치를 정하지는 않는다.
검증 대역은 처리 후 응답 유실, 중복·역순 웹훅, worker 중단, 입력 불일치, 외부 성공 뒤 로컬 커밋 실패와 기한 뒤 성공을 재현해야 한다. 외부 효과 수와 로컬 상태, 사용자 응답과 운영 큐를 함께 확인한다.
이 설계는 외부 효과가 있고 결과 조회 또는 합의된 대사가 가능한 경우에 유효하다. 다음 결정은 타임아웃을 몇 번 재시도할지보다 어떤 키와 증거로 기존 결과를 찾을지다. 자동 만료를 사용할 때도 그 뒤 도착한 결과를 처리할 정책까지 정해야 미확정 작업이 끝난다.
