AWS us-east-1 서비스 중단이 내게 가르쳐준 복원력 있는 시스템 구축

발행: (2025년 12월 15일 오전 04:45 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

AWS us‑east-1은 다시 다운될 것입니다. 다운될 때, 여러분의 시스템은 살아남을 수 있을까요?
지난 주말에 저는 이러한 장애를 견딜 수 있도록 설계된 시스템을 구축했습니다.

Surfline에서 구독 인프라를 8년간 구축하면서—Stripe, Apple, Google Play를 통한 결제 처리—클라우드 제공자가 실패할지가 아니라 실패했을 때 아키텍처가 어떻게 저하되는지가 진짜 문제라는 것을 배웠습니다.

저는 AWS Builders’ Library, Google SRE 실천법, Stripe 엔지니어링 블로그에서 가져온 세 가지 신뢰성 패턴을 구현하는 데 네 시간을 투자했습니다. 주요 내용은 다음과 같습니다.

왜 복원력이 중요한가

AWS에서 사고가 발생하면 흔히 나타나는 실패 유형은 다음과 같습니다:

  • Lambda 함수가 타임아웃
  • DynamoDB 호출이 실패하거나 쓰로틀링
  • SQS 큐가 백업

대부분의 애플리케이션에서는 사용자가 오류 페이지를 보고 나중에 다시 시도합니다. 결제 시스템은 상황이 다릅니다:

  • 실패한 결제가 실제로는 성공했을 수도 있습니다.
  • 재시도하면 고객에게 이중 청구가 발생할 수 있습니다.
  • 재시도 폭주(Thundering herd)가 장애를 연쇄적으로 확대시킬 수 있습니다.

따라서 금전적 손실이나 신뢰를 잃지 않으면서 부분적인 실패를 처리할 수 있는 패턴이 필요합니다.

패턴 1: 지터가 포함된 재시도

Timeouts, retries, and backoff with jitter에 관한 AWS Builders’ Library 글은 제가 재시도 로직을 생각하는 방식을 바꾸어 놓았습니다. 지터가 없으면 모든 클라이언트가 정확히 같은 간격으로 재시도해 복구 중인 서비스에 동기화된 파동을 일으킵니다.

// Full jitter formula from AWS Builders' Library
const calculateDelay = (attempt: number): number => {
  const exponentialDelay = Math.min(
    MAX_DELAY,
    INITIAL_DELAY * Math.pow(2, attempt)
  );
  // Full jitter: random value between 0 and exponential delay
  return Math.random() * exponentialDelay;
};

결과: 부하 테스트에서 성공률이 약 70 %에서 99 % 이상으로 급상승했습니다. 지터가 재시도 부하를 시간에 고르게 분산시켜 스파이크를 방지했기 때문입니다.

적용 사례

  • 쓰로틀링 중인 DynamoDB에 대해 Lambda가 재시도
  • NAT 게이트웨이를 통해 외부 API를 호출하는 ECS 작업
  • 서비스 통합에 대한 재시도 정책을 가진 Step Functions

패턴 2: 제한된 큐 + 워커 풀

제한된 큐만으로는 동시 처리량을 제한하지 못합니다. 테스트에서 큐 용량을 100으로 설정하고 200개의 요청을 보냈을 때, 기대했던 ~100개의 거절 대신 전혀 거절되지 않았습니다. Node.js가 요청을 쌓이기 전에 더 빨리 처리했기 때문입니다.

// What you actually need: queue + worker pool
class BoundedQueue {
  private queue: Request[] = [];
  private readonly capacity = 100;

  enqueue(request: Request): boolean {
    if (this.queue.length >= this.capacity) {
      return false; // HTTP 429 – fail fast
    }
    this.queue.push(request);
    return true;
  }
}
class WorkerPool {
  private activeWorkers = 0;
  private readonly maxWorkers = 10; // THIS controls throughput

  async process(queue: BoundedQueue) {
    while (this.activeWorkers < this.maxWorkers && queueHasWork()) {
      this.activeWorkers++;
      // process a single request...
      this.activeWorkers--;
    }
  }
}

멱등성 처리

class IdempotencyHandler {
  private inFlight = new Set<string>();
  private cache = new Map<string, { response: any; ttl: number }>();

  async process(idempotencyKey: string, operation: () => Promise<any>) {
    // Check cache first
    const cached = this.cache.get(idempotencyKey);
    if (cached) return cached.response;

    // Detect concurrent duplicates
    if (this.inFlight.has(idempotencyKey)) {
      throw new ConflictError('Request already in progress');
    }

    this.inFlight.add(idempotencyKey);
    try {
      const response = await operation();
      // Only cache successes
      if (response.success) {
        this.cache.set(idempotencyKey, {
          response,
          ttl: Date.now() + 24 * 60 * 60 * 1000,
        });
      }
      return response;
    } finally {
      this.inFlight.delete(idempotencyKey);
    }
  }
}

저장소 옵션

  • DynamoDB: TTL이 있는 조건부 쓰기(Conditional writes)로 자동 정리
  • Lambda Powertools: DynamoDB를 이용한 내장 멱등성 유틸리티
  • Step Functions: 실행 이름을 통한 네이티브 멱등성
// DynamoDB idempotency pattern
await dynamodb.put({
  TableName: 'IdempotencyStore',
  Item: {
    idempotencyKey: key,
    response: result,
    ttl: Math.floor(Date.now() / 1000) + 86400 // 24 h
  },
  ConditionExpression: 'attribute_not_exists(idempotencyKey)'
});

전체 구성

아래는 AWS에서 복원력 있는 결제 처리 파이프라인을 구현한 고수준 아키텍처입니다:

┌─────────────────────────────────────────────────────────────┐
│                     API Gateway (Rate Limiting)            │
└─────────────────────┬───────────────────────────────────────┘

┌─────────────────────▼───────────────────────────────────────┐
│                      SQS Queue (Bounded Buffer)            │
└─────────────────────┬───────────────────────────────────────┘

┌─────────────────────▼───────────────────────────────────────┐
│          Lambda (Reserved Concurrency = 10) – Worker Pool   │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 1. Check DynamoDB idempotency store                     ││
│  │ 2. Process payment with retry + jitter                  ││
│  │ 3. Store result in DynamoDB                             ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────┬───────────────────────────────────────┘

┌─────────────────────▼───────────────────────────────────────┐
│                 DynamoDB Tables                            │
│  - IdempotencyStore (TTL)                                   │
│  - ProcessingResults                                        │
└─────────────────────────────────────────────────────────────┘

앞으로의 계획

resilient‑relay 저장소에 전체 구현이 포함되어 있습니다. 예정된 개선 사항:

  • 실패한 결제를 위한 데드레터 큐 처리
  • RED(Rate, Errors, Duration) 가시성을 위한 CloudWatch 메트릭
  • 다중 리전 장애 복구 패턴

us‑east‑1이 다시 다운될 때—그리고 그럴 것입니다—시스템이 재앙이 아니라 우아하게 저하되도록 해야 합니다.

AWS Builders’ Library는 Amazon이 직접 AWS를 운영하면서 얻은 교훈을 바탕으로 만들어졌습니다. 지터에 관한 글 하나만으로도 충분히 읽어볼 가치가 있습니다.

행동 요청

여러분은 AWS 아키텍처에 어떤 신뢰성 패턴을 적용했나요? 프로덕션에서 효과가 있었던 사례나 크게 실패한 사례를 공유해 주세요.

Back to Blog

관련 글

더 보기 »