Lambda에서 Stripe 읽기 API 가속하기: Redis·DynamoDB 다층 캐시 설계와 검증
CPU L1/L2의 원리부터 Lambda·Redis·DynamoDB 다층 캐시, 캐시 혼합·정합성·멀티리전 무효화·부하 테스트까지 Stripe 참조 패턴을 운영 관점에서 해설합니다.
이 글은 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 penalty | miss 뒤 다음 계층이나 원본을 호출하는 추가 지연·비용 | 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-local | warm 실행 환경 하나 | 네트워크 왕복 없음 | 재사용 보장 없음, 다른 컨테이너와 공유 안 됨 |
| Redis/ElastiCache L1 | VPC의 공유 메모리 | 여러 Lambda가 공유, 빠른 key lookup | 연결·메모리 비용, eviction과 장애 |
| DynamoDB L2 | 내구성 있는 네트워크 저장소 | Redis miss 완충, 확장성 | 더 느림, TTL 삭제가 비동기 |
| API Gateway cache | Lambda 앞 | hit이면 Lambda 실행도 생략 | 동일하고 안전한 HTTP 응답에만 적합 |
| Stripe API | 권위 원본 | 현재 상태를 다시 확인 | 네트워크와 rate/concurrency limit |
실전에서 선택하는 캐시 패턴
| 패턴 | 동작 | 잘 맞는 경우 | 정합성 위험 |
|---|---|---|---|
| cache-aside | miss 때 원본을 읽고 캐시를 채움 | 읽기 중심 엔터티 조회 | 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 L2 | Redis 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의 남은 유효시간을 초과하면 안 됩니다.
조회 경로의 핵심 규칙은 네 가지입니다.
- L1과 L2의 값은 존재 여부뿐 아니라
expiresAt도 확인합니다. - L2 hit를 Redis에 올릴 때
min(L1 기본 TTL, L2 남은 TTL)을 사용합니다. - 캐시 읽기 실패는 제한된 시간 안에 원본 조회로 우회하는 fail-open이 기본입니다.
- 원본 조회 성공 뒤 캐시 쓰기가 실패해도 원본 응답은 반환합니다. 단, 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 + sk로 DeleteItem을 수행하는 편이 명확합니다.
웹훅은 TTL의 stale window를 줄여 주지만 완전한 동기화 장치는 아닙니다. Stripe 웹훅 문서는 이벤트 중복과 순서 불보장을 전제로 설계하라고 안내합니다.
안전한 무효화 경로는 다음 순서를 따릅니다.
- raw body로 Stripe 서명을 검증합니다.
- 빠르게 queue에 넣고
2xx를 응답합니다. - 같은 event ID의 재전송을 중복 제거하고 재처리는 멱등적으로 만듭니다. 서로 다른 Event가 같은
event.type + data.object.id를 나타낼 수도 있으므로 단순 delta를 중복 적용하지 말고 현재 원본 상태로 reconciliation합니다. - 이벤트 snapshot으로 캐시를 덮어쓰기보다 관련 L1·L2 키를 삭제합니다.
- 다음 조회가 원본에서 최신 상태를 다시 채우게 합니다.
- 재시도 횟수와 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은 비동기 분산 시스템 문제가 됩니다.
| 우선순위 | 실패 모드 | 대규모 서비스의 필수 방어책 |
|---|---|---|
| P0 | tenant·권한·live/test 혼합 | 신뢰된 context로 scoped key 생성, 반환 전 재인가, 충돌 테스트 |
| P0 | cache poisoning | webhook raw body 서명 검증, event ID dedupe, payload 직접 저장 금지, 원본 재조회 |
| P0 | stale 값으로 결제·권한 결정 | 표시 경로와 비가역 결정 경로 분리, 결정 직전 authoritative recheck |
| P0 | PII·PCI·residency 위반 | 필드 allowlist와 최소화, PAN/CVC·API key·webhook secret 캐시 금지, TLS·저장 암호화·IAM·허용 리전·명시 삭제 |
| P1 | 리전 간 무효화 지연·역순 webhook | durable queue, 멱등 consumer, 리전별 ack, retry·DLQ·replay, generation conditional fill |
| P1 | failover 뒤 cold-cache storm | 제한적 prewarm, weighted traffic ramp, 글로벌 origin 동시성 예산, backoff·circuit breaker |
| P1 | schema/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 시나리오는 단계별로 분리합니다.
- cold: 모든 캐시를 비운 뒤 원본 경로의 p50/p95/p99와 오류율을 측정합니다.
- warm: 실제 키 분포로 L1·L2 hit ratio와 원본 호출 감소율을 측정합니다.
- burst: 인기 키 만료 직후 트래픽을 집중해 stampede와 Stripe
429를 확인합니다. - degraded cache/origin: Redis timeout, DynamoDB throttling, Stripe 지연을 주입해 fail-open과 backpressure를 확인합니다.
- recovery: 장애를 제거한 뒤 queue backlog, 연결 수, 지연이 정상 수준으로 회복되는 시간을 측정합니다.
- isolation: 같은 객체 ID를 test/live, 서로 다른 account·권한·projection으로 요청해 교차 노출이 없는지 확인합니다.
- race:
miss 시작 → webhook invalidate → 이전 fetch 완료순서를 재현해 stale fill이 거부되는지 확인합니다. - 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 응답률, 요청당 총비용을 같은 대시보드에서 봐야 합니다.
단계적 배포 체크리스트
- 캐시 대상 필드와 freshness SLA를 문서화합니다.
- tenant·환경·버전·권한을 포함한 cache key를 테스트합니다.
- 캐시 없이도 동작하는 timeout·retry·rate limit·backpressure 경로를 먼저 검증합니다.
- DynamoDB L2부터 canary로 적용하고 hit ratio와 비용을 측정합니다.
- 필요성이 입증된 경우에만 Redis L1 또는 API Gateway 캐시를 추가합니다.
- webhook 중복·역순·지연·DLQ 복구 테스트를 자동화합니다.
- 계층별 read/write/bypass kill switch와 namespace generation rollback 절차를 준비합니다.
결론
이 구조의 가치는 “캐시를 두 개 붙였다”는 데 있지 않습니다. 권위 저장소를 명확히 하고, 원본 호출을 얼마나 줄였는지와 stale 위험·운영 비용을 함께 측정하는 데 있습니다.
Redis L1, DynamoDB L2, API Gateway 응답 캐시는 서로 대체 관계가 아니라 서로 다른 병목을 푸는 도구입니다. freshness 요구와 실제 hit ratio를 먼저 측정하고 가장 단순한 계층부터 도입하는 것이 핵심입니다. 캐시는 정답이 아니라 부하 테스트와 장애 주입으로 검증할 가설입니다.
참고 자료
- Stripe Developer Blog — Optimizing Stripe API performance with Lambda, caching, ElastiCache, and DynamoDB
- AWS — API Gateway caching
- AWS — DynamoDB TTL
- AWS — Lambda best practices
- AWS — Internet access for VPC-connected Lambda functions
- AWS — ElastiCache connection management
- AWS — Redis caching patterns
- AWS — ElastiCache Global Datastore
- AWS — DynamoDB Global Tables
- AWS — ElastiCache in-transit encryption
- Stripe — Webhooks
- Stripe — Rate limits
- Stripe — Integration security guide
- Intel — Intel 64 and IA-32 Architectures Optimization Reference Manual