Production Backend API Capstone
A Retry Is Not a New Command
한국어 원문으로 표시합니다.
한 줄 요약
클라이언트가 응답을 받지 못해 같은 요청을 다시 보내도 서버는 같은 명령으로 인식해야 한다. 트랜잭션과 조직 범위 멱등 키를 함께 사용하면 주문은 한 번만 생성되고 재시도는 처음 만들어진 리소스를 돌려준다.
왜 타임아웃이 중복 주문을 만드는가
서버가 커밋한 직후 네트워크가 끊기면 클라이언트는 성공을 모른다. 무조건 재시도하면 두 번째 주문이 생긴다. “요청을 한 번만 보내라”는 해결책은 분산 시스템에서 지킬 수 없다. 대신 클라이언트가 Idempotency-Key를 보내고 서버가 조직과 함께 저장한다. 첫 쓰기와 키 기록은 같은 트랜잭션이어야 하며, 둘 중 하나만 성공하는 상태가 없어야 한다.
PostgreSQL의 INSERT ... ON CONFLICT (org_id, idempotency_key)는 경쟁 요청도 데이터베이스 직렬화 지점에서 합친다. 먼저 조회하고 나중에 삽입하는 코드는 두 요청이 동시에 “없음”을 보고 모두 삽입하는 경쟁 조건이 생긴다. 충돌 경로는 새 입력 금액으로 기존 주문을 덮으면 안 된다. 원래 주문을 반환해 같은 명령의 결과가 안정적으로 유지되어야 한다.
현장에서 실패를 다루는 법
드라이버 연결과 커서는 컨텍스트 매니저로 닫고, 쓰기는 명시적 transaction 경계에 둔다. 예외가 나면 rollback을 보장하고 예외 메시지에 SQL 인자나 연결 문자열을 넣지 않는다. 통합 테스트는 같은 키를 다른 금액으로 두 번 보내 첫 응답이 201, 재시도가 200이며 ID와 원래 금액이 같은지 확인한다. 다른 조직은 같은 키를 독립적으로 쓸 수 있어야 한다.
재시도를 안전하게 만드는 세 가지 장치
재시도는 분산 시스템에서 피할 수 없다. 문제는 재시도해도 되는지를 호출하는 쪽이 알 수 없다는 것이다. 응답을 못 받은 것과 처리가 안 된 것은 다르다.
멱등 열쇠를 요청자가 만든다. 서버가 만들면 재시도할 때마다 달라지므로 소용이 없다. 클라이언트가 UUID 를 하나 만들어 첫 시도부터 마지막 재시도까지 같은 값을 보낸다.
create table payment_requests(
org_id bigint not null,
idem_key uuid not null,
request_sha bytea not null,
status text not null check(status in ('처리중','완료','실패')),
response jsonb,
created_at timestamptz not null default now(),
primary key (org_id, idem_key));
같은 열쇠에 다른 내용이 오면 거절한다. request_sha 를 함께 저장하는 이유다.
열쇠는 같은데 금액이 다르다면 그것은 재시도가 아니라 버그이므로, 조용히
성공시키는 것이 가장 나쁘다.
행을 먼저 잡고 일을 한다. insert ... on conflict do nothing returning id 가
비어 있으면 누군가 이미 시작한 것이다. 그때는 그 행의 상태를 보고, 완료면 저장된
응답을 그대로 돌려주고, 처리 중이면 409 로 되돌려 잠시 뒤 다시 묻게 한다.
서버에서 기다리게 하면 그 연결이 쌓여 또 다른 문제가 된다.
바깥 호출과 데이터베이스 트랜잭션을 한 덩어리로 묶지 않는다. 결제 게이트웨이를 부르는 동안 트랜잭션을 열어 두면, 그 느린 호출만큼 잠금이 유지되고 연결이 묶인다. 순서를 나눈다: 짧은 트랜잭션으로 의도를 기록하고, 트랜잭션 밖에서 바깥을 호출하고, 다시 짧은 트랜잭션으로 결과를 적는다. 중간에 죽으면 의도만 남은 행이 남으므로, 그것을 찾아 상대에게 상태를 물어보는 복구 작업을 함께 만든다.
재시도에는 반드시 상한과 지터를 준다. 모두가 같은 간격으로 재시도하면 장애가 회복되는 순간 다시 무너진다. 지수 백오프에 무작위 요소를 섞고, 총 시도 시간을 정하고, 그 뒤에는 실패로 처리해 사람이 보게 한다. 영원히 재시도하는 큐는 장애를 숨기는 장치가 된다.
실무 판단 기준
멱등성은 “중복에서 에러가 난다”보다 강한 계약이다. 사용자가 결과를 안전하게 다시 받을 수 있어야 한다. 키 보존 기간, 요청 본문 해시 충돌 정책, 오래된 키 정리도 실제 서비스에서는 명시한다. 이번 캡스톤은 가장 중요한 생성 경계와 충돌 의미를 먼저 증명하고, 다음 모듈에서 이를 실제 HTTP 상태와 본문 계약으로 외부에 노출한다.