티스토리 뷰
백엔드에서 Rate Limiting은 왜 필요할까? 429 응답, Retry-After, IP·사용자 단위 제한까지 FastAPI · Spring Boot · Node.js 실전 정리
octo54 2026. 6. 17. 12:00백엔드에서 Rate Limiting은 왜 필요할까? 429 응답, Retry-After, IP·사용자 단위 제한까지 FastAPI · Spring Boot · Node.js 실전 정리
백엔드 운영을 조금만 해보면, “장애를 어떻게 복구할까”보다 먼저 부딪히는 질문이 하나 있어요.
“애초에 과도한 요청을 어떻게 막을까?”
저도 이 구간을 꽤 늦게 체감했어요.
처음엔 기능만 잘 만들면 될 줄 알았거든요. 그런데 로그인 API, 검색 API, 문자 발송 API, AI 호출 API 같은 게 붙기 시작하면, “정상 사용자 트래픽”이 아니라 짧은 시간에 몰리는 요청 자체가 문제가 되더라고요.
- 같은 사용자가 버튼을 연속 클릭하는 경우
- 봇이 로그인 엔드포인트를 계속 두드리는 경우
- 비싼 외부 API를 붙인 엔드포인트가 과도하게 호출되는 경우
- 특정 클라이언트 하나가 서버 자원을 너무 많이 가져가는 경우
이럴 때 필요한 게 Rate Limiting입니다.
RFC 6585는 429 Too Many Requests 상태 코드를 정의하면서, 이것이 “주어진 시간 동안 너무 많은 요청을 보냈다”는 뜻이라고 설명합니다. 또 응답에는 상황을 설명하는 내용과 함께 Retry-After 헤더를 포함할 수 있다고 안내합니다. (IETF Datatracker)
이번 글에서는 이걸 FastAPI, Spring Boot, Node.js 기준으로 정리해보겠습니다.
한 줄 요약
Rate Limiting은 특정 시간 동안 허용할 요청 수를 제한하는 보호 장치입니다. 구현 자체는 FastAPI 미들웨어, Spring HandlerInterceptor, Express 미들웨어처럼 요청이 컨트롤러에 들어가기 전 공통 계층에 두는 게 가장 자연스럽고, 제한을 넘겼을 때는 보통 429 Too Many Requests와 Retry-After 헤더를 함께 내려주는 방식으로 시작합니다. FastAPI는 미들웨어를 요청/응답 공통 처리 지점으로 설명하고, Spring은 HandlerInterceptor의 preHandle이 핸들러 실행 전 요청을 가로챌 수 있다고 설명하며, Express도 애플리케이션 및 라우터 레벨 미들웨어를 공식 지원합니다. (IETF Datatracker)
Rate Limiting이란?
Rate Limiting은 말 그대로 요청 속도 제한입니다.
아주 단순하게 보면 이런 규칙이에요.
- 1분에 최대 60번
- 10초에 최대 5번
- 사용자당 1시간에 최대 3번
- IP당 초당 10번
RFC 6585는 서버가 너무 많은 요청을 받았을 때 429 상태 코드로 응답할 수 있다고 설명합니다. MDN도 429는 “주어진 시간 안에 너무 많은 요청을 보냈다”는 뜻이며, 흔히 이런 메커니즘을 rate limiting이라고 부른다고 설명합니다. (IETF Datatracker)
즉 Rate Limiting은 단순히 “막는다”가 아니라,
자원을 공정하게 쓰게 하고, 비싼 기능을 보호하고, 장애 전파를 줄이는 장치에 가깝습니다. (IETF Datatracker)
왜 필요한가
이건 실제 운영에서 생각보다 빨리 필요해집니다.
예를 들어 이런 경우를 생각해볼 수 있어요.
- 로그인 API에 무차별 대입 요청이 들어옴
- 회원가입 인증번호 발송 API가 과하게 호출됨
- 검색 API가 너무 자주 불려 DB를 압박함
- AI 요약 API가 비싼 외부 비용을 유발함
- 결제 준비 API를 프론트가 실수로 여러 번 연속 호출함
429는 이런 과도한 호출을 “이제 좀 천천히 해”라고 돌려보내는 표준적인 방법 중 하나입니다. RFC 6585는 Retry-After 헤더를 함께 내려 클라이언트가 얼마나 기다려야 하는지 알려줄 수 있다고 설명합니다. (IETF Datatracker)
저는 개인적으로 Rate Limiting이 “보안 기능”이면서 동시에 “운영 기능”이라고 느껴요.
공격 방어에도 쓰이지만, 정상 사용자의 실수성 폭주 요청도 꽤 잘 막아주거든요. (IETF Datatracker)
어디 기준으로 제한해야 할까
여기서 많이 헷갈립니다.
Rate Limiting은 보통 이런 기준 중 하나로 잡습니다.
- IP 기준
- 사용자 ID 기준
- API 키 기준
- 엔드포인트 기준
- 조합 기준(IP + path, user + path)
초기엔 보통 IP 기준이나 사용자 기준으로 많이 시작합니다.
다만 로그인 전 API는 사용자 ID가 없으니 IP 기준이 더 자연스럽고, 로그인 후 API는 사용자 기준이 더 공정할 때가 많아요. 이처럼 “어디를 키로 삼아 제한할지”는 미들웨어나 인터셉터처럼 공통 요청 처리 계층에서 잡는 게 구현이 깔끔합니다. FastAPI 미들웨어, Spring HandlerInterceptor, Express 미들웨어는 모두 이런 공통 처리에 적합한 지점이라고 공식 문서가 설명합니다. (FastAPI)
429 응답은 어떻게 내려야 할까
기본 감각은 이렇습니다.
- 상태 코드: 429 Too Many Requests
- 응답 바디: 사람이 읽을 수 있는 메시지
- 헤더: 가능하면 Retry-After
RFC 6585는 429 응답이 요청이 너무 많음을 의미하고, Retry-After를 줄 수 있다고 설명합니다. MDN도 같은 방향으로 설명합니다. (IETF Datatracker)
예시는 대략 이런 느낌입니다.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{"message":"요청 한도를 초과했습니다. 60초 후 다시 시도해주세요."}
이 작은 디테일이 꽤 중요해요.
그냥 막는 것보다 “언제 다시 시도하면 되는지”까지 알려주는 쪽이 훨씬 운영 친화적입니다. (IETF Datatracker)
알고리즘은 뭘 써야 할까
실무에선 여러 방식이 있지만, 초반엔 고정 윈도우(Fixed Window) 만 알아도 충분히 시작할 수 있습니다.
예를 들면:
- 현재 1분 창에서 호출 수를 센다
- 1분에 60회를 넘으면 막는다
- 다음 1분이 시작되면 카운터를 초기화한다
이 방식은 구현이 단순해서 교육용, 초기 서비스, 간단한 내부 API에 꽤 잘 맞습니다.
물론 더 정교하게 가려면 sliding window나 token bucket 같은 알고리즘도 보지만, 이번 글은 “바로 붙일 수 있는 시작점”에 집중하겠습니다.
검색 잘 되는 기술 글도 글 하나에 검색 의도 하나만 잡는 구성이 강하다고 정리되어 있고, 이 시리즈도 그 방향으로 가는 게 좋아 보여요.
1) FastAPI에서 Rate Limiting 시작하기
FastAPI 공식 문서는 미들웨어를 모든 요청이 특정 path operation에 가기 전과 응답이 돌아오기 전에 공통 처리를 넣는 곳으로 설명합니다. 그래서 요청 제한 같은 기능을 붙이기에 아주 자연스럽습니다. (FastAPI)
설치 방법
pip install fastapi uvicorn
예제 코드
# main.py
import time
from collections import defaultdict, deque
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
# 60초 동안 최대 5번 허용
WINDOW_SECONDS = 60
MAX_REQUESTS = 5
# key -> 요청 시각 목록
request_store: dict[str, deque[float]] = defaultdict(deque)
def get_client_key(request: Request) -> str:
# 실제 운영에서는 X-Forwarded-For 처리나 사용자 ID 기준도 고려
return request.client.host if request.client else "unknown"
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
key = get_client_key(request)
now = time.time()
timestamps = request_store[key]
# 윈도우 밖 요청 제거
while timestamps and now - timestamps[0] > WINDOW_SECONDS:
timestamps.popleft()
if len(timestamps) >= MAX_REQUESTS:
retry_after = int(WINDOW_SECONDS - (now - timestamps[0])) if timestamps else WINDOW_SECONDS
return JSONResponse(
status_code=429,
content={
"message": "요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.",
"retry_after_seconds": retry_after,
},
headers={"Retry-After": str(max(retry_after, 1))},
)
timestamps.append(now)
return await call_next(request)
@app.get("/health")
def health():
return {"ok": True}
@app.get("/api/hello")
def hello():
return {"message": "hello"}
실행 방법
uvicorn main:app --reload
FastAPI에서 핵심 감각
여기서 중요한 건 미들웨어에 두었다는 점입니다.
FastAPI 공식 문서가 설명하듯 미들웨어는 모든 요청에 공통으로 적용되기 때문에, 로그인 API든 검색 API든 한 곳에서 제한 정책을 태우기 좋습니다. (FastAPI)
그리고 429 응답과 Retry-After를 같이 주는 방식은 RFC 6585의 취지와도 잘 맞습니다. (IETF Datatracker)
2) Spring Boot에서 Rate Limiting 시작하기
Spring Framework 문서는 HandlerInterceptor가 요청 흐름을 가로채는 데 유용하고, preHandle(..)이 실제 핸들러 실행 전에 호출된다고 설명합니다. 이건 요청 제한처럼 컨트롤러에 들어가기 전 차단이 필요한 기능과 잘 맞습니다. (Home)
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
인터셉터 구현
// RateLimitInterceptor.java
package com.example.demo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private static final long WINDOW_MILLIS = 60_000L;
private static final int MAX_REQUESTS = 5;
private final Map<String, Deque<Long>> requestStore = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String key = request.getRemoteAddr();
long now = System.currentTimeMillis();
requestStore.putIfAbsent(key, new ConcurrentLinkedDeque<>());
Deque<Long> timestamps = requestStore.get(key);
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > WINDOW_MILLIS) {
timestamps.pollFirst();
}
if (timestamps.size() >= MAX_REQUESTS) {
long retryAfter = timestamps.peekFirst() == null
? 60
: Math.max(1, (WINDOW_MILLIS - (now - timestamps.peekFirst())) / 1000);
response.setStatus(429);
response.setHeader("Retry-After", String.valueOf(retryAfter));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("""
{"message":"요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요."}
""");
return false;
}
timestamps.addLast(now);
return true;
}
}
인터셉터 등록
// WebConfig.java
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RateLimitInterceptor rateLimitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**");
}
}
컨트롤러
// DemoController.java
package com.example.demo;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class DemoController {
@GetMapping("/hello")
public Map<String, Object> hello() {
return Map.of("message", "hello");
}
}
실행 방법
./gradlew bootRun
Spring Boot에서 핵심 감각
Spring 쪽은 HandlerInterceptor가 정말 잘 맞습니다.
공식 문서가 말하는 preHandle(..)이 바로 “컨트롤러 들어가기 전에 가로채기” 지점이라서, Rate Limiting 같은 횡단 관심사를 적용하기 좋거든요. (Home)
그리고 path 기준으로 /api/**만 제한하는 식으로 범위를 좁히기 쉽다는 점도 꽤 실전적입니다. (Home)
3) Node.js에서 Rate Limiting 시작하기
Express 공식 문서는 미들웨어가 애플리케이션 레벨, 라우터 레벨에서 동작하며 요청-응답 사이클 중간에 공통 기능을 수행한다고 설명합니다. 또 커스텀 미들웨어 작성도 공식 가이드로 따로 제공합니다. 요청 제한은 이런 미들웨어 패턴과 아주 잘 맞습니다. (Express.js)
설치 방법
npm install express
예제 코드
// server.js
import express from "express";
const app = express();
const WINDOW_MS = 60 * 1000;
const MAX_REQUESTS = 5;
// key -> request timestamps
const requestStore = new Map();
function getClientKey(req) {
return req.ip || req.socket.remoteAddress || "unknown";
}
function rateLimitMiddleware(req, res, next) {
const key = getClientKey(req);
const now = Date.now();
const timestamps = requestStore.get(key) || [];
const validTimestamps = timestamps.filter((ts) => now - ts <= WINDOW_MS);
if (validTimestamps.length >= MAX_REQUESTS) {
const retryAfterSeconds = Math.max(
1,
Math.ceil((WINDOW_MS - (now - validTimestamps[0])) / 1000)
);
res.setHeader("Retry-After", String.valueOf(retryAfterSeconds));
return res.status(429).json({
message: "요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.",
retry_after_seconds: retryAfterSeconds,
});
}
validTimestamps.push(now);
requestStore.set(key, validTimestamps);
next();
}
app.use("/api", rateLimitMiddleware);
app.get("/health", (req, res) => {
res.json({ ok: true });
});
app.get("/api/hello", (req, res) => {
res.json({ message: "hello" });
});
app.listen(3000, () => {
console.log("server running on http://localhost:3000");
});
실행 방법
node server.js
Node.js에서 핵심 감각
Node.js는 이런 요청 제한 로직이 미들웨어로 분리되는 순간 훨씬 보기 좋아집니다.
Express 공식 문서도 미들웨어가 공통 요청 처리에 적합하다고 설명하고 있고, 이런 류의 로직은 라우트 핸들러 안에 넣기보다 app.use("/api", ...) 식으로 묶는 게 훨씬 관리가 쉽습니다. (Express.js)
고정 윈도우 방식의 한계
여기까지 예제는 전부 교육용으로는 충분하지만, 운영에선 한계가 있습니다.
예를 들면 이런 문제들이 있어요.
- 서버가 여러 대면 메모리 상태가 공유되지 않음
- 재시작하면 카운터가 초기화됨
- 동일 IP 뒤에 여러 사용자가 있을 수 있음
- 윈도우 경계에서 몰아서 보내는 요청에 약할 수 있음
그래서 운영에선 Redis 같은 공용 저장소나 더 정교한 알고리즘을 붙이는 경우가 많습니다.
다만 지금 단계에선 “어디 계층에 두는지”, “429와 Retry-After를 어떻게 반환하는지”, “IP 기준/사용자 기준을 어떻게 생각하는지” 감각을 먼저 잡는 게 더 중요합니다.
어디에 적용해야 할까
모든 API에 똑같이 거는 게 정답은 아닙니다.
오히려 이런 식으로 나누는 편이 좋습니다.
강하게 걸어야 할 곳
- 로그인
- 회원가입
- 비밀번호 재설정
- 인증번호 발송
- AI 호출
- 외부 비용이 큰 API
상대적으로 느슨해도 되는 곳
- 건강 상태 확인
- 내부 관리자 API
- 정적 데이터 조회
즉 Rate Limiting도 결국 비용과 위험이 큰 엔드포인트를 보호하는 정책입니다.
RFC 6585는 429가 너무 많은 요청을 의미한다고 설명하지만, 어디에 어떤 기준으로 적용할지는 애플리케이션 설계의 몫입니다. (IETF Datatracker)
실무에서 자주 하는 실수
1. 429만 내려주고 Retry-After는 안 줌
클라이언트 입장에선 언제 다시 시도해야 할지 모르게 됩니다. RFC 6585는 Retry-After를 둘 수 있다고 분명히 설명합니다. (IETF Datatracker)
2. 모든 엔드포인트에 똑같은 제한
로그인 API와 단순 조회 API는 비용과 위험이 다릅니다.
3. 라우트 핸들러마다 직접 구현
이건 금방 중복됩니다. 미들웨어나 인터셉터가 훨씬 자연스럽습니다. FastAPI, Spring, Express 문서 모두 이런 공통 계층을 공식 지원합니다. (FastAPI)
4. 운영 환경에서 프록시/IP 고려를 안 함
request.client.host, getRemoteAddr(), req.ip만 무조건 믿으면 실제 사용자 식별이 꼬일 수 있습니다. 프록시 뒤 환경이라면 별도 신뢰 설정이 필요합니다.
5. 제한은 걸었는데 모니터링을 안 함
어느 IP가 자주 막히는지, 어떤 API가 429를 많이 내는지 봐야 정책이 맞는지 조정할 수 있습니다.
FAQ
Q. Rate Limiting은 꼭 429로 응답해야 하나요?
429는 “짧은 시간에 너무 많은 요청”을 표현하는 표준적인 상태 코드입니다. RFC 6585가 이를 정의하고 있고, 일반적인 rate limiting 구현에서 가장 많이 씁니다. 다만 RFC는 429 사용을 강제하지는 않는다고도 설명합니다. (IETF Datatracker)
Q. Retry-After는 꼭 넣어야 하나요?
필수는 아니지만 강력 추천입니다. RFC 6585와 MDN 모두 429 응답에 Retry-After를 포함할 수 있다고 설명합니다. 클라이언트 친화적인 구현을 하려면 넣는 쪽이 좋습니다. (IETF Datatracker)
Q. FastAPI에서는 미들웨어가 제일 좋은가요?
대부분의 공통 요청 제한 로직엔 그렇습니다. FastAPI 공식 문서도 미들웨어가 모든 요청과 응답 전후 공통 처리에 적합하다고 설명합니다. (FastAPI)
Q. Spring Boot에서는 Filter보다 Interceptor가 나은가요?
요청 제한처럼 MVC 핸들러 진입 전에 공통 제어가 필요한 경우 HandlerInterceptor가 아주 자연스럽습니다. Spring 공식 문서도 preHandle(..)로 핸들러 실행 전 가로채기를 설명합니다. (Home)
Q. Node.js에서는 라이브러리 없이도 시작할 수 있나요?
네. Express 공식 미들웨어 패턴만으로도 고정 윈도우 방식의 기본 제한은 충분히 시작할 수 있습니다. 다만 운영으로 가면 공용 저장소나 전문 라이브러리를 검토하는 경우가 많습니다. (Express.js)
핵심 요약
- Rate Limiting은 특정 시간 동안 허용할 요청 수를 제한하는 보호 장치입니다. (IETF Datatracker)
- 제한을 넘기면 보통 429 Too Many Requests로 응답하고, 가능하면 **Retry-After**도 함께 내려주는 쪽이 좋습니다. (IETF Datatracker)
- FastAPI는 미들웨어, Spring Boot는 HandlerInterceptor, Node.js는 Express 미들웨어에 두는 게 가장 자연스럽습니다. (FastAPI)
- 처음엔 고정 윈도우 방식으로도 충분히 시작할 수 있고, 운영으로 가면 공용 저장소와 더 정교한 정책을 검토하면 됩니다.
- 검색에 잘 걸리는 개발 글은 질문형 제목, 첫 문단 정답, 정의 문장, 실행 코드, FAQ, 추천 태그 구조가 강합니다.
출처
- RFC 6585 — 429 Too Many Requests 정의 및 Retry-After 언급. (IETF Datatracker)
- MDN — 429 Too Many Requests 설명. (MDN 웹 문서)
- FastAPI 공식 문서 — Middleware. (FastAPI)
- Spring Framework 공식 문서 — Interception / HandlerInterceptor. (Home)
- Express 공식 문서 — Using middleware / Writing middleware. (Express.js)
- 글 구성 참고: 업로드하신 “AI 검색에 잘 걸리는 글” 가이드.
백엔드개발, FastAPI, SpringBoot, Nodejs, Express, RateLimiting, 429TooManyRequests, RetryAfter, API보호, 백엔드시리즈
'study > 백엔드' 카테고리의 다른 글
- Total
- Today
- Yesterday
- 쿠버네티스
- Python
- rag
- REACT
- PostgreSQL
- 웹개발
- 주니어개발자
- SEO최적화
- 백엔드개발
- NestJS
- node.js
- 개발블로그
- Prisma
- LangChain
- fastapi
- Next.js
- seo 최적화 10개
- flax
- nextJS
- JAX
- Express
- llm
- JWT
- kotlin
- nodejs
- DevOps
- CI/CD
- 생성형AI
- SpringBoot
- 딥러닝
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
