현재 프로젝트 BUKAE(부캐)의 서버 개발을 담당하고 있다.
BUKAE는 여러 외부 API에 의존하는 구조를 가지고 있다. 그중 일부 API는 서비스의 핵심 기능과 직접적으로 연결되어 있으며, 호출 정책을 위반할 경우 단순한 오류가 아니라 서비스 전체 기능이 중단될 수 있는 리스크를 내포하고 있다.
🚨 문제 상황: 외부 API Rate Limit과 24시간 Ban
BUKAE에서는 상품 정보를 정확하게 조회하기 위해 외부 커머스 API를 사용하고 있다. 개발 중 API 문서를 확인하던 과정에서 다음과 같은 정책을 확인했다.
- 분당 최대 50회 호출 가능
- 제한 초과 시 24시간 동안 API 호출 차단
이 정책은 단순히 "잠시 후 다시 시도하라"는 응답을 주는 수준이 아니었다. 한 번의 실수로 하루 동안 서비스의 핵심 기능이 완전히 마비될 수 있는 구조였다.
트래픽이 몰리는 순간은 예측하기 어렵고, 51번째 요청을 서버단에서 제어하지 못하면 24시간 장애로 이어진다. 즉, 이 문제는 try-catch나 retry 같은 사후 대응이 아니라 사전에 차단해야 하는 설계 문제였다.
🤔 선택지: Redis vs Local Rate Limit
검색을 해본 결과 외부 API Rate Limit을 구현하는 방식은 크게 두 가지가 있는 것 같았다.
1. Redis 기반 분산 Rate Limiter
모든 서버가 Redis를 통해 호출 횟수를 공유하는 정석적인 방식이다. 서버가 여러 대로 확장되어도 정확한 제어가 가능하지만, 현재 단일 서버 구조인 BUKAE에게는 인프라 비용 증가와 새로운 장애 지점(SPOF) 추가라는 단점이 더 컸다.
2. Local Memory 기반 Rate Limiter
각 서버 인스턴스의 메모리에서 호출 횟수를 관리한다. 외부 의존성이 없고 네트워크 비용이 0이라는 장점이 있다.
현재 서버 애플리케이션은 단일 인스턴스 환경으로 추가로 Redis를 도입하는것은 비용문제도 있고 이용자 수를 고려했을때 오버엔지니어링이라 판단했다.
그래서 Rate Limiter를 사용하기로 결정했고 많이들 사용하는 라이브러리인 Bucket4j를 사용하게 되었다.
🛠️ 설계 포인트: 왜 Fail-Fast인가?
동기식 Spring Boot 서버에서 Rate Limit을 설계할 때 가장 주의해야 할 점은 스레드 풀(Thread Pool) 고갈이다.
만약 토큰이 생길 때까지 스레드를 대기(Wait)시키도록 설계하면, 트래픽이 몰릴 때 모든 Tomcat 스레드가 API 호출을 기다리며 점유 상태가 된다. 이는 해당 API뿐만 아니라 서비스 전체가 먹통이 되는 연쇄 장애(Cascading Failure)로 이어진다.
따라서 토큰이 없다면 즉시 에러를 반환하는 Fail-Fast 전략을 선택했다.
💻 구현: Bucket4j를 이용한 보호 로직
1. Safety Margin 확보
문서상 제한은 분당 50회였지만, 네트워크 지연과 시간 경계 문제를 고려해 45회로 제한을 낮췄다. 이 10%의 여유는 서비스를 보호하기 위한 최소한의 장치이다.
2. 외부 API Client 적용 코드
@Configuration
public class RateLimitConfig {
/**
* [Outbound] 쿠팡 API 전용 Rate Limiter
* 정책: 1분당 45회 (API 허용량 50회 - 안전마진 5회)
*/
@Bean
public Bucket coupangRateLimiterBucket() {
Bandwidth limit = Bandwidth.builder()
.capacity(45)
.refillGreedy(45, Duration.ofMinutes(1))
.build();
return Bucket.builder()
.addLimit(limit)
.build();
}
}
@Component
public class CoupangClient {
private final Bucket bucket;
// 생성자 주입을 통해 "어떤 정책의 버킷인지"는 알 필요 없이, 그냥 주입받은 것을 사용만 합니다.
public CoupangClient(@Qualifier("coupangRateLimiterBucket") Bucket bucket) {
this.bucket = bucket;
}
public CoupangSearchRawResponse search(String keyword) {
/*
* [장애 예방] Fail-Fast 전략 채택
* 동기 서버 환경에서 토큰 획득을 위해 Thread를 차단(Blocking)할 경우,
* Tomcat Thread Pool 고갈로 인한 전체 서버 장애로 확산될 위험이 있음.
* 따라서 토큰이 없으면 즉시 에러를 반환하여 현재 스레드를 자원으로 신속히 반납함.
*/
if (!bucket.tryConsume(1)) {
// 운영자가 트래픽 패턴을 분석할 수 있도록 Warning 레벨 로그 기록
log.warn("⚠️ [Rate Limit 차단] 쿠팡 API 호출 제한 도달 - Keyword: {}", keyword);
// 사용자에게는 503(Service Unavailable) 상태 코드를 전달하도록 설계
throw new BusinessException(ErrorCode.EXTERNAL_API_LIMIT_EXCEEDED);
}
try {
// 토큰을 확보한 경우에만 실제 HTTP 통신 수행
return restTemplate.exchange(...);
} catch (Exception e) {
// API 호출 실패 시에도 로그에 컨텍스트를 남겨 복구 근거를 제공
log.error("❌ [API 통신 오류] 호출 중 예외 발생. 사유: {}", e.getMessage());
throw e;
}
}
}
📉 결과: 24시간 Ban 없는 안정적인 연동
이제 동시에 50회 이상 검색을 시도하더라도, 서버는 분당 45회까지만 실제 외부 API를 호출한다. 46번째 요청부터는 사용자에게 "잠시 후 다시 시도해달라"는 메시지가 내려가지만, 서비스 전체가 24시간 동안 마비되는 최악의 상황은 확실히 차단할 수 있게 되었다.
만약 이용자 수가 늘고 서버를 scale-out 해야하는 시기가 오면 Redis 도입을 팀에게 제안해볼 것 같다.
'개발 기록 > 프로젝트 - BUKAE' 카테고리의 다른 글
| GPU 연산을 분리한 비동기 처리 아키텍처 설계 (0) | 2026.02.07 |
|---|
