API Gateway 장애가 전체 서비스를 마비시키는 이유: Single Point of Failure 제거 전략
API Gateway 장애가 전체 서비스를 마비시키는 이유: Single Point of Failure 제거 전략
MSA를 도입하면서 API Gateway를 구축했다. 인증, 라우팅, 로깅이 한 곳에서 처리되니 깔끔했다. 그런데 어느 날 Gateway 인스턴스 하나가 메모리 부족으로 죽었다. 30초 만에 전체 서비스가 마비됐다.
분산 시스템을 만들었는데, 정작 입구는 단일 장애점(Single Point of Failure)이었던 것이다.
API Gateway가 SPOF가 되는 순간
API Gateway는 모든 트래픽이 통과하는 관문이다. 이 관문이 막히면 아무리 뒤에 있는 서비스가 건강해도 소용없다.
흔한 SPOF 패턴들:
1. 단일 인스턴스 운영
"트래픽이 적으니까 한 대로 충분해" → 그 한 대가 죽으면 끝
2. Sticky Session 의존
특정 인스턴스에 세션이 고정되면 해당 인스턴스 장애 시 세션 유실
3. 공유 상태 저장
Gateway에 인메모리 캐시나 상태를 저장하면 인스턴스 간 불일치 발생
4. 동기식 외부 의존성
인증 서버가 느려지면 Gateway 전체가 느려짐
해결 전략 1: 다중 인스턴스 + 로드밸런서
최소 3개 이상의 Gateway 인스턴스를 운영하고, 앞단에 L4/L7 로드밸런서를 배치한다.
# Nginx 로드밸런서 설정 예시
upstream api_gateway {
least_conn; # 연결 수 기반 분산
server gateway-1:8080 weight=5;
server gateway-2:8080 weight=5;
server gateway-3:8080 weight=5;
# Health check
server gateway-1:8080 max_fails=3 fail_timeout=30s;
}
server {
listen 443 ssl;
location / {
proxy_pass http://api_gateway;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_next_upstream error timeout http_502 http_503;
}
}
핵심 설정:
least_conn: 연결 수가 적은 서버로 분산max_fails: 3회 실패 시 해당 서버 제외proxy_next_upstream: 실패 시 다음 서버로 자동 재시도
해결 전략 2: Stateless Gateway 설계
Gateway는 상태를 가지면 안 된다. 모든 요청은 어떤 인스턴스로 가도 동일하게 처리되어야 한다.
세션/토큰 처리:
// BAD: 인메모리 세션 저장
Map<String, Session> sessions = new ConcurrentHashMap<>();
// GOOD: Redis 외부 저장소 사용
@Autowired
private RedisTemplate<String, Session> redisTemplate;
public Session getSession(String token) {
return redisTemplate.opsForValue().get("session:" + token);
}
Rate Limiting:
// BAD: 로컬 카운터
AtomicInteger counter = new AtomicInteger();
// GOOD: Redis 분산 카운터
public boolean isAllowed(String clientId) {
String key = "ratelimit:" + clientId;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
}
return count <= 100; // 분당 100회 제한
}
해결 전략 3: Circuit Breaker 적용
Gateway가 호출하는 외부 서비스(인증, 권한 등)에 장애가 발생하면 Gateway까지 죽을 수 있다. Circuit Breaker로 장애를 격리해야 한다.
@CircuitBreaker(name = "authService", fallbackMethod = "authFallback")
@TimeLimiter(name = "authService")
public CompletableFuture<AuthResult> authenticate(String token) {
return CompletableFuture.supplyAsync(() ->
authClient.validate(token)
);
}
// 인증 서버 장애 시 캐시된 결과 사용
private CompletableFuture<AuthResult> authFallback(String token, Exception e) {
AuthResult cached = authCache.get(token);
if (cached != null && !cached.isExpired()) {
return CompletableFuture.completedFuture(cached);
}
return CompletableFuture.failedFuture(
new ServiceUnavailableException("Auth service unavailable")
);
}
Resilience4j 설정:
resilience4j:
circuitbreaker:
instances:
authService:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 3
timelimiter:
instances:
authService:
timeoutDuration: 2s
해결 전략 4: Health Check와 자동 복구
장애 감지가 빨라야 복구도 빠르다. Kubernetes 환경이라면 Probe 설정이 핵심이다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
spec:
replicas: 3
template:
spec:
containers:
- name: gateway
image: api-gateway:latest
ports:
- containerPort: 8080
# Liveness: 죽었는지 확인
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
# Readiness: 트래픽 받을 준비 됐는지 확인
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
Health Endpoint 구현:
@RestController
@RequestMapping("/health")
public class HealthController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private AuthClient authClient;
// Liveness: 프로세스가 살아있는지만 확인
@GetMapping("/live")
public ResponseEntity<String> live() {
return ResponseEntity.ok("OK");
}
// Readiness: 의존성까지 확인
@GetMapping("/ready")
public ResponseEntity<Map> ready() {
Map<String, String> status = new HashMap<>();
// Redis 연결 확인
try {
redisTemplate.opsForValue().get("health-check");
status.put("redis", "UP");
} catch (Exception e) {
status.put("redis", "DOWN");
return ResponseEntity.status(503).body(status);
}
return ResponseEntity.ok(status);
}
}
해결 전략 5: Graceful Shutdown
배포나 스케일링 시 진행 중인 요청을 끊지 않고 안전하게 종료해야 한다.
@Component
public class GracefulShutdownHandler {
private volatile boolean shuttingDown = false;
private AtomicInteger activeRequests = new AtomicInteger(0);
@PreDestroy
public void shutdown() {
shuttingDown = true;
// 최대 30초간 진행 중인 요청 완료 대기
int waitTime = 0;
while (activeRequests.get() > 0 && waitTime < 30) {
try {
Thread.sleep(1000);
waitTime++;
} catch (InterruptedException e) {
break;
}
}
}
public void requestStarted() {
activeRequests.incrementAndGet();
}
public void requestCompleted() {
activeRequests.decrementAndGet();
}
}
Kubernetes 설정:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: gateway
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"] # LB에서 제거될 시간 확보
모니터링 지표
SPOF를 예방하려면 선제적 모니터링이 필수다.
| 지표 | 정상 범위 | 알람 조건 |
|---|---|---|
| 인스턴스 수 | 3개 이상 | 2개 이하 |
| CPU 사용률 | < 70% | > 85% (5분 지속) |
| 메모리 사용률 | < 75% | > 90% |
| P99 응답시간 | < 100ms | > 500ms |
| 에러율 | < 0.1% | > 1% |
| Circuit Open 횟수 | 0 | > 0 |
결론
API Gateway는 MSA의 핵심 인프라지만, 잘못 설계하면 가장 큰 약점이 된다. SPOF를 제거하려면:
- ✅ 최소 3개 이상 인스턴스 운영
- ✅ Stateless 설계 (외부 저장소 활용)
- ✅ Circuit Breaker로 외부 의존성 격리
- ✅ Health Check와 자동 복구
- ✅ Graceful Shutdown 구현
- ✅ 선제적 모니터링
분산 시스템의 입구가 단일 장애점이 되지 않도록 설계하자.
더 자세한 분산 시스템 아키텍처 패턴은 PowerSoft Tech Blog에서 확인할 수 있다.
Backend Architecture Series | January 2026
Author: PowerSoft R&D Center
댓글
댓글 쓰기