엣지 미들웨어 rate limit 설계 노트 — 위협 모델부터 429 응답까지

rate-limit미들웨어엣지보안Next.js

초록

사이드 프로젝트가 검색·커뮤니티에 노출되기 시작하면, 문의 API와 로그인 엔드포인트에는 사람보다 먼저 기계적 트래픽이 붙는 경우가 많습니다. 본 글은 엔터프라이즈 WAF·Zero Trust 백서의 범위를 다루지 않습니다. 대신 애플리케이션 앞단, 특히 Next.js 계열 프레임워크의 엣지 미들웨어 계층에서 **쓰기 경로(write path)**에 적용하는 rate limit을 설계 문제로 재정의하고, 그 설계를 구성하는 변수—키(key), 윈도우(window), 저장소(store), 응답(response), 관측(observability)—를 하나의 분석틀로 묶어 제시합니다. 핵심 주장은 두 가지입니다. 첫째, rate limit의 목표는 “봇을 완벽하게 판별하는 것”이 아니라 남용의 기댓값을 비싸게 만들고 정상 사용자의 마찰을 최소화하는 것이어야 합니다. 둘째, 이 설계는 보안 계층의 일부이지 전체가 아니며, 핸들러 내부의 인증·인가·입력 검증을 대체할 수 없습니다. 본 글은 이 두 주장을 뒷받침하기 위해 위협 모델 축소, 설계 변수의 트레이드오프, 실패 모드 분류, 그리고 한계를 명시적으로 서술합니다.


1. 서론

개인 개발자나 소규모 팀이 운영하는 서비스가 검색 엔진에 색인되고 SNS에서 언급되기 시작하면, 트래픽의 질이 바뀝니다. 초기에는 지인과 얼리어답터의 수동 클릭이 대부분이지만, 일정 시점을 지나면 크롤러, 스크래퍼, 자동화된 폼 스패머, 자격 증명 스터핑(credential stuffing) 스크립트가 유입됩니다. 이들은 대개 사람보다 빠르고, 사람보다 인내심이 많으며, 사람보다 비용에 둔감합니다. 문제는 이 트래픽이 단순히 “불쾌한 로그 한 줄”로 끝나지 않는다는 점입니다. 문의 폼 뒤에 메일 발송 API가 붙어 있다면 스팸 제출 한 건마다 실제 비용이 발생합니다. 로그인 엔드포인트에 무제한 시도가 허용된다면, 약한 비밀번호를 쓰는 사용자 계정이 무차별 대입 공격에 노출됩니다. 웹훅 수신 엔드포인트가 보호되지 않는다면, 서명 검증 로직 자체가 서비스 거부(DoS)의 표면이 될 수 있습니다.

이 글의 목적은 “어떤 rate limit 라이브러리를 쓸 것인가”라는 도구 선택 질문에 답하는 것이 아닙니다. 도구 선택은 팀의 기존 인프라(사용 중인 KV 제품, 배포 플랫폼)에 크게 좌우되며, 그 선택 자체는 이 글이 다루는 설계 질문에 대한 답을 이미 알고 있을 때 훨씬 쉬워집니다. 라이브러리와 KV(Key-Value) 벤더는 시간이 지나면 바뀌지만, 설계 질문의 구조는 상대적으로 오래 유지됩니다. 실제로 이 질문들—무엇을 키로 삼을 것인가, 얼마나 긴 창을 볼 것인가, 카운터를 어디에 저장할 것인가, 한도를 넘긴 요청에 무엇을 응답할 것인가, 그리고 이 모든 결정을 어떻게 관측할 것인가—은 어떤 스택을 쓰든 반복해서 등장합니다. 본 글은 이 질문들을 순서대로 다루되, 각 결정이 왜 그런 형태를 취하는지에 대한 근거를 함께 제시하는 것을 목표로 합니다.

또한 본 글은 “한계”를 결론부에 부차적으로 덧붙이는 대신, 하나의 독립된 절로 명시적으로 다룹니다. rate limit을 배포한 뒤 “이제 보안이 해결되었다”는 인식이 생기는 것이 실무에서 가장 위험한 부작용이기 때문입니다. 분산 프록시, 주거용 IP 풀, 느린 인간형 자동화(slow-drip automation)는 단순한 요청 빈도 제한으로 막히지 않습니다. 이 사실을 설계 초기에 인정하는 편이, 나중에 “왜 아직도 스팸이 들어오냐”는 질문에 답하는 것보다 저렴합니다.


2. 배경

2.1 왜 “사이트 전체 보호”가 아닌가

직관적으로는 모든 요청에 동일한 rate limit을 적용하는 것이 가장 단순해 보입니다. 그러나 정적 마케팅 페이지, 블로그 상세, OG 이미지 요청까지 동일한 한도로 묶으면 검색 엔진 크롤러의 정상적인 색인 작업이나 SNS 공유 미리보기 생성기(Slack, Discord, 카카오톡의 링크 프리뷰 봇 등)가 오탐으로 차단될 위험이 커집니다. 이는 SEO와 공유 가능성이라는, 사이드 프로젝트의 성장에 필수적인 채널을 스스로 막는 결과를 낳습니다.

반면 rate limit이 투자 대비 효과가 큰 지점은 대체로 다음과 같이 좁게 정의됩니다.

  • 문의·리드 폼: 메일 API 비용과 직결되는 쓰기 경로
  • 로그인·회원가입·비밀번호 재설정: 계정 탈취 시도가 집중되는 지점
  • 토큰 발급·웹훅 수신: 별도의 서명 검증과 병행되어야 하는 지점
  • 관리자·내부 API: 노출 자체가 문제가 되는 지점

이 관찰은 단순하지만 실무적으로 중요한 원칙으로 이어집니다. “사이트 전체 보호”보다 쓰기·비밀이 걸린 경로의 matcher 설정이 먼저입니다. Next.js의 미들웨어 matcher 옵션(또는 동등한 프레임워크의 라우트 필터)을 이용해 보호 대상을 명시적으로 좁히는 작업이, 어떤 알고리즘을 쓰느냐보다 먼저 이루어져야 합니다.

2.2 미들웨어의 역할과 한계

엣지 미들웨어는 요청이 오리진 함수에 도달하기 전에 실행되는 얇은 레이어입니다. 이 레이어의 강점은 지연시간이 낮고, 여러 리전에 분산되어 실행되며, 애플리케이션 로직과 분리되어 있다는 점입니다. 그러나 이 강점은 동시에 제약이 됩니다. 미들웨어 런타임은 일반적으로 완전한 Node.js API 표면을 제공하지 않으며, 파일시스템이나 일부 네이티브 모듈에 접근할 수 없습니다. 따라서 rate limit 카운터를 저장하려면 미들웨어 실행 환경에서 접근 가능한 외부 스토어(예: Redis 호환 KV)를 사용하거나, 인스턴스 로컬 메모리를 사용해야 하며 각각 트레이드오프가 다릅니다.

더 중요한 개념적 구분은, 미들웨어가 인증을 대체하지 않는다는 점입니다. 미들웨어는 “이 클라이언트가 너무 빠르게 요청하고 있다”는 신호에만 반응할 수 있을 뿐, “이 클라이언트가 이 리소스에 접근할 권한이 있는가”라는 질문에는 답하지 않습니다. 세션 골격에서 다룬 것처럼, 인증·인가는 핸들러 내부에서 별도로, 그리고 명시적으로 검증되어야 합니다. rate limit은 그 검증이 도달하기 전 단계에서 트래픽의 속도만을 조절하는 방지턱입니다.

2.3 봇 트래픽의 스펙트럼 — 전부 악의적인 것은 아니다

“봇”이라는 단어는 흔히 악의적인 스크래퍼나 스패머를 떠올리게 하지만, 실제로 사이트에 도달하는 비인간 트래픽의 스펙트럼은 훨씬 넓습니다. 검색 엔진 크롤러(Googlebot, Bingbot), SNS 링크 프리뷰 생성기, 업타임 모니터링 서비스, RSS 리더, 그리고 최근에는 LLM 학습이나 검색 증강을 위한 크롤러까지, 이들 대부분은 사이트의 성장이나 정상 운영에 필요한 트래픽입니다. rate limit 설계가 이 스펙트럼을 “봇=차단 대상”으로 단순화하면, 검색 노출이 줄거나 SNS 공유 미리보기가 깨지는 자기 파괴적인 결과를 낳습니다.

이 스펙트럼을 다루는 실무적인 접근은, 알려진 “양의(benign) 봇”의 User-Agent 패턴이나 공개된 IP 대역을 화이트리스트에 반영하고, 나머지 미분류 비인간 트래픽에 대해서만 일반적인 rate limit 정책을 적용하는 것입니다. 다만 User-Agent는 위조가 가능하므로, 화이트리스트에 포함시킬 때는 가능하다면 역방향 DNS 조회나 공개된 IP 범위 대조와 같은 보조 검증을 함께 사용하는 것이 안전합니다. 이 보조 검증 없이 User-Agent 문자열만으로 화이트리스트를 만들면, 공격자가 “Googlebot”을 사칭하는 User-Agent로 우회하는 가장 기초적인 시도조차 막지 못합니다. 이 스펙트럼의 존재는, rate limit 설계가 “사람과 기계를 구분하는 문제”가 아니라 “원하는 기계 트래픽과 원하지 않는 기계 트래픽을 구분하는 문제”에 더 가깝다는 것을 시사합니다.

요청 → 미들웨어 → 한도 검사 → 429 또는 통과


3. 분석틀

본 글에서는 엣지 rate limit 설계를 다섯 개의 독립적이지만 상호작용하는 변수로 분해합니다. 이 분석틀은 특정 라이브러리의 API를 설명하기 위한 것이 아니라, 어떤 구현을 선택하든 답해야 하는 질문들의 집합입니다.

  1. 키(Key) — 무엇을 기준으로 요청을 그룹화할 것인가
  2. 윈도우(Window)와 알고리즘 — 얼마나 긴 시간 동안, 어떤 방식으로 카운트를 유지할 것인가
  3. 저장소(Store) — 카운터를 어디에, 어떤 일관성 보장 수준으로 저장할 것인가
  4. 응답(Response) — 한도를 넘긴 요청에 무엇을 돌려줄 것인가
  5. 관측(Observability) — 이 모든 결정이 실제로 작동하는지 어떻게 확인할 것인가

이 다섯 변수는 서로 독립적이지 않습니다. 예를 들어 키를 세밀하게(예: 사용자 ID 단위) 잡을수록 저장소의 카디널리티(cardinality) 요구가 커지고, 윈도우를 짧게 잡을수록 저장소의 쓰기 빈도가 늘어납니다. 따라서 각 변수를 독립적으로 최적화하는 것이 아니라, 보호하려는 경로의 비즈니스 리스크에 맞춰 다섯 변수를 함께 조정하는 것이 본 분석틀의 핵심 제안입니다. 이어지는 본론에서는 각 변수를 순서대로 다루되, 각 결정이 다른 변수에 미치는 영향을 함께 언급합니다.


4. 본론

4.1 키 설계 — 무엇을 묶을 것인가

가장 단순한 키는 클라이언트의 IP 주소입니다. 그러나 IP만을 키로 사용하면 두 가지 실패 모드가 발생합니다. 첫째, 기업 NAT나 모바일 통신사의 CGNAT(Carrier-Grade NAT) 뒤에 있는 다수의 사용자가 하나의 IP를 공유하는 경우, 그중 한 명이 한도를 소진하면 나머지 무고한 사용자가 함께 차단됩니다. 둘째, 공격자는 IP를 회전시키는 것이 상대적으로 쉬운 반면, 이 회전을 막기 위해 사이트가 지불하는 대가(예: VPN·프록시 전면 차단)는 정상 사용자에게도 영향을 미칩니다.

이 문제를 완화하기 위한 현실적인 절충안은 경로별로 키의 구성 요소를 다르게 설계하는 것이며, 이 절충안이 실질적으로 효과를 내려면 각 경로의 위협 모델을 먼저 구체적으로 정의해 두어야 합니다.

  • 인증되지 않은 공개 폼(문의, 회원가입): 경로 + IP 조합, 여기에 폼 자체의 숨은 필드(허니팟)나 브라우저 지문의 저비용 신호를 결합
  • 인증된 요청(로그인 이후의 API): 사용자 ID 자체를 키로 사용. IP가 바뀌어도 동일 계정의 남용 패턴을 추적할 수 있음
  • 로그인 시도: IP + 시도한 계정 식별자의 조합. IP 단위로만 제한하면 한 공격자가 여러 계정을 순회하며 각 계정당 시도 횟수를 낮게 유지하는 우회가 가능하고, 계정 단위로만 제한하면 여러 IP를 순회하며 한 계정을 집중 공격하는 우회가 가능하기 때문에 두 축을 모두 봐야 합니다.

키에 User-Agent 문자열만 추가하는 것은 위조 비용이 사실상 0에 가깝기 때문에 신뢰할 수 있는 신호가 아닙니다. User-Agent는 로깅과 관측의 보조 필드로는 유용하지만, 키의 핵심 구성 요소로 삼기에는 부적합합니다.

4.1-1 키 설계의 카디널리티 트레이드오프

키를 세밀하게 잡을수록 오탐(false positive, 정상 사용자를 잘못 차단하는 것)은 줄어들지만, 그 대가로 저장소가 추적해야 하는 유일 키의 수(카디널리티)가 늘어납니다. 경로 + IP 조합은 이미 수만 개의 유일 키를 만들어낼 수 있고, 여기에 사용자 ID디바이스 지문까지 추가하면 카디널리티는 더 커집니다. 카디널리티가 커지면 저장소의 메모리 사용량과 비용이 함께 늘어나므로, 키 설계는 “오탐을 얼마나 허용할 수 있는가”와 “저장소 비용을 얼마나 감당할 수 있는가” 사이의 절충점을 찾는 작업이기도 합니다.

이 절충을 다루는 실무적인 방법은, 모든 경로에 동일하게 세밀한 키를 적용하지 않고, 위협 모델에 따라 키의 세밀도를 차등 적용하는 것입니다. 이 차등 적용은 초기 설계 시점에 한 번 정해두면 이후 경로가 추가될 때마다 “이 새 경로는 어느 등급에 속하는가”라는 짧은 질문만으로 키 구조를 결정할 수 있어, 매번 처음부터 설계를 반복하지 않아도 됩니다. 로그인처럼 위험도가 높은 경로는 IP + 계정 식별자처럼 세밀한 키를 쓰고, 문의 폼처럼 상대적으로 낮은 경로는 경로 + IP만으로 충분한 경우가 많습니다. 모든 경로에 최대로 세밀한 키를 적용하는 것은 과잉 설계이며, 저장소 비용 대비 얻는 오탐 감소 효과가 낮은 경로까지 비싼 키 구조를 적용하는 것은 낭비입니다.

4.2 윈도우와 알고리즘 — 시간을 어떻게 자를 것인가

rate limit 알고리즘은 대략 세 계열로 나눌 수 있습니다.

**고정 윈도우(Fixed Window)**는 “분당 N회”처럼 시계를 일정 간격으로 자르고, 각 구간마다 카운터를 0으로 리셋하는 방식입니다. 구현이 가장 단순하지만, 경계 문제(boundary problem)가 있습니다. 예를 들어 한도가 “1분당 10회”일 때, 공격자가 59초에 10회, 다음 구간 시작인 1초 후에 다시 10회를 보내면 2초 사이에 20회가 통과합니다.

**슬라이딩 윈도우(Sliding Window)**는 현재 시점을 기준으로 최근 N초를 항상 계산하기 때문에 경계 문제가 없습니다. 다만 정확한 슬라이딩 윈도우는 요청마다 타임스탬프를 저장하거나, 두 개의 고정 윈도우를 가중 평균하는 근사 방식을 써야 하므로 고정 윈도우보다 저장소 부담이 큽니다.

**토큰 버킷(Token Bucket)**은 일정 속도로 토큰이 채워지는 버킷을 두고, 요청마다 토큰을 소비하는 방식입니다. 순간적인 버스트를 일정 수준까지 허용하면서도 장기 평균 속도를 제한할 수 있어, “가끔 몰아서 쓰지만 평소엔 조용한” 정상 사용 패턴에 관대합니다.

어떤 알고리즘을 고르든, 제품적으로 중요한 것은 사람 한 명이 실수로 넘길 수 있는 합리적 상한봇이 실질적 이득을 얻는 하한 사이에 충분한 간격을 두는 것입니다. 이 간격이 좁으면 두 가지 실패가 동시에 일어날 수 있습니다. 너무 빡세게 잡으면 정상 사용자가 두세 번 재시도하다가 잠기고 지원 티켓이 발생합니다. 너무 느슨하게 잡으면 봇이 한도 아래에서 조용히 자원을 소진하고, 그 결과는 다음 달 청구서에서 발견됩니다. 이 청구서 리스크는 사이드 프로젝트 FinOps에서 다루는 “유령 비용”의 한 형태이기도 합니다. 이 간격을 정하는 데 확실한 정답은 없지만, 최소한 “사람이 실수로 두세 번 재시도하는 정도는 절대 차단하지 않는다”는 하한선만큼은 팀 내부에서 명시적으로 합의해 두는 것이 좋습니다.

4.3 저장소 — 어디에 카운터를 둘 것인가

엣지 환경은 기본적으로 여러 리전, 여러 인스턴스에 분산되어 실행됩니다. 각 인스턴스가 자신의 메모리에만 카운터를 유지한다면, 동일한 클라이언트의 요청이 서로 다른 인스턴스로 라우팅될 때마다 카운터가 초기화된 것처럼 보입니다. 실질적으로 이는 한도가 인스턴스 수만큼 느슨해지는 결과를 낳습니다. 인스턴스가 5개라면, 이론상 “분당 10회” 한도가 “분당 최대 50회”로 완화될 수 있습니다.

이 문제를 해결하기 위해 흔히 쓰이는 패턴이 Upstash Redis와 같은 공유 카운터(shared counter) 스토어입니다. 모든 인스턴스가 동일한 외부 스토어에 카운터를 읽고 쓰기 때문에, 인스턴스 수와 무관하게 일관된 한도를 유지할 수 있습니다. 다만 이 패턴에는 새로운 트레이드오프가 따라옵니다.

첫째, 외부 스토어에 대한 네트워크 왕복이 매 요청마다 추가되어 지연시간이 늘어납니다. 둘째, 외부 스토어 자체가 새로운 장애 지점이자 비용 지점이 됩니다. 셋째, 가장 중요한 질문으로, 외부 스토어가 응답하지 않을 때 무엇을 할 것인가를 명시적으로 결정해야 합니다.

  • Fail-open: 스토어 오류 시 요청을 통과시킴. 가용성을 우선하지만, 스토어 장애가 곧 무제한 트래픽 허용으로 이어질 위험이 있습니다.
  • Fail-closed: 스토어 오류 시 요청을 차단함. 보안을 우선하지만, 스토어 장애가 서비스 전체 장애로 확산될 위험이 있습니다.

이 선택은 경로의 성격에 따라 달라져야 합니다. 결제·인증처럼 오남용의 대가가 큰 경로는 fail-closed 쪽으로, 마케팅 폼처럼 오남용의 대가가 상대적으로 작고 가용성이 더 중요한 경로는 상황에 따라 fail-open을 선택할 수 있습니다. 두 선택 모두 옳고 그름의 문제가 아니라, 가용성과 보안 중 어느 쪽의 실패를 더 감당할 수 있는지에 대한 조직의 우선순위를 반영하는 문제입니다. 중요한 것은 이 결정이 우연히 코드의 예외 처리 방식에 의해 정해지는 것이 아니라, 의식적으로 문서화된 선택이어야 한다는 점입니다. 이 선택이 문서화되어 있지 않으면, 장애 대응 중인 담당자가 즉흥적으로 코드를 수정해 임시 동작을 결정하게 되고, 이 임시 결정이 그대로 굳어져 원래 의도와 다른 정책으로 남을 위험이 있습니다.

키·저장소·응답·화이트리스트·관측

4.4 응답 설계 — 429와 그 이후

한도를 초과한 요청에 어떻게 응답할지는 생각보다 자주 간과됩니다. 일부 구현은 한도 초과 시에도 200 OK와 빈 본문 또는 성공 메시지를 반환하는데, 이는 두 가지 문제를 낳습니다. 첫째, 정상 사용자는 자신의 요청이 왜 반영되지 않는지 알 수 없어 혼란스럽습니다. 둘째, 자동화된 클라이언트는 이를 “성공”으로 학습하여 동일한 패턴을 반복할 뿐, 속도를 줄이지 않습니다.

정직한 응답은 HTTP 429 Too Many Requests입니다. 여기에 Retry-After 헤더나 이에 준하는 재시도 힌트를 포함하면, 잘 작성된 클라이언트(및 봇)는 백오프(backoff)를 적용할 근거를 얻습니다. 사람 사용자에게는 “잠시 후 다시 시도해 주세요”와 같은 짧고 명확한 안내 UI가 필요합니다. 이 안내가 없으면 사용자는 폼이 고장 났다고 오해하고 이탈합니다. 안내 문구를 작성할 때는 “차단되었다”는 표현보다 “잠시 후 다시 시도해 주세요”처럼 중립적인 문구를 쓰는 것이, 정상 사용자에게 불필요한 불안감을 주지 않는 방법입니다.

또한 화이트리스트 설계가 필요합니다. CI/CD 파이프라인이 배포 후 헬스체크를 반복 호출하다가 자기 자신의 rate limit에 걸리는 경우가 실무에서 실제로 발생합니다. 팀 구성원의 알려진 IP, CI 러너의 아웃바운드 IP, 모니터링 서비스의 헬스체크 엔드포인트는 명시적으로 예외 처리하여, 보호 장치가 자기 배포 파이프라인을 막는 자충수를 두지 않도록 해야 합니다.

4.5 관측성 — 규칙을 조이기 전에 봐야 하는 것

관측 없이 규칙을 조이는 것은 눈을 감고 다이얼을 돌리는 것과 같습니다. 차단된 요청의 수, 차단이 가장 많이 발생하는 상위 경로, 차단된 상위 키(원본 IP나 사용자 ID를 그대로 로깅하지 않고 해시하는 것이 바람직합니다)를 최소한의 대시보드로 확보하는 것만으로도 “규칙을 더 풀어야 하는지, 키 설계를 바꿔야 하는지”에 대한 의사결정이 데이터에 근거하게 됩니다.

실무에서 관찰되는 유용한 패턴 하나는, 차단 스파이크의 시점을 다른 신호와 겹쳐 보는 것입니다. 예를 들어 갑작스러운 차단 증가가 FinOps 비용 알림과 같은 날 발생한다면, 이는 우연이 아니라 동일한 원인—예를 들어 특정 크롤러가 여러 경로를 동시에 두드리기 시작했다는 사실—을 가리키는 두 개의 서로 다른 증상일 가능성이 높습니다. 관측 계층을 rate limit 전용으로 고립시키지 않고 다른 운영 신호와 나란히 두는 것이 진단 속도를 높입니다.

4.6 테스트 가능성 — 규칙을 코드와 문서가 같게 유지하기

키 생성 로직, 윈도우 경계 계산, 화이트리스트 매칭과 같은 순수 함수는 미들웨어 런타임 전체를 띄우지 않고도 단위 테스트로 고정할 수 있습니다. 이는 단위 테스트 대상 선정에서 다루는 원칙과 정확히 맞물립니다. “IP가 이런 형식일 때 키가 이렇게 생성된다”, “경계 시각에 카운터가 이렇게 넘어간다”는 명세를 예제 표로 고정해 두면, 이후 누군가—또는 AI 코딩 도구—가 이 로직을 리팩터링하더라도 회귀를 즉시 감지할 수 있습니다. rate limit 규칙이 코드에만 존재하고 문서에는 없다면, 리뷰어는 매번 구현을 역공학해야 합니다.

4.6-1 반례 — 관측 없이 규칙을 조인 사례

관측의 필요성을 반례로 설명하면 더 명확해집니다. 한 팀이 문의 폼의 스팸이 늘어나자, 데이터 없이 “분당 5회”였던 한도를 “분당 1회”로 즉시 낮춘 사례가 있습니다. 이 조치는 스팸을 줄이는 데는 성공했지만, 동시에 진성 리드가 폼을 두 번 이상 다시 제출해야 하는 상황(입력 실수 정정, 페이지 새로고침 후 재제출)에서 정상 사용자까지 차단하는 부작용을 낳았습니다. 이 부작용은 몇 주가 지나서야 “문의가 갑자기 줄었다”는 별도의 증상으로 드러났고, 원인을 rate limit 조정과 연결하기까지 추가적인 조사가 필요했습니다.

이 사례가 보여주는 것은, 관측 계층 없이 규칙을 조이면 두 가지 실패가 모두 “조용히” 발생한다는 점입니다. 스팸이 줄어드는 것은 눈에 보이는 성공으로 인식되지만, 정상 사용자가 차단되는 것은 “문의가 줄었다”는 모호한 신호로만 나타나 원인 추적이 늦어집니다. 만약 이 팀이 차단된 요청의 수와 차단된 요청의 특성(반복 제출 간격, 이전 성공 여부)을 관측하고 있었다면, 한도를 낮추는 즉시 “정상적인 재시도 패턴까지 차단되고 있다”는 신호를 발견하고 한도를 재조정할 수 있었을 것입니다.

4.7 운영 관점의 통합 — 하나의 결정 문장으로

지금까지 다룬 다섯 변수는 실무에서 다음과 같은 하나의 문장으로 요약될 수 있어야 합니다. 예를 들어: “문의 폼은 경로+IP 키, 1분 슬라이딩 윈도우, 분당 5회 한도, 공유 Redis 카운터, 실패 시 fail-open, 초과 시 429와 안내 문구를 반환하며, 차단 수는 일별로 집계해 모니터링한다.” 이러한 문장이 팀 위키나 코드 주석에 명시적으로 존재하지 않는다면, 이 설계는 “누군가의 머릿속에만 있는 설정값”으로 남아 다음 담당자가 이유 없이 숫자를 바꾸는 위험을 안게 됩니다.

4.8 화이트리스트의 역설 — 관대함이 새로운 공격 표면이 되는 경우

4.4절에서 다룬 화이트리스트는 CI/CD와 모니터링 도구가 자기 자신의 rate limit에 걸리는 문제를 막는 유용한 장치이지만, 이 장치 자체가 새로운 공격 표면이 될 수 있다는 점을 별도로 고려해야 합니다. 화이트리스트가 IP 기반이라면, 그 IP 대역(예: 사무실 공유기의 고정 IP)이 유출되거나 추측 가능한 범위 안에 있다면 공격자가 그 IP를 경유하거나 스푸핑하여 화이트리스트의 혜택을 노리는 시나리오가 이론적으로 존재합니다. 화이트리스트가 헤더 기반의 비밀 값(예: 내부 서비스 간 통신에만 쓰이는 시크릿 헤더)이라면, 이 시크릿이 클라이언트 코드에 실수로 노출되지 않도록 별도로 주의해야 합니다.

이 역설을 완화하는 방법은, 화이트리스트를 “rate limit을 완전히 면제”하는 것이 아니라 “한도를 완화”하는 것으로 설계하는 것입니다. 예를 들어 일반 사용자는 분당 5회, 화이트리스트에 포함된 CI 러너는 분당 100회처럼 여전히 상한을 두면, 화이트리스트가 우회되더라도 무제한 트래픽으로 이어지지는 않습니다. 완전한 면제보다 완화된 상한을 두는 이 설계는, 화이트리스트가 실수로 잘못된 대상에 적용되었을 때의 피해 범위도 함께 줄여줍니다.


5. 논의

본 글에서 제시한 다섯 변수 분석틀은 특정 구현에 종속되지 않는다는 장점이 있지만, 동시에 “구체적인 숫자를 알려주지 않는다”는 한계를 스스로 갖습니다. 이는 의도적인 선택입니다. 적절한 한도는 서비스의 사용자층, 폼의 위치, 메일 발송 비용, 계정 탈취의 대가에 따라 크게 달라지므로, 일반화된 숫자를 제시하는 것은 오히려 오해를 낳을 수 있습니다. 대신 본 글이 제안하는 것은 질문의 순서입니다. 먼저 보호할 경로를 좁히고, 다음으로 키를 정의하고, 그다음 윈도우와 저장소를 고르고, 마지막으로 응답과 관측을 설계하는 순서를 지키면, 어떤 숫자를 채우든 설계의 논리적 일관성은 유지됩니다. 이 순서를 거꾸로 밟아 응답 형식부터 정하고 나중에 키를 정의하려 하면, 이미 정해진 응답 로직에 맞춰 키 설계를 억지로 끼워 맞추게 되어 부자연스러운 타협이 생기기 쉽습니다.

또한 이 설계는 정적인 것이 아니라 반복적으로 조정되어야 하는 것입니다. 초기 배포 시점의 한도는 몇 주 뒤 트래픽 패턴이 바뀌면 더 이상 적절하지 않을 수 있습니다. 관측 계층이 없다면 이 조정 자체가 불가능하므로, 본 글에서 관측을 응답 설계 다음, 즉 초기 구현의 일부로 배치한 것은 “나중에 추가해도 되는 것”이 아니라 “처음부터 함께 배포되어야 하는 것”이라는 주장을 담고 있습니다.

마지막으로, rate limit을 다른 방어 계층과 어떻게 조합할지에 대한 논의가 필요합니다. CAPTCHA나 Cloudflare Turnstile과 같은 챌린지 기반 검증은 추가적인 신호를 제공하지만, 이 신호는 무료가 아닙니다. 정상 사용자의 전환율을 낮추고, 접근성(스크린 리더 사용자 등)에 부담을 주며, 구현·유지 비용이 듭니다. 따라서 이러한 도구는 rate limit이 이미 명확히 실패하고 있는 특정 경로(예: 반복적인 회원가입 스팸)에 선택적으로 추가하는 것이 합리적이며, 모든 폼에 기본으로 까는 것은 과잉 설계일 수 있습니다.

성장 단계별 rate limit 정책의 재조정

이 글이 제시한 설계는 “한 번 설정하고 끝내는 것”이 아니라, 서비스의 성장 단계에 따라 재조정이 필요한 살아있는 정책입니다. 초기 단계(사용자가 적고 트래픽이 낮은 시기)에는 오탐의 비용이 상대적으로 크게 느껴집니다—정상 사용자 한 명을 잘못 차단하는 것이 전체 사용자 기반에서 차지하는 비중이 크기 때문입니다. 이 시기에는 한도를 다소 느슨하게 설정하고, 관측을 통해 실제 남용 패턴을 학습하는 데 집중하는 것이 합리적입니다.

서비스가 성장하여 트래픽이 늘어나면, 반대로 남용의 절대적 규모(스팸 메일 발송량, 봇 트래픽으로 인한 서버 비용)가 커지기 시작합니다. 이 시기에는 초기에 학습한 관측 데이터를 근거로 한도를 점진적으로 조정하고, 경로별로 차등화된 정책을 도입하는 것이 자연스러운 다음 단계입니다. 이 재조정을 “처음에 잘못 설정했다”는 실패로 여기지 않고, 트래픽 패턴이 실제로 변화했다는 신호로 받아들이는 것이 정책을 건강하게 유지하는 태도입니다.

클라이언트 측 신호의 보조적 활용

지금까지의 논의는 서버·엣지 측에서 관측 가능한 신호(요청 빈도, IP, 헤더)에 집중했지만, 클라이언트 측에서 수집할 수 있는 보조 신호—예를 들어 폼 작성에 걸린 시간, 마우스 움직임의 존재 여부, 페이지 체류 시간—를 rate limit의 키나 판단 로직에 보조적으로 결합하는 접근도 존재합니다. 이런 신호는 서버 단독으로는 얻을 수 없는 정보를 제공하지만, 클라이언트 측 코드는 근본적으로 조작 가능하다는 한계를 항상 지닙니다. 따라서 이런 신호는 “신뢰할 수 있는 판단 근거”가 아니라 “의심도를 조정하는 보조 가중치”로만 활용해야 하며, 이 신호에만 의존해 서버 측 검증을 생략하는 것은 위험합니다.


6. 한계

본 글이 제시하는 설계는 다음과 같은 한계를 명확히 인정합니다.

분산 프록시와 주거용 IP 풀: 공격자가 대규모 주거용 IP 풀(residential proxy pool)을 사용하면, 각 요청이 서로 다른 IP에서 오는 것처럼 보이기 때문에 IP 기반 키는 무력화됩니다. 이 경우 행동 기반 신호(요청 간격의 규칙성, 헤더 조합의 이상성 등)가 필요하지만, 이는 본 글의 범위를 넘어서는 별도의 전문 영역입니다.

느린 인간형 자동화: 요청 사이에 무작위 지연을 두고, 사람과 유사한 마우스 이동 패턴을 흉내 내는 자동화는 단순 빈도 기반 rate limit으로 탐지되지 않습니다. 빈도가 이미 사람의 범위 안에 있기 때문입니다.

계정 탈취와 로직 버그: 유출된 자격 증명을 이용한 정상 속도의 로그인, 또는 IDOR(Insecure Direct Object Reference)와 같은 권한 부여 로직의 버그는 rate limit의 관할 밖에 있습니다. 이들은 미들웨어가 아니라 애플리케이션 로직 내부에서 해결되어야 합니다.

“보안 완료”라는 인지적 함정: 가장 실무적으로 위험한 한계는 기술적인 것이 아니라 인지적인 것입니다. rate limit을 배포한 뒤 이를 하나의 체크박스로 취급하면, 팀은 핸들러 내부의 auth() 호출 누락이나 입력 검증 부재와 같은 더 근본적인 문제를 놓칠 수 있습니다. rate limit은 방어의 한 계층일 뿐이며, 다른 계층의 부재를 가려서는 안 됩니다.

이러한 한계를 인정한다는 것은 rate limit이 무의미하다는 뜻이 아닙니다. 오히려 반대로, 이 한계를 명확히 알고 있을 때 rate limit이 정확히 무엇을 담당하고 무엇을 담당하지 않는지에 대한 팀의 기대치가 현실적으로 조정됩니다. 이는 이후 보안 인시던트가 발생했을 때 “rate limit이 있는데 왜 뚫렸냐”는 잘못된 질문 대신, “이 인시던트는 원래 rate limit의 관할 밖이었는가”라는 정확한 질문을 하게 만듭니다.

측정의 한계: 이 글이 제시한 다섯 변수 분석틀 역시 정량적으로 검증된 것이 아니라 반복되는 실무 관찰에서 귀납된 구조입니다. 어떤 키 설계나 윈도우 크기가 “최적”인지에 대한 일반화된 수치는 존재하지 않으며, 각 서비스는 자신의 트래픽 패턴과 비즈니스 리스크에 맞춰 이 다섯 변수를 실험적으로 조정해야 합니다. 이 글은 조정의 방향을 제시했을 뿐, 조정의 최종 값을 대신 결정해 주지는 않습니다.


7. 결론

엣지 rate limit은 완벽한 봇 차단기가 아니라, 쓰기 경로의 남용 기댓값을 낮추는 방지턱입니다. 본 글은 이 방지턱을 설계하는 문제를 키·윈도우·저장소·응답·관측이라는 다섯 변수로 분해하고, 각 변수의 선택이 다른 변수와 어떻게 상호작용하는지를 서술했습니다. 보호 경로를 좁히는 것에서 시작해, 키 설계에서 정상 사용자의 오탐을 줄이고, 윈도우와 알고리즘에서 봇과 사람 사이의 간격을 확보하고, 저장소의 일관성과 실패 모드를 명시적으로 선택하고, 429 응답과 화이트리스트로 정직하게 소통하며, 관측으로 이 모든 결정을 데이터에 근거해 반복적으로 조정하는 흐름이 본 글이 제안하는 설계 절차입니다.

이 절차의 위에서 CAPTCHA나 WAF와 같은 도구는 선택적 강화 계층으로 추가될 수 있지만, 그 자체가 설계의 시작점이 되어서는 안 됩니다. 그리고 무엇보다, 이 모든 장치가 인증·인가·입력 검증이라는 애플리케이션 내부의 근본적인 방어를 대체할 수 없다는 점을 결론으로 다시 강조합니다. 구현에 사용하는 라이브러리와 KV 제품은 시간이 지나면 바뀌겠지만, 본 글이 제시한 다섯 변수의 질문 순서는 코드 리뷰에서 반복해서 물을 만한 가치가 있는 질문 세트로 남을 것입니다.

사이드 프로젝트가 처음 스팸 트래픽을 마주하는 시점은 대개 예고 없이 찾아옵니다. 그 시점에 급하게 임의의 숫자로 한도를 설정하기보다, 이 글이 제시한 순서—경로를 좁히고, 키를 정의하고, 저장소와 실패 모드를 선택하고, 응답과 화이트리스트를 설계하고, 관측으로 마무리하는 순서—를 미리 한 번 훑어 두는 것이, 실제로 그 순간이 왔을 때 감이 아니라 근거로 대응할 수 있게 해주는 최소한의 준비입니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.