본문으로 건너뛰기
목록으로 돌아가기
스터디
32

Lambda에서 Stripe 읽기 API 가속하기: Redis·DynamoDB 다층 캐시 설계와 검증

CPU L1/L2의 원리부터 Lambda·Redis·DynamoDB 다층 캐시, 캐시 혼합·정합성·멀티리전 무효화·부하 테스트까지 Stripe 참조 패턴을 운영 관점에서 해설합니다.

박효열 (Hyoyoul Park)
#AWS Lambda#Redis#DynamoDB#Stripe#Caching#Computer Science

이 글은 Stripe Developer Blog의 참조 패턴을 학습 목적으로 재구성하고, AWS·Stripe 공식 문서와 식별 정보를 제거한 일반 운영 원칙으로 보강한 글입니다. Stripe의 내부 운영 아키텍처나 측정 결과를 재현한 것이 아니며, 다이어그램과 코드는 새로 작성한 학습용 자료입니다.

먼저 결론: API Gateway 캐시가 핵심은 아니다

원문이 설명하는 핵심은 API Gateway 응답 캐시가 아니라 Lambda 내부의 다단계 cache-aside입니다. Lambda가 ElastiCache for Redis를 L1, DynamoDB를 L2로 확인하고, 두 계층 모두 유효한 값을 주지 못할 때만 Stripe API를 호출합니다.

API Gateway 캐시는 Lambda 앞에서 동일한 HTTP 응답을 재사용하므로 hit 시 Lambda 실행 자체를 생략할 수 있습니다. 반면 Redis·DynamoDB 엔터티 캐시는 Lambda 뒤에서 동작하므로 Lambda는 실행되지만, 여러 API가 같은 고객·구독 같은 엔터티를 재사용하기 쉽고 키 단위 무효화를 더 세밀하게 설계할 수 있습니다.

따라서 “API Gateway 캐시를 붙여 해결했다”가 아니라 “Lambda가 원본 API 호출 전에 L1·L2 캐시를 조회하는 구조를 제안했다”가 정확한 해석입니다.

캐시란 무엇인가

캐시는 다시 사용할 가능성이 높은 데이터나 계산 결과의 복사본을 소비자와 더 가까운 빠른 저장소에 두어, 느리거나 비싼 원본 접근을 줄이는 계층입니다. cache-aside 구조에서는 캐시 전체를 지워도 원본에서 정답을 복구할 수 있어야 합니다. 캐시를 지웠을 때 유일한 데이터까지 사라진다면 그것은 캐시가 아니라 또 하나의 권위 저장소입니다.

용어의미운영에서 확인할 것
hit / miss유효한 복사본을 찾음 / 못 찾아 다음 계층으로 이동계층별 조건부 hit ratio
miss penaltymiss 뒤 다음 계층이나 원본을 호출하는 추가 지연·비용origin p95/p99, 429, 비용
eviction용량이나 정책 때문에 아직 TTL이 남은 값도 축출Redis memory와 evictions
freshness / staleness원본과 얼마나 같고, 얼마나 오래됐는지실제 stale age와 freshness SLO
working set일정 시간 동안 반복 접근되는 데이터 집합캐시 용량이 working set을 감당하는지

캐시가 효과적인 이유는 지역성(locality) 때문입니다. 방금 사용한 데이터를 곧 다시 쓰는 시간 지역성과, 인접한 데이터를 함께 쓰는 공간 지역성이 반복 접근을 예측하게 합니다. CPU 캐시는 두 지역성을 모두 적극 활용하고, API 엔터티 캐시는 주로 “인기 고객이나 구독을 다시 읽는다”는 시간 지역성을 활용합니다.

속도는 계층 수가 아니라 적중률과 miss 비용으로 결정된다

Redis hit 비율을 h1, Redis가 miss였을 때 DynamoDB의 유효 hit 비율을 h2라고 두면 원본 호출 비율은 다음과 같이 추정할 수 있습니다.

h1 = P(Redis hit)
h2 = P(DynamoDB valid hit | Redis miss)

P(origin call) = (1 - h1) × (1 - h2)
combined hit rate = 1 - P(origin call)
origin QPS = read QPS × P(origin call)

경로별 종단 간 지연을 사용하면 평균 지연의 단순 모델은 다음과 같습니다.

E[T] = h1 × T(redis path)
     + (1 - h1) × h2 × T(dynamo path)
     + (1 - h1) × (1 - h2) × T(origin path)

계층을 하나 더 넣으면 그 계층의 네트워크 왕복, 직렬화, 장애 가능성도 더해집니다. 따라서 hit ratio 하나만 높다고 성공이 아닙니다. miss가 한꺼번에 몰릴 때의 queueing과 p95/p99, 원본 호출 감소량, stale 응답, 요청당 총비용을 함께 봐야 합니다.

CPU L1·L2와 Redis L1·DynamoDB L2는 같은 캐시가 아니다

그림 1. 일반적인 CPU 캐시의 개념적 위치입니다. 실제 프로세서 평면도가 아니며 CPU·SoC·chiplet마다 공유 범위와 배치가 다릅니다. CPU의 L1/L2와 애플리케이션의 Redis L1/DynamoDB L2는 가까운 계층부터 조회한다는 아이디어만 비슷합니다.

CPU의 L1·L2·L3는 보통 프로세서 다이 위의 빠른 SRAM으로 구현되고 하드웨어가 cache line과 일관성을 관리합니다. L1 instruction/data cache는 코어에 가장 가깝고, L2는 더 크며, L3 또는 last-level cache는 여러 코어가 공유하는 경우가 많습니다. DRAM은 대체로 CPU 다이 밖의 주기억장치입니다. 다만 L2가 항상 코어 전용이고 L3가 항상 공유되는 것은 아니며, 특정 제품에는 stacked cache나 다른 구조도 있습니다.

Stripe 글에서 Redis를 L1, DynamoDB를 L2라고 부른 것은 논리적 우선순위에 붙인 이름입니다. 두 계층의 정합성은 CPU처럼 자동으로 유지되지 않으므로 애플리케이션이 키, TTL, 무효화, 장애 정책을 직접 설계해야 합니다.

계층위치와 범위장점핵심 한계
CPU L1/L2/L3프로세서 다이하드웨어가 매우 짧은 지연으로 자동 관리작고 프로세서별 구조가 다름
Lambda process-localwarm 실행 환경 하나네트워크 왕복 없음재사용 보장 없음, 다른 컨테이너와 공유 안 됨
Redis/ElastiCache L1VPC의 공유 메모리여러 Lambda가 공유, 빠른 key lookup연결·메모리 비용, eviction과 장애
DynamoDB L2내구성 있는 네트워크 저장소Redis miss 완충, 확장성더 느림, TTL 삭제가 비동기
API Gateway cacheLambda 앞hit이면 Lambda 실행도 생략동일하고 안전한 HTTP 응답에만 적합
Stripe API권위 원본현재 상태를 다시 확인네트워크와 rate/concurrency limit

실전에서 선택하는 캐시 패턴

패턴동작잘 맞는 경우정합성 위험
cache-asidemiss 때 원본을 읽고 캐시를 채움읽기 중심 엔터티 조회miss penalty, stale resurrection
write-through원본 갱신과 캐시 갱신 경로를 함께 운영쓰기 뒤 즉시 반복 조회dual-write 부분 실패와 쓰기 지연
write-behind캐시에 먼저 쓰고 원본은 나중에 반영손실을 허용할 수 있는 집계순서·유실 위험, 결제의 권위 쓰기에는 부적합
refresh-ahead인기 키를 만료 전에 갱신예측 가능한 hot key불필요한 refresh와 동시성 폭증
stale-while-revalidate제한된 stale 값을 반환하며 뒤에서 갱신UI·참조 데이터권한·잔액·현재 결제 판단에는 위험

일반적으로 이벤트 무효화와 TTL을 함께 둡니다. 이벤트는 stale window를 줄이고 TTL은 이벤트가 누락됐을 때의 최종 안전망입니다. 어느 방식도 그 자체로 strong consistency를 만들지는 않습니다.

아키텍처 개요

그림 2. Stripe 글의 L1 Redis–L2 DynamoDB cache-aside 패턴을 학습용으로 다시 그린 구조입니다. 점선의 API Gateway 캐시는 비교를 위한 선택지이며 원문의 구성요소가 아닙니다.

흐름·시퀀스 다이어그램은 Mermaid 원본에서 정적 SVG로 렌더링했고, CPU 위치도는 직접 작성한 SVG입니다. 이미지를 누르면 최대 500%까지 확대하고 드래그하거나 방향키로 이동할 수 있습니다. 브라우저에서 Mermaid 런타임을 실행하지 않아 그 렌더링 비용은 없지만, 확대 UI를 위한 소량의 client JavaScript는 hydrate됩니다.

각 구성요소의 책임은 다음과 같습니다.

계층책임hit 시 이점반드시 정할 것
API Gateway 캐시(선택)동일한 안전한 GET 응답 재사용Lambda 호출 자체 생략 가능인증·tenant를 포함한 cache key
Redis L1자주 읽는 엔터티의 짧은 수명 캐시매우 낮은 캐시 조회 지연연결 수, eviction, 짧은 TTL
DynamoDB L2Redis miss를 흡수하는 내구성 캐시Stripe 원본 호출 감소복합 키, expiresAt 검사, 비용
Stripe API권위 있는 현재값cache miss 시 최종 조회rate/concurrency limit, timeout
Webhook + queue변경 이벤트를 비동기로 전달stale window 축소서명, 중복, 역순, 재시도, DLQ

여기서 Stripe가 source of truth이고 캐시는 가속 계층일 뿐입니다. 캐시 장애가 원본의 정상 응답까지 실패시키거나, 캐시가 새로운 권위 저장소처럼 동작하면 책임 경계가 무너집니다.

읽기 경로: L1 → L2 → 원본

그림 3. L1과 L2에서 유효한 항목을 찾고 모두 실패했을 때만 원본 API를 호출합니다. L2 항목을 L1으로 승격할 때는 L2의 남은 유효시간을 초과하면 안 됩니다.

조회 경로의 핵심 규칙은 네 가지입니다.

  1. L1과 L2의 값은 존재 여부뿐 아니라 expiresAt도 확인합니다.
  2. L2 hit를 Redis에 올릴 때 min(L1 기본 TTL, L2 남은 TTL)을 사용합니다.
  3. 캐시 읽기 실패는 제한된 시간 안에 원본 조회로 우회하는 fail-open이 기본입니다.
  4. 원본 조회 성공 뒤 캐시 쓰기가 실패해도 원본 응답은 반환합니다. 단, Lambda 반환 후의 fire-and-forget 작업은 완료가 보장되지 않으므로 짧은 timeout을 둔 쓰기를 기다리거나 큐로 넘깁니다.

다음 코드는 구현 방향을 설명하는 AWS SDK v3 스타일의 TypeScript 의사코드입니다. 그대로 배포하는 production-ready 코드는 아닙니다.

type CacheEntry<T> = {
  value: T;
  expiresAt: number;
};

async function readStripeEntity<T>(
  scope: CacheScope,
  entityId: string,
): Promise<T> {
  const key = buildScopedKey(scope, entityId);

  const l1 = await failOpen(() => redisCache.get<T>(key));
  const l1CheckedAt = nowSeconds();
  if (l1 && l1.expiresAt > l1CheckedAt) return l1.value;

  const l2 = await failOpen(() =>
    durableCache.get<T>({
      pk: scope.partitionKey,
      sk: ["ENTITY", entityId].join("#"),
    }),
  );

  const promotedAt = nowSeconds();
  if (l2 && l2.expiresAt > promotedAt) {
    const remaining = l2.expiresAt - promotedAt;
    const ttl = downwardJitter(Math.min(L1_TTL_SECONDS, remaining));

    if (ttl > 0) {
      await failOpen(() =>
        withTimeout(
          redisCache.set(
            key,
            { value: l2.value, expiresAt: l2.expiresAt },
            ttl,
          ),
          CACHE_WRITE_TIMEOUT_MS,
        ),
      );
    }
    if (l2.expiresAt > nowSeconds()) return l2.value;
  }

  // 한 warm Lambda 실행 환경 안의 중복 요청만 합친다.
  return singleFlightLocal(key, async () => {
    const current = await origin.retrieve<T>(scope, entityId);
    const writtenAt = nowSeconds();
    const l1Ttl = downwardJitter(L1_TTL_SECONDS);

    await Promise.allSettled([
      withTimeout(
        redisCache.set(
          key,
          { value: current, expiresAt: writtenAt + l1Ttl },
          l1Ttl,
        ),
        CACHE_WRITE_TIMEOUT_MS,
      ),
      withTimeout(
        durableCache.put({
          pk: scope.partitionKey,
          sk: ["ENTITY", entityId].join("#"),
          value: current,
          expiresAt: writtenAt + L2_TTL_SECONDS,
        }),
        CACHE_WRITE_TIMEOUT_MS,
      ),
    ]);

    return current;
  });
}

downwardJitter()는 TTL을 늘리지 않고 조금 줄이는 방향으로만 흔들어 여러 키가 동시에 만료되는 현상을 완화합니다. singleFlightLocal은 컨테이너 하나에서만 유효하므로 여러 Lambda 실행 환경 사이의 stampede에는 짧은 Redis SET NX lock 같은 분산 조정이 별도로 필요합니다.

각 비동기 조회가 끝난 뒤 시각을 다시 읽는 이유도 중요합니다. 요청 시작 때의 오래된 now로 만료를 판정하면 느린 cache read가 끝난 시점에는 이미 만료된 값을 반환하거나 L1으로 승격할 수 있습니다. L1 envelope에는 L2의 절대 expiresAt을 그대로 보존하고, 승격 쓰기를 기다린 뒤 반환 직전에도 다시 확인합니다.

캐시 키는 격리 계약이지, 인가 장치가 아니다

고객 ID 하나만 키로 사용하면 test/live, 연결 계정, API 버전, 권한별 응답이 섞일 수 있습니다. 최소한 다음 범위를 구분해야 합니다.

stripe:{mode}:{account-scope}:{api-version}:{entity-type}:{entity-id}:{view-hash}:{schema-version}:g{generation}

view-hash에는 응답 필드 집합이나 권한 범위를 안정적으로 표현한 버전을 사용합니다. schema-version은 직렬화 형식을, 객체별 generation은 현재 invalidation namespace를 구분합니다. 키는 서버가 신뢰할 수 있는 context로 생성하고, 반환 직전에도 현재 사용자에게 해당 객체를 보여 줄 권한이 있는지 확인합니다. 키를 hash하거나 prefix를 나누는 것만으로는 접근 제어가 되지 않습니다.

비밀키, Authorization 헤더, 이메일 같은 개인정보 원문을 cache key나 로그에 넣어서는 안 됩니다. 캐시에는 전체 Stripe 객체를 무조건 저장하기보다 사용 목적에 필요한 최소 필드만 담고 암호화·접근 제어·보존 기간을 함께 검토해야 합니다.

실전의 “캐시 혼합”은 세 종류다

혼합 유형대표 사고방지 원칙
의미 혼합같은 ID의 test/live·Connected Account·권한·API 버전·projection이 한 키를 공유mode, trusted account scope, API/schema version, resource, projection을 canonical key에 포함
계층 혼합L2의 만료 직전 값을 L1에 긴 TTL로 승격하거나 Redis만 지워 오래된 L2가 다시 올라옴모든 tier가 같은 absolute expiry·generation을 검사하고 관련 계층을 함께 무효화
역할 혼합폐기 가능한 조회 캐시와 idempotency·lock·rate limit·session을 같은 eviction/장애 영역에 저장정확성 상태는 durable store 또는 별도 용량·장애 경계로 분리

특히 역할 혼합은 단순 cache miss를 중복 결제, lock 상실, rate limit 우회로 바꿀 수 있습니다. Redis DB 번호나 key prefix는 메모리·eviction·권한·장애를 분리하는 강한 경계가 아닙니다.

키뿐 아니라 값의 envelope도 검증한다

롤링 배포나 잘못된 invalidation으로 다른 버전의 값이 들어올 수 있으므로 캐시 값에 출처와 유효성 정보를 함께 둡니다.

type CacheEnvelope<T> = {
  schemaVersion: 3;
  source: "stripe";
  sourceAccount: string;
  resourceType: string;
  resourceId: string;
  generation: number;
  fetchedAt: number;
  hardExpiresAt: number;
  value: T;
};

읽을 때 account, resource ID, 지원 schema, generation, hardExpiresAt을 다시 확인합니다. 검증된 성공 응답만 캐시하고, webhook payload는 서명을 확인한 뒤에도 곧바로 공유 캐시에 쓰기보다 무효화 신호로 사용해 원본을 다시 읽는 편이 안전합니다. 안정적으로 판별한 not-found만 매우 짧게 negative cache할 수 있으며, 401/403, 429, timeout, 5xx는 공유 negative cache에 넣지 않습니다.

삭제 뒤 오래된 값이 되살아나는 stale resurrection

그림 4. 먼저 시작한 조회가 늦게 끝나며 삭제된 구버전 값을 다시 채우는 경쟁입니다. 조회 시작 때 읽은 generation이 바뀌었다면 cache fill을 거부합니다.

단순한 “원본 갱신 후 캐시 삭제”만으로는 충분하지 않습니다. 조회 A가 이전 값을 원본에서 읽는 동안 변경 이벤트가 캐시를 삭제하고, A가 뒤늦게 완료돼 이전 값을 다시 넣을 수 있기 때문입니다. 객체별 generation 또는 invalidation watermark를 두고 조회 시작 세대와 저장 시점 세대가 같을 때만 conditional fill하면 이 경쟁을 막을 수 있습니다.

단순한 get generation → compare → cache set도 비교와 저장 사이에 다시 race가 생깁니다. production에서는 generation을 cache-key namespace에 포함해 늦은 쓰기가 구세대 키에만 남게 하거나, 동일한 coordinator에서 conditional write·transaction을 원자적으로 수행해야 합니다. generation은 무효화 정확성에 필요한 상태이므로 축출 가능한 조회 캐시와 같은 장애 경계에 두지 않습니다. 안정적인 버전 토큰이 없다면 짧은 hard TTL과 이벤트 이후 authoritative refetch로 stale window를 제한합니다.

API Gateway 캐시와 어떻게 다른가

질문API Gateway 응답 캐시Redis + DynamoDB 엔터티 캐시
위치Lambda 앞Lambda 뒤
hit 시 Lambda생략 가능실행됨
캐시 단위완성된 HTTP 응답고객·구독 같은 엔터티
재사용 범위같은 route와 cache key여러 route·서비스에서 공유 가능
무효화stage/entry 정책 중심엔터티 키 단위로 설계 가능
주의점인증·query/header key 누락stale 데이터·연결·VPC·운영 복잡성

API Gateway REST API 캐시 문서는 기본 TTL 300초, 최대 3,600초를 설명하며 GET 메서드만 기본 캐시 대상으로 둡니다. 동일하고 안전한 GET 응답이 많이 반복될 때 좋은 선택지지만, 사용자별 응답에서 인증이나 query parameter를 cache key에서 빠뜨리면 데이터가 교차 노출될 수 있습니다.

TTL만으로는 부족하다

원문의 시간 값은 설명용 예시이지 보편적인 SLA가 아닙니다. TTL은 “얼마나 오래 stale을 허용할 수 있는가”라는 제품 요구에서 역산해야 합니다.

특히 DynamoDB TTL 문서에 따르면 만료 항목 삭제는 비동기입니다. 만료된 행이 한동안 조회될 수 있으므로 TTL 기능을 읽기 유효성 검사로 오해하면 안 됩니다. 애플리케이션이 expiresAt > now를 확인하고, 무효화 시에는 캐시 테이블의 정확한 pk + skDeleteItem을 수행하는 편이 명확합니다.

웹훅은 TTL의 stale window를 줄여 주지만 완전한 동기화 장치는 아닙니다. Stripe 웹훅 문서는 이벤트 중복과 순서 불보장을 전제로 설계하라고 안내합니다.

안전한 무효화 경로는 다음 순서를 따릅니다.

  1. raw body로 Stripe 서명을 검증합니다.
  2. 빠르게 queue에 넣고 2xx를 응답합니다.
  3. 같은 event ID의 재전송을 중복 제거하고 재처리는 멱등적으로 만듭니다. 서로 다른 Event가 같은 event.type + data.object.id를 나타낼 수도 있으므로 단순 delta를 중복 적용하지 말고 현재 원본 상태로 reconciliation합니다.
  4. 이벤트 snapshot으로 캐시를 덮어쓰기보다 관련 L1·L2 키를 삭제합니다.
  5. 다음 조회가 원본에서 최신 상태를 다시 채우게 합니다.
  6. 재시도 횟수와 DLQ, 무효화 지연을 관측합니다.

운영에서 빠지기 쉬운 함정

Cache stampede

동일 키가 만료된 순간 요청이 몰리면 모두 Stripe로 향할 수 있습니다. 로컬 single-flight, 짧은 분산 lock, TTL jitter, 키별 동시성 상한을 조합합니다. lock 보유자가 실패할 때를 대비해 짧은 만료시간과 원본 timeout도 필요합니다.

여러 인기 키가 같은 시각에 만료되는 cache avalanche, 한 키에 트래픽이 집중되는 hot key, 존재하지 않는 ID를 반복 조회하는 cache penetration도 따로 시험합니다. 분산 lock 하나를 correctness lock처럼 믿어서는 안 되며, lease가 만료된 이전 holder의 늦은 작업까지 막아야 한다면 fencing token이 필요합니다.

Lambda 연결 재사용

Lambda 모범 사례는 SDK 클라이언트와 데이터베이스 연결을 handler 밖에서 초기화해 실행 환경을 재사용하라고 권장합니다. Redis 연결을 요청마다 새로 만들면 동시 확장 때 연결 폭증이 생길 수 있습니다. 제한된 pool, connect/read timeout, keep-alive와 서버 측 연결 지표를 함께 봐야 합니다.

VPC egress와 비용

VPC에 연결된 Lambda의 인터넷 접근 문서가 설명하듯, ElastiCache 접근을 위해 Lambda를 VPC에 연결하면 Stripe의 인터넷 API로 나가는 egress 경로가 필요합니다. NAT Gateway의 고정·처리 비용과 추가 지연을 포함해 계산해야 합니다. 반복 조회가 적다면 Redis 클러스터의 상시 비용이 절감한 원본 호출 비용보다 클 수도 있습니다.

캐시 장애와 stale-if-error

Redis 장애는 DynamoDB나 원본으로 우회하고, DynamoDB 장애는 원본으로 우회합니다. 그러나 원본 장애를 임의의 stale 응답으로 숨겨서는 안 됩니다. 가격·권한·결제 상태처럼 민감한 데이터는 freshness SLA와 최대 stale 허용 시간을 명시한 경우에만 stale-if-error를 검토합니다.

화면에 참고용 상태를 보여 주는 조회와 결제 승인·확정·환불·권한 변경 같은 비가역적 결정은 분리해야 합니다. 후자는 캐시 값만 믿지 말고 Stripe 또는 강한 정합성의 권위 저장소를 다시 확인해야 합니다. POST/mutation의 중복 방지는 조회 캐시가 아니라 Stripe idempotency와 축출되지 않는 durable dedupe record의 책임입니다.

글로벌 규모에서 캐시를 운영할 때

그림 5. 글로벌 외부 API를 사용하는 소비자 측 멀티리전 예시입니다. Stripe 내부 아키텍처를 나타내지 않습니다. 각 리전의 지역 캐시는 지연을 줄이지만 검증된 이벤트, durable queue, 세대가 포함된 동일한 key contract로 무효화를 전달해야 합니다.

이 그림은 Stripe 같은 글로벌 외부 API를 사용하는 소비자 시스템의 예시이지 Stripe 내부 운영 구조가 아닙니다. 각 리전에 local Redis를 두면 읽기 지연은 줄지만, 데이터 복제와 cache invalidation은 비동기 분산 시스템 문제가 됩니다.

우선순위실패 모드대규모 서비스의 필수 방어책
P0tenant·권한·live/test 혼합신뢰된 context로 scoped key 생성, 반환 전 재인가, 충돌 테스트
P0cache poisoningwebhook raw body 서명 검증, event ID dedupe, payload 직접 저장 금지, 원본 재조회
P0stale 값으로 결제·권한 결정표시 경로와 비가역 결정 경로 분리, 결정 직전 authoritative recheck
P0PII·PCI·residency 위반필드 allowlist와 최소화, PAN/CVC·API key·webhook secret 캐시 금지, TLS·저장 암호화·IAM·허용 리전·명시 삭제
P1리전 간 무효화 지연·역순 webhookdurable queue, 멱등 consumer, 리전별 ack, retry·DLQ·replay, generation conditional fill
P1failover 뒤 cold-cache storm제한적 prewarm, weighted traffic ramp, 글로벌 origin 동시성 예산, backoff·circuit breaker
P1schema/API version 혼합key와 envelope versioning, N/N-1 reader 호환, generation 전환
P2높은 hit ratio가 문제를 가림latency SLO와 별도로 freshness SLO, stale age, invalidation p95/p99 관측

ElastiCache Global Datastore의 리전 간 복제는 비동기이고, DynamoDB Global Tables도 선택한 일관성 모드와 리전 구성에 따라 보장이 달라집니다. 어느 복제본도 Stripe의 권위 상태처럼 취급해서는 안 되며, 상위 Redis에는 별도의 stale window가 남습니다.

TTL은 개인정보 삭제 증명이 아닙니다. 만료 항목이 실제로 남을 수 있고 snapshot·복제본·로그에도 흔적이 생길 수 있으므로 삭제 요청은 명시적 delete와 보존 정책으로 처리합니다. Stripe 보안 통합 가이드ElastiCache 전송 중 암호화 문서를 기준으로 데이터 최소화, 암호화, 접근 경계와 리전별 규제를 함께 검토합니다.

관측과 롤백도 계층별로 분리한다

hit ratio만 보지 말고 다음을 같은 대시보드에서 연결합니다.

  • L1 hit ratio와 L1 miss 중 L2 conditional hit ratio
  • fill amplification = origin fetch / distinct missed key
  • 실제 stale age, freshness SLO 위반, webhook-to-all-regions invalidation p95/p99
  • queue backlog·DLQ·generation 충돌, Redis latency·memory·eviction·connections·replication lag
  • DynamoDB throttle/error와 Stripe 429/5xx, origin latency, failover 뒤 cold-hit 회복 시간

긴급 스위치는 L1 read/write, L2 read/write, cache fill, stale-while-revalidate, entity bypass, namespace generation을 따로 둡니다. 정보 혼합이 의심되면 캐시 read를 먼저 차단하고 원본 동시성 상한을 건 채 우회합니다. 전체 FLUSHALL보다 새 generation으로 전환해 오염된 namespace를 더 이상 읽지 않는 방식이 rollback과 원본 보호에 유리합니다.

어떤 캐시 계층을 선택할까

그림 6. API Gateway 응답 캐시와 엔터티 캐시를 선택하는 기준입니다. 두 방식은 함께 사용할 수도 있지만 측정된 병목 없이 모두 도입할 필요는 없습니다.

가장 단순한 계층부터 시작하는 편이 좋습니다.

  • 반복되는 동일 GET 응답이고 Lambda 실행도 피하고 싶다면 API Gateway 캐시를 먼저 검토합니다.
  • 여러 API가 같은 엔터티를 재사용하고 내구성 있는 캐시가 필요하면 DynamoDB L2를 검토합니다.
  • 측정 결과 매우 높은 반복 조회와 낮은 지연 요구가 확인될 때 Redis L1을 추가합니다.
  • 요청량이 낮거나 freshness 요구가 매우 엄격하면 원본 직접 호출이 더 단순하고 안전할 수 있습니다.

Locust로 검증할 테스트 시나리오

원문에는 재현 가능한 before/after benchmark, 트래픽 규모, hit ratio, p95/p99, 월 비용 차이가 없습니다. 따라서 “몇 배 빨라졌다”라고 결론내릴 수 없고, 아래 가설을 직접 검증해야 합니다.

아래 부하·장애 주입은 격리된 staging, Stripe sandbox 또는 stub origin에서만 수행합니다. live 결제 mutation이나 production 고객 트래픽에 synthetic load를 보내거나 의도적으로 429를 유발해서는 안 됩니다.

추정 원본 호출률
≈ 전체 읽기율 × (1 - L1 hit ratio) × (1 - L2 conditional hit ratio)

conditional L2 hit ratio는 L1 miss 요청 중 L2가 맞힌 비율입니다. Locust 시나리오는 단계별로 분리합니다.

  1. cold: 모든 캐시를 비운 뒤 원본 경로의 p50/p95/p99와 오류율을 측정합니다.
  2. warm: 실제 키 분포로 L1·L2 hit ratio와 원본 호출 감소율을 측정합니다.
  3. burst: 인기 키 만료 직후 트래픽을 집중해 stampede와 Stripe 429를 확인합니다.
  4. degraded cache/origin: Redis timeout, DynamoDB throttling, Stripe 지연을 주입해 fail-open과 backpressure를 확인합니다.
  5. recovery: 장애를 제거한 뒤 queue backlog, 연결 수, 지연이 정상 수준으로 회복되는 시간을 측정합니다.
  6. isolation: 같은 객체 ID를 test/live, 서로 다른 account·권한·projection으로 요청해 교차 노출이 없는지 확인합니다.
  7. race: miss 시작 → webhook invalidate → 이전 fetch 완료 순서를 재현해 stale fill이 거부되는지 확인합니다.
  8. failover/rollout: 리전 전환과 schema N/N+1 혼합 배포에서 cold-cache 폭증, 호환 읽기, rollback을 확인합니다.

평균만 보면 꼬리 지연과 장애 확대가 가려집니다. L1/L2/origin별 p50·p95·p99, Lambda cold/warm, cache hit ratio, Stripe 요청 수·429, Redis 연결·eviction, DynamoDB RCU/WCU·throttling, webhook 지연·DLQ, stale 응답률, 요청당 총비용을 같은 대시보드에서 봐야 합니다.

단계적 배포 체크리스트

  1. 캐시 대상 필드와 freshness SLA를 문서화합니다.
  2. tenant·환경·버전·권한을 포함한 cache key를 테스트합니다.
  3. 캐시 없이도 동작하는 timeout·retry·rate limit·backpressure 경로를 먼저 검증합니다.
  4. DynamoDB L2부터 canary로 적용하고 hit ratio와 비용을 측정합니다.
  5. 필요성이 입증된 경우에만 Redis L1 또는 API Gateway 캐시를 추가합니다.
  6. webhook 중복·역순·지연·DLQ 복구 테스트를 자동화합니다.
  7. 계층별 read/write/bypass kill switch와 namespace generation rollback 절차를 준비합니다.

결론

이 구조의 가치는 “캐시를 두 개 붙였다”는 데 있지 않습니다. 권위 저장소를 명확히 하고, 원본 호출을 얼마나 줄였는지와 stale 위험·운영 비용을 함께 측정하는 데 있습니다.

Redis L1, DynamoDB L2, API Gateway 응답 캐시는 서로 대체 관계가 아니라 서로 다른 병목을 푸는 도구입니다. freshness 요구와 실제 hit ratio를 먼저 측정하고 가장 단순한 계층부터 도입하는 것이 핵심입니다. 캐시는 정답이 아니라 부하 테스트와 장애 주입으로 검증할 가설입니다.

참고 자료