The Contract 202 Accepted Begins
한국어 원문으로 표시합니다.
한 줄 요약
202 는 "했다"가 아니라 "받았다"이다. 그러므로 202 를 주는 API 는 반드시 "어디서 결과를 확인하는가"를 함께 알려 줘야 한다.
왜 이게 필요했나
30초 걸리는 작업을 동기 API 로 만들면 세 가지가 동시에 깨집니다. 로드밸런서의 유휴 타임아웃(대개 60초)에 걸리고, 클라이언트가 재시도하면 같은 작업이 두 번 돌고, 서버 워커가 30초씩 묶입니다.
비동기 API 는 이것을 두 단계로 나눕니다. 접수와 확인입니다. 접수는 빠르게 끝나고 식별자를 돌려줍니다. 확인은 그 식별자로 상태를 물어봅니다.
어떻게 동작하나
HTTP 에는 이미 이 패턴을 위한 어휘가 있습니다.
접수 응답은 202 Accepted 이고, Location 헤더에 상태 조회 URL 을 담습니다. 본문에는 작업 식별자와 현재 상태를 넣습니다. 이 세 가지가 없으면 클라이언트는 무엇을 해야 할지 모릅니다.
상태 조회는 200 으로 현재 상태를 돌려줍니다. 상태 어휘는 단순할수록 좋습니다 — queued, running, succeeded, failed 정도입니다. 아직 진행 중이면 Retry-After 로 다음 폴링 시점을 제안합니다. 이게 없으면 클라이언트들이 각자 1초마다 폴링해서 상태 API 가 새 병목이 됩니다.
완료되면 결과를 어떻게 줄지는 두 갈래입니다. 상태 응답 본문에 결과를 함께 담거나, 303 See Other 로 결과 리소스 URL 을 가리키는 것입니다. 후자가 REST 스럽지만 클라이언트 구현이 늘어납니다. 결과가 작으면 전자로 충분합니다.
제출도 멱등해야 합니다. 클라이언트가 202 를 받기 전에 연결이 끊겨 재시도하면 같은 작업이 두 개 만들어집니다. 제출에 멱등성 키를 요구하고, 같은 키에는 같은 job id 를 돌려주면 됩니다.
웹훅은 폴링의 대안입니다. 완료 시 서버가 클라이언트의 URL 로 POST 합니다. 지연이 없고 폴링 부하도 없지만 대가가 있습니다 — 클라이언트가 공개 엔드포인트를 가져야 하고, 그 엔드포인트가 죽어 있을 수 있으므로 서버가 재시도와 DLQ 를 갖춰야 하고, 위조를 막기 위해 서명이 필요합니다. 실무에서는 웹훅과 폴링을 함께 제공하는 경우가 많습니다.
현장에서 만나는 모습
가장 흔한 설계 실수는 job id 로 진짜 결과 리소스의 ID 를 그대로 쓰는 것입니다. 아직 만들어지지 않은 리소스의 ID 를 미리 준다는 뜻이라, 실패했을 때 그 ID 는 영원히 유령이 됩니다. job id 와 result id 는 분리하는 편이 안전합니다.
두 번째. 작업 상태를 영원히 보관하지 마세요. 완료된 작업 상태에 TTL 을 두고, 만료된 조회에는 404 대신 410 Gone 을 주면 클라이언트가 "없었던 것"과 "지났던 것"을 구별할 수 있습니다.
202 를 돌려준 뒤의 계약
202 Accepted 는 "받았다" 일 뿐 "될 것이다" 가 아닙니다. 그래서 세 가지를 함께
줘야 클라이언트가 코드를 쓸 수 있습니다.
HTTP/1.1 202 Accepted
Location: /api/jobs/7f3a-91cd
Retry-After: 3
Content-Type: application/json
{"job_id": "7f3a-91cd", "status": "queued",
"poll_url": "/api/jobs/7f3a-91cd", "estimated_sec": 30}
Location— 어디를 물어보면 되는지. 표준 헤더라 도구들이 이해합니다.Retry-After— 얼마 뒤에 물어보라. 없으면 클라이언트가 초당 열 번씩 칩니다.- 작업 ID — 재시도해도 같은 작업임을 알아볼 열쇠.
상태 조회는 최소 네 상태를 구분합니다.
{"status": "queued"} → 아직 시작 안 함
{"status": "running", "progress": 0.4} → 진행 중
{"status": "succeeded", "result_url": "/files/…"} → 결과가 있다
{"status": "failed", "error": {"code": "…","message": "…"}, "retriable": false}
retriable 이 중요합니다. 클라이언트가 다시 보낼지 사람에게 물을지를 이 값으로
정합니다.
폴링 대신 알림
폴링은 단순하지만 낭비입니다. 세 대안이 있고 각각 값이 다릅니다.
| 방식 | 서버 부담 | 클라이언트 복잡도 | 방화벽 |
|---|---|---|---|
| 폴링 | 요청 수만큼 | 가장 단순 | 문제 없다 |
| 롱폴링 | 연결 유지 | 단순 | 프록시 타임아웃 주의 |
| SSE | 연결 유지 | 보통 | 대개 통과 |
| 웹훅 | 가장 적다 | 수신 엔드포인트 필요 | 상대가 공개 주소여야 |
웹훅을 쓰면 재시도와 서명 을 함께 설계해야 합니다. 받는 쪽이 잠깐 죽으면 다시 보내야 하고, 위조를 막으려면 본문에 HMAC 서명을 붙여야 합니다.
멱등 키로 중복 제출 막기
클라이언트가 202 를 받기 전에 타임아웃이 나면 같은 요청을 또 보냅니다. 그러면 작업이 두 개 생깁니다.
POST /api/jobs
Idempotency-Key: 8f14e45f-ea3b-4d29-9a1c-2b3c4d5e6f70
서버는 이 키를 저장하고, 같은 키가 다시 오면 새로 만들지 않고 처음 만든 작업의 202 를 그대로 돌려줍니다. 키는 클라이언트가 만들되 재시도 사이에 바뀌지 않아야 하므로, 요청 내용에서 결정적으로 유도하거나 요청 시작 시점에 한 번 생성해 보관합니다.
키의 보존 기간은 24시간 정도가 흔합니다. 그보다 오래 두면 저장소가 자라고, 짧으면 느린 재시도를 못 잡습니다.
다음 실습에서 할 것
작업 제출 API 를 만들어 202 와 Location 을 정확히 돌려주고, 워커가 상태를 전이시키고, Retry-After 로 폴링을 조절하고, 제출을 멱등하게 만들고, 마지막에 완료 웹훅까지 붙입니다.