Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 31 |
Tags
- Be
- 국비지원교육
- 오픈챌린지
- 백엔드개발자
- OPENPATH
- UXUI챌린지
- JPA
- 자바
- 디자인교육
- 패스트캠퍼스
- Java
- 환급챌린지
- Spring
- 오픈패스
- 오블완
- 국비지원취업
- 객체지향
- 백준
- mysql
- KDT
- 디자인강의
- UXUI기초정복
- 부트캠프
- 내일배움카드
- UXUIPrimary
- 디자인챌린지
- 국비지원
- 티스토리챌린지
- baekjoon
- 백엔드 부트캠프
Archives
- Today
- Total
군만두의 IT 개발 일지
[스터디9] 06. 처리율 제한 장치의 설계 - 실습 본문
목차
4장. 처리율 제한 장치의 설계
처리율 제한 장치의 사용 예시
- 초당 게시글 작성 횟수 제한: 사용자가 초당 2회 이상 게시글을 등록할 수 없도록 설정
- IP별 계정 생성 횟수 제한: 같은 IP 주소에서 하루에 10개 이상의 계정 생성 시도를 제한
- 디바이스별 리워드 요청 제한: 동일한 디바이스에서 주당 5회 이상 리워드 요청이 불가하도록 설계
- API 호출 횟수 제한: 예를 들어, 트위터 API는 3시간 동안 300개 트윗만 올릴 수 있도록 제한함
- 분당 읽기 요청 제한: 구글 Docs API는 사용자당 분당 300번의 읽기 요청만 허용함
이외에도 일상 생활에서의 예시가 있을지 고민하다가 티켓팅 시스템의 처리율 제한 장치를 간단히 구현해보기로 했다.
1단계 문제 이해 및 설계 범위 확정
Q: 초당 수만 건의 티켓 구매 요청이 몰리는 상황에서 어떻게 시스템을 보호할까?
A: Rate Limiter를 통한 트래픽 제어, 적절한 알고리즘 선택, Redis 기반 분산 처리, Fail-safe 전략
비즈니스 요구사항
- 티켓팅 서비스: IU 콘서트처럼 인기 티켓 오픈 시 트래픽 급증
- 공정성: 동일한 사용자가 시스템을 독점하지 못하도록 제한
- 가용성: Rate Limiter 장애가 전체 서비스를 다운시키면 안 됨
- 확장성: 여러 서버에서 일관된 Rate Limiting 적용
기술적 요구사항
- 응답시간: Rate Limit 검사는 50ms 이하
- 메모리 효율: 사용자 당 최소 메모리 사용
- 정확성: 분산 환경에서도 정확한 카운팅
- 유연성: 다양한 제한 규칙 지원
2단계 개략적 설계안 제시 및 동의 구하기

Q: Rate Limiter를 어디에 배치할까?
A:
- 클라이언트측: 신뢰할 수 없음 (조작 가능)
- 서버측: ✅ 안전하고 정확한 제어
- 미들웨어/게이트웨이: 좋은 선택지이지만 지연시간 증가
Rate Limiter 알고리즘 비교
| 알고리즘 | 메모리 효율 | 정확도 | 버스트 허용 | 구현 복잡도 | 티켓팅 적합도 |
| Token Bucket | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 최적 |
| Leaky Bucket | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Fixed Window | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| Sliding Window Log | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
🎫 Token Bucket을 선택한 이유
- 티켓 오픈 순간의 버스트 트래픽을 자연스럽게 처리 가능
- 사용자에게 "여러 번의 기회"를 주는 직관적인 모델
- Redis와 Lua 스크립트로 효율적 구현 가능
3단계 상세 설계
Token Bucket 동작 원리
- 토큰 저장소: 각 사용자별로 토큰 버킷 생성
- 토큰 보충: 일정한 속도(refill rate)로 토큰 추가
- 요청 처리: 토큰이 있으면 소모하고 요청 허용
- 요청 거부: 토큰이 없으면 Rate Limit 발생
1) 티켓 구매: 용량 3개, 초당 1개 보충
초기: [🪙🪙🪙] → 1번째 구매 → [🪙🪙] → 2번째 구매 → [🪙] → 3번째 구매 → []
4번째 구매 시도 → RATE_LIMITED
60초 후: [🪙🪙🪙]
2) 일반 조회: 용량 10개, 초당 2개 보충
3) IP 기반: 용량 20개, 초당 5개 보충 (익명 사용자)
애플리케이션 설정 (application.yml)
# Rate Limiter 설정
rate-limiter:
ticket-purchase:
capacity: 3 # 3개 용량
refill-rate: 1 # 초당 1개 보충
window-seconds: 60
user-request:
capacity: 10 # 10개 용량
refill-rate: 2 # 초당 2개 보충
window-seconds: 60
spring:
data:
redis:
url: redis://localhost:6379
timeout: 2000ms
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
4단계 마무리
API 엔드포인트
| Method | Endpoint | 설명 | Rate Limit |
| GET | /api/tickets | 티켓 목록 조회 | 사용자 요청 (10개/분) |
| POST | /api/tickets/purchase | 티켓 구매 | 티켓 구매 (3개/분) |
| GET | /api/tickets/rate-limit-status | Rate Limit 상태 | 사용자 요청 (10개/분) |
| GET | /api/test/rate-limit | 테스트용 엔드포인트 | 사용자 요청 |
Token Bucket Rate Limiter 서비스
@Service
public class TokenBucketRateLimiter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisScript<Long> tokenBucketScript;
public boolean isAllowed(String key, int capacity, int refillRate, int requestedTokens) {
String redisKey = "token_bucket:" + key;
long currentTime = System.currentTimeMillis() / 1000;
try {
Long result = redisTemplate.execute(
tokenBucketScript,
List.of(redisKey),
String.valueOf(capacity),
String.valueOf(refillRate),
String.valueOf(currentTime),
String.valueOf(requestedTokens)
);
if (result == null) {
logger.error("Redis script returned null for key: {}", key);
return false; // Fail Closed
}
return result == 1;
} catch (Exception e) {
logger.error("Rate limiter error for key: " + key, e);
return simpleRateLimit(key, capacity);
}
}
// 기본 1개 토큰 요청
public boolean isAllowed(String key, int capacity, int refillRate) {
return isAllowed(key, capacity, refillRate, 1);
}
}
Lua 스크립트 (Redis 원자적 연산)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local currentTime = tonumber(ARGV[3])
local requestedTokens = tonumber(ARGV[4])
-- 현재 토큰 정보 가져오기
local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1]) or capacity
local lastRefill = tonumber(bucket[2]) or currentTime
-- 토큰 보충 계산
local tokensToAdd = math.floor((currentTime - lastRefill) * refillRate)
tokens = math.min(capacity, tokens + tokensToAdd)
-- 요청된 토큰 수만큼 사용 가능한지 확인
if tokens >= requestedTokens then
-- 토큰 소모
tokens = tokens - requestedTokens
-- Redis에 업데이트
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', currentTime)
redis.call('EXPIRE', key, 3600) -- 1시간 TTL
return 1 -- 허용
else
-- 현재 상태만 업데이트 (토큰은 소모하지 않음)
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', currentTime)
redis.call('EXPIRE', key, 3600)
return 0 -- 거부
end
Spring Interceptor
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
@Autowired
private RateLimitService rateLimitService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String userId = extractUserId(request);
String requestURI = request.getRequestURI();
String clientIp = rateLimitService.getClientIpAddress(request);
boolean allowed = true;
String limitType = "";
try {
// 티켓 구매 API - 엄격한 제한
if (requestURI.contains("/tickets/purchase")) {
limitType = "ticket_purchase";
if (userId != null) {
allowed = rateLimitService.isTicketPurchaseAllowed(userId);
} else {
allowed = rateLimitService.isIpRequestAllowed(clientIp);
}
}
// 일반 API 요청
else {
limitType = "user_request";
if (userId != null) {
allowed = rateLimitService.isUserRequestAllowed(userId);
} else {
allowed = rateLimitService.isIpRequestAllowed(clientIp);
}
}
} catch (Exception e) {
logger.error("Rate limit check failed - blocking for safety", e);
allowed = false; // Fail Closed
}
if (!allowed) {
sendRateLimitErrorResponse(response, limitType);
return false;
}
return true;
}
private String extractUserId(HttpServletRequest request) {
// 헤더에서 사용자 ID 추출
return request.getHeader("X-User-Id");
}
}
Rate Limit 서비스
@Service
public class RateLimitService {
@Autowired
private TokenBucketRateLimiter tokenBucketLimiter;
@Autowired
private RateLimitConfig rateLimitConfig;
/**
* 티켓 구매 요청 제한 확인
*/
public boolean isTicketPurchaseAllowed(String userId) {
RateLimitConfig.TicketPurchase config = rateLimitConfig.getTicketPurchase();
String key = "ticket_purchase:" + userId;
return tokenBucketLimiter.isAllowed(
key,
config.getCapacity(),
config.getRefillRate()
);
}
/**
* 일반 사용자 요청 제한 확인
*/
public boolean isUserRequestAllowed(String userId) {
RateLimitConfig.UserRequest config = rateLimitConfig.getUserRequest();
String key = "user_request:" + userId;
return tokenBucketLimiter.isAllowed(
key,
config.getCapacity(),
config.getRefillRate()
);
}
/**
* IP 기반 요청 제한 (익명 사용자)
*/
public boolean isIpRequestAllowed(String ipAddress) {
String key = "ip_request:" + ipAddress;
return tokenBucketLimiter.isAllowed(key, 20, 5); // 20개 용량, 초당 5개 보충
}
}
Docker 구성
version: '3.8'
services:
redis:
image: redis:7-alpine
container_name: ticketing-redis
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- ticketing-network
ticketing-app:
build: .
container_name: ticketing-service
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
depends_on:
- redis
networks:
- ticketing-network
restart: unless-stopped
volumes:
redis_data:
networks:
ticketing-network:
driver: bridge
성능 최적화 전략
- Connection Pooling: 최대 10개 연결로 효율적 관리
- Lua 스크립트 캐싱: Redis에서 스크립트 재사용으로 성능 향상
- TTL 자동 정리: 1시간 후 자동으로 메모리 해제
- Fail-Closed 전략: Redis 장애 시 안전한 차단으로 시스템 보호
예상 성능 지표
- 처리량: 초당 1,000+ TPS
- 응답시간: 평균 30ms, 99%ile 50ms
- 메모리: 사용자당 약 100B
- 동시 사용자: 10,000+ 명
자동화 테스트 스크립트
# 티켓 구매 Rate Limit 테스트
$purchaseSuccess = 0
$purchaseFailed = 0
for ($i = 1; $i -le 6; $i++) {
Write-Host "Purchase Request $i" -NoNewline
try {
$headers = @{
"X-User-Id" = "test-user"
"Content-Type" = "application/json"
}
$body = @{
ticketId = "TICKET_001"
quantity = 1
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "$BASE_URL/api/tickets/purchase" -Method POST -Headers $headers -Body $body
Write-Host " - SUCCESS" -ForegroundColor Green
$purchaseSuccess++
} catch {
$statusCode = $_.Exception.Response.StatusCode.value__
if ($statusCode -eq 429) {
Write-Host " - RATE LIMITED" -ForegroundColor Red
$purchaseFailed++
}
}
Start-Sleep -Milliseconds 300
}
# 예상 결과: 1-3번 SUCCESS, 4-6번 RATE_LIMITED
Fail-Safe 전략
Q: Rate Limiter가 실패하면 어떻게 할까?
A: Fail-Safe 전략 사용
- Fail-Closed: Redis 장애 시 모든 요청 차단
- Circuit Breaker: 연속 실패 시 자동으로 우회 (미구현)
- Local Fallback: 간단한 in-memory 카운터로 대체
- 모니터링 알림: 즉시 장애 알림으로 빠른 복구 (미구현)
분산 환경 대응
Q: 여러 데이터센터에서 Rate Limiter를 운영한다면?
A: 분산 전략 사용
- Redis Cluster: 샤딩을 통한 수평 확장
- Eventually Consistent: 약간의 불일치는 허용하되 전체적인 제한 유지 (미구현)
- Regional Limit: 지역별로 독립적인 Rate Limit 적용 (미구현)
- Global + Local: 글로벌 한도와 로컬 한도를 조합 (미구현)
실제 테스트 결과
티켓 구매 Rate Limit 테스트

일반 요청 Rate Limit 테스트

Token Recovery 확인

최종 결과

면접 예상 질문 및 답변
Q1: Rate Limiter 알고리즘을 어떻게 선택할까?
A: 비즈니스 요구사항에 따라 선택한다.
- Token Bucket: 일반적인 웹 서비스 (버스트 허용 중요)
- Leaky Bucket: 안정적인 처리율이 중요한 결제 시스템
- Fixed Window: 대용량 트래픽, 단순한 구현이 필요한 경우
- Sliding Window: 정확한 제한이 필요한 SLA 보장 서비스
Q2: 분산 환경에서 정확한 카운팅을 어떻게 보장할까?
A: Redis + Lua 스크립트로 원자적 연산을 보장하고, Eventually Consistent 전략으로 약간의 불일치는 허용하되 전체적인 제한은 유지합니다.
Q3: Redis가 다운되면 어떻게 할까?
A: Fail-Closed 전략을 채택하여 안전을 우선시하고, 간단한 local cache로 fallback하며 Circuit Breaker 패턴으로 자동 복구를 지원한다.
참고 자료
- Vagelis Bisbikis, "Fan-Out and Fan-In Patterns: Building a Personalized Feed in Laravel", 2025.02.10, https://medium.com/@vagelisbisbikis/fan-out-and-fan-in-patterns-building-a-personalized-feed-in-laravel-676515f65e03
- dewble, "[Architecture]처리율 제한 장치 설계(Rate limiting)", 2023.07.08, https://dewble.tistory.com/entry/designing-a-rate-limiter
- 박종훈, "API 제한 설정하기 - 처리율 제한 장치의 설계", 2023.05.16, https://jonghoonpark.com/2023/05/17/처리율-제한-장치-설계
- 민동현, "처리율 제한 장치의 설계", 2022.03.18, https://donghyeon.dev/인프라/2022/03/18/처리율-제한-장치의-설계/
- aal2525, "가상 면접 사례로 배우는 대규모 시스템 설계 기초 – 처리율 제한 장치", 2025.02.16, https://velog.io/@aal2525/가상-면접-사례로-배우는-대규모-시스템-설계-기초-처리율-제한-장치의-설계

이 글은 『 가상 면접 사례로 배우는 대규모 시스템 설계 기초』 책을 학습한 내용을 정리한 것입니다.
'학습일지 > 시스템 설계' 카테고리의 다른 글
| [스터디9] 09. 분산 시스템을 위한 유일 ID 생성기 설계 (0) | 2025.09.06 |
|---|---|
| [스터디9] 08. 키-값 저장소 설계 (1) | 2025.08.28 |
| [스터디9] 05. 처리율 제한 장치의 설계 (0) | 2025.07.26 |
| [스터디9] 04. 뉴스 피드 시스템 설계 - 실습2 (1) | 2025.07.08 |
| [스터디9] 03. 뉴스 피드 시스템 설계 - 실습1 (0) | 2025.06.27 |
Comments