LabHub
배우기 러닝패스 코스

통합과 배포 · 200 이라고 다 들어간 것은 아니다 · 이론

요청은 200 인데 세 건이 사라졌다

LabHub 에서 이어서 보기

한 줄 요약

묶어 보내는 API 는 요청 하나에 200 을 주면서 본문 안에 건별 결과를 담는 경우가 많고, 호출한 쪽이 상태 코드만 보면 실패한 몇 건은 아무 데도 남지 않은 채 사라진다. 그래서 건별 결과를 기록하고, 다시 보낼 것과 고칠 것을 가르고, 마지막에 양쪽 수를 맞춰 보는 대사가 필요하다.

왜 이게 필요했나

"어제 정산 200건 보냈는데 상대 쪽에는 187건만 있대요." 이 신고에서 가장 먼저 확인하는 것은 우리 로그다. 그런데 우리 로그에는 200건 전부 성공이라고 적혀 있다. 거짓말을 한 것이 아니다. 우리가 본 것이 요청의 상태 코드뿐이었을 뿐이다.

묶음 전송 API 의 응답은 대개 이렇게 생겼다.

{"batch_id": "B-07", "accepted": 27, "rejected": 3, "results": [{"item_id": "IT-0031", "status": "rejected", "reason": "unknown_account"},             {"item_id": "IT-0032", "status": "accepted", "reason": null}]}

요청은 성공했다. 서버는 요청을 받아 처리했고, 그 결과가 본문에 들어 있다. HTTP 의 관점에서 200 은 옳다 — [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html)이 정의하는 2xx 는 "요청을 받아 이해하고 수락했다" 는 뜻이지 "요청 안의 모든 항목이 성공했다" 는 뜻이 아니다.

그래서 이 사고는 오류도 경보도 없이 난다. 그리고 며칠씩 쌓인 뒤에야 상대 쪽 숫자와 맞춰 보다가 발견된다.

어떻게 동작하나

고치는 순서는 넷이다.

첫째, 건별 결과를 우리 쪽에 남긴다. 보낸 건마다 status·reason·시도 횟수 를 기록한다. 이 기록이 없으면 다음 세 단계를 할 수 없다. 흔한 실수는 "실패한 것만" 기록하는 것인데, 그러면 "보낸 적이 없는 것" 과 "보내서 성공한 것" 이 구별되지 않는다.

둘째, 실패를 두 갈래로 가른다. 다시 보내면 풀리는 실패와 사람이 고쳐야만 하는 실패다.

| 이유 | 다시 보내면 | 해야 할 일 |
| --- | --- | --- |
| 일시 보류·속도 제한·일시적 오류 | 풀린다 | 큐에 다시 넣는다 |
| 모르는 계좌·잘못된 값 | 같은 답이 온다 | 데이터를 고친다 |
| 이미 처리됨(중복) | 같은 답이 온다 | 아무것도 안 한다 |

이 분류가 없으면 두 가지 중 하나가 된다. 전부 재시도하면 고칠 수 없는 건들이 큐에서 영원히 돌고, 아무것도 재시도하지 않으면 잠깐 걸린 건까지 사람 손으로 간다. 속도 제한은 [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585.html)의 429 로 오는 경우가 많고 Retry-After 를 함께 준다 — 그것은 명백히 다시 보내야 하는 쪽이다. 실패 이유를 기계가 읽을 수 있게 담는 표준 형식으로는 [RFC 9457 의 problem details](https://www.rfc-editor.org/rfc/rfc9457.html)가 있고, type 으로 갈래를 구분한다.

셋째, 실패분만 다시 보낸다. 여기서 묶음 전체를 다시 보내면 이미 들어간 건이 두 번 들어간다. 상대가 중복을 막아 주면 다행이고, 안 막아 주면 우리가 사고를 낸다. 재시도의 단위는 묶음이 아니라 건이다.

넷째, 대사한다. 보낸 쪽의 "성공으로 기록한 건수와 합계" 와 받은 쪽의 "실제로 들어간 건수와 합계" 를 맞춰 본다. 두 숫자가 같아야 하고, 다르면 어느 건이 어느 쪽에만 있는지까지 내려간다.

발신함 120건   ├─ 성공 기록 112건 · 합계 X   └─ 실패 기록   8건 (고쳐야 함 8)상대 원장     112행 · 합계 X      ← 건수와 합계가 **둘 다** 맞아야 한다

건수만 맞춰 보면 안 된다. 한 건이 빠지고 다른 한 건이 두 번 들어가면 건수는 그대로다. 그래서 합계나 집합 비교를 함께 한다.

현장에서 만나는 모습

첫째, 응답의 results 순서가 요청 순서와 같다고 가정한다. 많은 API 가 실제로 같은 순서로 주지만, 문서에 그렇게 적혀 있지 않으면 믿으면 안 된다. item_id 로 맞춰 읽는다.

둘째, 부분 실패를 예외로 던진다. "3건 실패" 를 예외로 올려 배치 전체를 롤백하면, 성공한 27건까지 다시 보내게 되고 그것이 중복을 만든다.

셋째, 재시도 횟수에 상한이 없다. 일시적 실패로 분류된 건이 사실은 영구적 실패라면 그 건은 큐를 영원히 돈다. 시도 횟수를 기록하고 몇 번 뒤에는 사람에게 넘긴다.

넷째, 대사를 사고 뒤에만 한다. 대사는 사고 조사 도구가 아니라 매일 도는 것이어야 한다. 어제 것과 오늘 것을 비교할 수 있어야 언제부터 어긋났는지 알 수 있다.

다섯째, 남은 건의 주인이 없다. "고쳐야 함 8건" 을 만들어 놓고 누가 고칠지 안 적으면 그 8건은 영원히 남는다. 대사표의 마지막 칸은 사람 이름이다.

다음 실습에서 할 것

묶음 전송을 받는 파트너 서버를 띄우고, 보낼 것 120건을 만든다. 먼저 상태 코드만 보는 방식으로 보내 우리 기록과 상대 원장의 차이를 숫자로 확인한다. 그다음 건별 결과를 읽어 우리 쪽에 기록하고, 실패 이유를 다시 보낼 것과 고칠 것으로 가르고, 다시 보낼 것만 골라 재전송한다. 마지막으로 보낸 쪽과 받은 쪽의 건수와 합계를 맞춰 보는 대사표를 만들고, 남은 건의 처리 계획을 적는다.