요청을 제한하면서 클라이언트에게 언제 다시 오라고 말하지 않던 문제
문제 발생
외부 연동 API에 속도 제한을 걸었습니다. 초과하면 이렇게 응답했습니다.
HTTP/1.1 403 Forbidden클라이언트 입장에서는 권한이 없는 것인지 잠시 뒤 다시 오면 되는지 알 수 없었습니다. 대부분의 클라이언트가 즉시 재시도했고, 제한을 건 의미가 사라졌습니다.
원인 분석
전용 상태 코드가 있습니다. RFC 6585의 정의는 한 문장입니다 — 429 상태 코드는 사용자가 주어진 시간 동안 너무 많은 요청을 보냈음을 나타낸다("rate limiting").
그리고 응답이 무엇을 담아야 하는지도 적혀 있습니다 — 응답 표현은 그 조건을 설명하는 세부 정보를 포함해야 하며(SHOULD), 새 요청을 보내기 전에 얼마나 기다려야 하는지를 나타내는 Retry-After 헤더를 포함할 수 있다(MAY).
Retry-After 자체는 RFC 9110에 정의돼 있습니다 — 사용자 에이전트가 후속 요청을 보내기 전에 얼마나 기다려야 하는지를 나타내며, 503과 3xx 리다이렉트 코드 응답에 적용된다. 429에 함께 쓰는 것이 RFC 6585가 안내하는 조합입니다.
명세는 구현 자유도도 명시합니다 — 이 명세는 원 서버가 사용자를 어떻게 식별하는지, 요청을 어떻게 세는지 정의하지 않습니다. 그리고 캐시 관련 제약도 있습니다 — 429 응답은 캐시에 저장되어서는 안 됩니다(MUST NOT).
해결 방안
- 상태 코드와 대기 시간을 함께 보냅니다.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/problem+json
{ "type": "https://meokko.com/problems/rate-limited", "title": "요청이 너무 많습니다", "status": 429 }-
왜 걸렸는지 설명합니다. 명세가 "조건을 설명하는 세부 정보"를 요구하는 이유는 클라이언트 개발자가 한도를 추측하지 않게 하기 위해서입니다. 분당 한도, 적용 범위(사용자/IP/엔드포인트)를 응답이나 문서에 드러냅니다.
-
남은 한도를 헤더로 알려주면 더 좋습니다.
RateLimit-*계열 헤더를 쓰면 클라이언트가 429를 맞기 전에 스스로 속도를 줄일 수 있습니다. -
클라이언트 쪽에는 지수 백오프와 지터를 넣습니다.
Retry-After를 존중하고, 없으면 백오프로 재시도합니다. 모든 클라이언트가 정확히 60초 뒤에 동시에 오면 그 순간 다시 무너집니다. -
인증 실패와 구분합니다. 401/403은 "자격/권한" 문제이고 429는 "속도" 문제입니다. 섞어 쓰면 클라이언트의 재시도 정책이 틀어집니다.
-
정말 공격 상황에서는 다른 선택도 가능합니다. 명세도 서버가 이 코드를 반드시 쓸 필요는 없고 연결을 끊어도 된다고 적습니다 — 응답을 만드는 비용조차 아까운 상황이 있습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.