Cache를 도입하는 가장 흔한 이유는:
DB Query 감소
API Response 단축
외부 API 호출 감소
서버 부하 감소
다.
하지만 Cache를 잘못 사용하면:
오래된 상품 정보
잘못된 가격
이미 취소된 주문 상태
권한 변경 미반영
DB와 Cache 불일치
같은 문제가 생긴다.
즉:
Cache는 성능 최적화 도구이면서 동시에 새로운 상태 저장소다.
원래:
PostgreSQL
=
Source of Truth
이었다.
Cache를 넣으면:
Redis
Local Memory
CDN
Browser Cache
에도 데이터가 존재한다.
하지만 중요한 원칙은:
Cache
≠
Source of Truth
이다.
예:
상품 가격
→ PostgreSQL
주문 상태
→ PostgreSQL
Feature Flag
→ DB / SSM
Cache는:
원본 데이터를 더 빠르게 읽기 위한 복제본
으로 본다.
모든 Query에 Cache를 붙이지 않는다.
좋은 후보:
읽기 빈도 높음
변경 빈도 낮음
계산 비용 큼
외부 API 호출 비용 큼
약간 오래된 값 허용 가능
이다.
예:
상품 목록
상품 상세
FAQ
카테고리
공통 코드
배너
검색 조건 Metadata
통신사/요금제 기본 정보
등이다.
예:
주문 현재 상태
재고
가격 확정 정보
관리자 권한
고객 개인정보
중복 신청 판단
결제/금전 상태
같은 데이터다.
오래된 값이 실제 업무 오류로 이어질 수 있다.
다음 질문을 먼저 한다.
얼마나 자주 읽는가?
얼마나 자주 바뀌는가?
몇 초 오래돼도 괜찮은가?
잘못된 값이 보이면 어떤 피해가 있는가?
다시 계산하기 쉬운가?
예:
FAQ
10분 오래돼도 큰 문제 없음
반면:
주문 상태
10분 오래되면 문제
다.
즉 Cache TTL은 기술 숫자가 아니라 업무 요구다.
TTL:
Time To Live
Cache Entry가 얼마 동안 유효한지를 의미한다.
예:
product:123
TTL 300초
이면 5분 후 만료된다.
장점:
Cache Hit 증가
DB 부하 감소
하지만:
Stale Data 증가
한다.
Cache Miss 증가
DB Query 증가
Redis 사용 의미 감소
할 수 있다.
예:
FAQ
30분
상품 상세
5분
카테고리
10분
통신사 공통 코드
1시간
등이다.
정확한 값은 실제 변경 빈도와 트래픽을 보고 정한다.
Cache 종류는 여러 가지다.
Browser Cache
CDN Cache
Application Local Cache
Redis
DB Query Result Cache
각 위치마다 특성이 다르다.
사용자 브라우저가 직접 보관한다.
예:
정적 JS
CSS
이미지
폰트
에 매우 적합하다.
CloudFront 같은 CDN이 Edge에서 응답을 보관한다.
예:
상품 이미지
배너
정적 Asset
에 적합하다.
NestJS Process Memory에 저장하는 방식이다.
예:
Map
LRU Cache
같은 형태다.
매우 빠름
추가 Network 없음
구현 단순
이다.
서버가 여러 대면:
Server A Cache
Server B Cache
가 서로 다를 수 있다.
하지만 Cache라면 이 자체는 문제가 아니다.
Source of Truth에서 다시 만들면 된다.
여러 서버가 하나의 Cache를 공유할 수 있다.
Server A
↓
Redis
↑
Server B
형태다.
여러 Instance
공유 Cache 필요
TTL 관리
Atomic Counter
Rate Limit
Distributed Lock
등이다.
현재:
단일 또는 소규모 EC2
PostgreSQL
NestJS
라면 먼저:
Query 최적화
Index
TanStack Query Client Cache
CloudFront
만으로 해결 가능한지 본다.
예:
SELECT
매번 8초
걸리는 Query에 Cache를 붙여:
Cache Hit
20ms
로 만들었다고 끝이 아니다.
Cache Miss 때 여전히 8초다.
순서:
Query 분석
Index
Pagination
필요 Column만 Select
N+1 제거
그 다음 Cache
가 좋다.
가장 흔한 방식이다.
흐름:
Request
↓
Cache 조회
Hit
→ 반환
Miss
→ DB 조회
→ Cache 저장
→ 반환
이다.
const cached =
await cache.get(key);
if (cached) {
return cached;
}
const result =
await repository.find(...);
await cache.set(
key,
result,
ttl,
);
return result;
단순함
필요한 데이터만 Cache
Cache 장애 시 DB fallback 가능
이다.
첫 Miss에는 DB Query가 발생한다.
그리고 여러 요청이 동시에 Miss하면 문제가 생긴다.
예:
product:list
TTL 만료
되는 순간 고객 요청 1,000개가 동시에 들어온다.
모두:
Cache Miss
를 본다.
결과:
DB Query 1,000개
가 동시에 실행될 수 있다.
이걸 Cache Stampede 또는 Thundering Herd라고 볼 수 있다.
평소에는:
Redis가 99% 요청 처리
해서 DB가 조용하다.
그런데 Cache가 한꺼번에 만료되면:
DB로 전체 Traffic 이동
한다.
Cache마다 TTL을 조금 다르게 한다.
예:
기본 TTL
300초
실제 TTL
270~330초
처럼 만든다.
많은 Cache Key가:
정각 10:00
에 동시에 생성됐다면:
10:05
에 모두 동시에 만료될 수 있다.
Jitter를 주면 만료 시점이 분산된다.
예:
정확히 5분 후 반드시 만료
라는 Security/Business 요구가 있으면 Random TTL을 함부로 쓰지 않는다.
Cache Miss가 발생했을 때:
첫 요청만 DB 조회
하고 나머지는 결과를 기다리게 한다.
Request A
Cache Miss
→ DB Query
Request B
Cache Miss
→ A의 Query 대기
Request C
Cache Miss
→ A의 Query 대기
결과:
DB Query 1회
만 실행된다.
예:
Map<cacheKey, Promise<Result>>
를 이용할 수 있다.
Server A
Server B
가 동시에 Miss를 보면 각 서버에서 하나씩 Query할 수 있다.
Redis Lock 등을 활용할 수 있다.
DB Query가 충분히 싸다면:
두 서버가 각각 한 번 Query
정도는 문제가 아닐 수 있다.
트래픽 규모에 맞춘다.
Cache가 만료됐더라도:
조금 오래된 값
을 먼저 반환한다.
뒤에서 최신 값을 다시 가져온다.
Fresh Cache
→ 바로 반환
Stale Cache
→ 기존 값 반환
→ Background Refresh
완전 만료
→ DB Query
이다.
예:
FAQ
상품 설명
배너
카테고리
통계 요약
처럼 몇 초~몇 분 오래된 데이터가 큰 문제가 아닌 경우다.
주문 상태
권한
가격 확정
재고
인증 상태
처럼 최신성이 중요한 데이터다.
예:
Fresh
5분
Stale 허용
10분
으로 나눌 수 있다.
예:
{
"data": {},
"cachedAt": "2026-10-04T00:00:00Z"
}
처럼 저장한다.
현재 시간이 Fresh Window를 넘었는지 판단한다.
Cache에서 가장 어려운 문제 중 하나다.
DB 값이 바뀌었는데 Cache는 이전 값을 가지고 있다.
기존:
product:iphone18
price = 1,500,000
관리자가:
1,450,000
으로 변경했다.
DB는 새 값인데 Cache는 5분 동안 옛 가격을 보여줄 수 있다.
DB 변경 후 Cache는 건드리지 않는다.
TTL이 지나면 최신 값이 들어간다.
구현 매우 단순
이다.
TTL 동안 Stale Data가 존재한다.
따라서 오래된 값이 허용 가능한 데이터에 적합하다.
예:
상품 Update
↓
DB Commit
↓
cache.del(productKey)
한다.
다음 조회에서 최신 값을 다시 Cache한다.
이를 Cache-Aside의 Write 전략으로 볼 수 있다.
DB Update
SUCCESS
↓
Cache Delete 전에
Server Crash
하면 Cache에 오래된 값이 남는다.
Cache Invalidation을 하더라도:
TTL = 무한대
로 두면 위험하다.
Delete Event가 유실될 수 있기 때문이다.
Write 후 Invalidate
+
TTL
이다.
Invalidate 실패해도 TTL이 최종적으로 복구한다.
1001의 Outbox를 이용해:
Product Update
+
CACHE_INVALIDATE_REQUESTED
COMMIT
후 Consumer가 Cache를 삭제할 수 있다.
Cache Stale이 몇 분 허용되고 TTL이 있다면 단순:
DB Update
→ best-effort cache.delete
만으로도 충분할 수 있다.
예:
FAQ
Invalidate 실패
→ 큰 문제 없음
하지만:
상품 가격
Invalidate 실패
→ 고객 잘못된 가격 노출 가능
이면 더 강한 구조가 필요하다.
예:
Product Description
10분
Product Price
30초
처럼 하나의 Product DTO 전체를 같은 TTL로 Cache하지 않을 수도 있다.
Cache 단위를 결정한다.
예:
product:{id}
또는:
product-list:{category}:{page}
이다.
예:
all-products
하나로 10,000개 상품을 넣으면:
한 상품 변경
→ 전체 Cache 삭제
가 필요하다.
Key 수 폭증
Network 호출 증가
Invalidation 복잡
이 생긴다.
예:
product:{id}
category:{id}:products:{page}
faq:list
처럼 한다.
관리자 주문 목록은:
필터
검색
페이지
정렬
상태
조합이 많다.
예:
status=WAITING
page=1
status=WAITING
page=2
search=홍길동
page=1
carrier=LGU
status=DONE
...
조합이 끝없이 생긴다.
예:
Index
Pagination
Search 구조 개선
필요 Column Select
이 더 중요하다.
Cache Key 종류가 과도하게 많아지는 문제다.
예:
search:{query}
에서 모든 검색어를 Cache하면 Key가 끝없이 늘어난다.
공격자가:
random query 100만 개
를 날려 Redis Memory를 소비하게 할 수도 있다.
모든 Miss 결과를 Cache하지 않고:
자주 조회되는 Query만
Cache할 수 있다.
현재 규모에서는 복잡한 Admission Algorithm까지 필요 없다.
예:
product:999999
→ 없음
매번 DB를 조회할 필요가 없다.
존재하지 않는 결과를 짧게 Cache하는 방식이다.
예:
NOT_FOUND
TTL 30초
이다.
공격/오류로 존재하지 않는 ID가 계속 조회되면:
Cache Miss
→ DB Query
가 반복된다.
Negative Cache가 도움이 된다.
왜냐하면:
없던 Product가 방금 생성
될 수 있기 때문이다.
예:
const result = await cache.get(key);
if (!result) {
라고만 하면 Cache된 null과 Miss를 구분하기 어렵다.
예:
{
"found": false
}
같이 저장할 수 있다.
예:
{service}:{domain}:{version}:{identifier}
형태가 좋다.
예:
togethermall:product:v2:123
1002의 Schema/API Versioning과 연결된다.
Response 구조가 바뀌었는데 기존 Cache가 남아 있을 수 있다.
기존:
product:v1:123
새 버전:
product:v2:123
을 사용한다.
새 Application은 v2 Key만 사용한다.
v1은 TTL 이후 자연스럽게 사라진다.
예:
FLUSHALL
을 Production에서 실행하면 모든 Cache가 한꺼번에 사라진다.
결과:
DB Traffic 급증
할 수 있다.
대규모 Cache를 비운 뒤:
인기 상품
공통 코드
메인 페이지
같은 데이터를 미리 채울 수 있다.
실제 접근되는 데이터만 Cache하는 Lazy 방식이 단순하다.
Warm
주요 Entry가 존재
Cold
Cache가 비어 있음
이다.
Application Release나 Redis 재시작 이후 Cache가 비어 있을 수 있다.
평소보다 DB Load가 증가할 수 있다.
예:
Cache Hit Rate
95%
→
20%
로 떨어졌다면 DB 부하 증가 원인이 될 수 있다.
hits
/
(hits + misses)
이다.
높다고 무조건 좋은 것은 아니다.
성능 Metric만 보면 안 된다.
Freshness도 같이 본다.
예:
cache_hit_total
cache_miss_total
cache_error_total
cache_set_total
cache_eviction_total
cache_latency
cache_entry_age
정도를 볼 수 있다.
추가로:
Memory Usage
Connections
Evictions
Command Latency
Key Count
등을 본다.
Redis Memory가 부족해 Cache Entry를 자동으로 밀어낼 수 있다.
Cache가 없어도 Source of Truth에서 다시 조회할 수 있어야 한다.
예:
Session 상태를 Cache에만 저장
원본 없음
이면 Cache가 단순 Cache가 아니라 State Store다.
역할을 명확히 구분한다.
Cache
없어져도 재생성 가능
Session
없어지면 로그인 상태 소실
이다.
운영 정책도 달라진다.
Redis가 다운됐다고 하자.
다음 선택이 있다.
DB fallback
Fail request
Stale local cache
기능 제한
업무에 따라 정한다.
예:
상품 Cache Redis 장애
↓
DB 직접 조회
하면 서비스는 조금 느려지지만 계속 동작한다.
Redis 장애:
100% Cache Miss
↓
DB Traffic 20배
가 될 수 있다.
예:
DB fallback 동시 요청 제한
을 둔다.
Cache 장애 시:
인기 상품만 제공
일부 무거운 추천 기능 OFF
검색 제한
같은 방식도 가능하다.
Redis Timeout이 발생할 때마다:
매 Request
2초 Redis Timeout
+
DB Query
하면 응답이 매우 느려진다.
예:
Cache Circuit Open
↓
바로 DB fallback
한다.
Half Open
↓
Redis Ping
↓
정상
→ Cache 복구
같은 구조다.
현재 규모에서는:
짧은 Redis Timeout
빠른 Fallback
만으로 충분할 수 있다.
Cache는 성능을 위한 시스템인데:
Redis 5초 기다림
은 목적과 반대다.
보통 Cache 호출은 빠르게 실패하고 원본으로 넘어가는 편이 낫다.
Cache
수십~수백 ms 수준
DB
더 긴 시간
처럼 업무/Infra에 맞게 조절한다.
Redis에는 보통 JSON/String 형태로 저장한다.
예:
{
"id": "product_123",
"name": "..."
}
이다.
DTO 구조가 바뀌면 기존 Cache Entry가 깨질 수 있다.
1002의 Versioning과 같은 문제다.
product:v3:123
처럼 사용한다.
Redis도 데이터 저장소다.
예:
이름
전화번호
주소
를 그대로 Cache하면 데이터 사본이 하나 더 생긴다.
관리자 주문 Detail을 Cache해서 몇 ms 줄이는 것보다:
PII 복제 감소
가 더 중요할 수 있다.
하지만:
TTL 24시간
이면 그동안 사본이 유지된다.
정책을 고려한다.
예:
logger.info({
cacheKey,
cachedValue,
});
는 개인정보가 로그에 복제될 수 있다.
key
hit/miss
latency
ttl
정도면 충분하다.
나쁜 예:
user:01012345678:orders
Redis Key 목록만 봐도 개인정보가 노출된다.
user:u_123:orders
처럼 사용한다.
권한 계산이 복잡하면 Cache하고 싶을 수 있다.
하지만 권한 변경 반영 지연이 위험하다.
관리자 권한을 제거했는데:
permission cache TTL
1시간
이면 1시간 동안 기존 권한을 사용할 수 있다.
예:
RBAC 변경
↓
permission:user:123
삭제
한다.
Cache 조회 실패 시:
기존 권한 허용
보다 DB 재조회가 안전하다.
Logout/Revocation과의 Consistency를 고려해야 한다.
0928에서 다룬 Dynamic Config도 Cache가 필요하다.
예:
Feature Flag TTL
30초
이다.
예:
AI Production Action Kill Switch
5초
처럼 한다.
Cache 때문에 Kill Switch가 늦게 반영되면 위험하다.
SSM 일시 장애 시:
마지막 정상값
을 잠깐 유지할 수 있다.
예:
AI_PROD_ACCESS=false
로 껐는데 Cache가 과거:
true
를 유지하면 안 된다.
예:
FAQ Cache 장애
→ DB fallback
Kill Switch Cache 장애
→ Fail Closed
상품 Cache 장애
→ DB fallback
처럼 정책을 나눈다.
개념적으로 다음처럼 분류할 수 있다.
STRONG-ish
BOUNDED_STALENESS
BEST_EFFORT
예:
상품 설명
최대 5분 오래된 값 허용
처럼 오래될 수 있는 최대 시간을 정의한다.
예:
인기 검색어
내부 Dashboard 일부
처럼 약간의 지연이나 누락이 큰 문제가 없는 데이터다.
예:
상품 Cache
99% 요청에서 100ms 이하
또는:
Stale Age
5분 이하
같은 목표다.
예:
상품이 변경되면:
product:{id}
product-list:{category}
search:{keyword}
등 여러 Cache가 영향을 받을 수 있다.
한 DB Row가 여러 Derived Cache에 포함될 수 있다.
예:
product:123
tag=product:123
category-list
tag=category:iphone
같이 관련 Cache를 Group으로 관리할 수 있다.
현재 규모에서는 명시적 Key Pattern 정도가 더 단순할 수 있다.
예:
const cacheKeys = {
productDetail: (id: string) =>
`product:v1:${id}`,
categoryProducts: (
categoryId: string,
page: number,
) =>
`category:${categoryId}:products:${page}`,
};
처럼 중앙화한다.
나쁜 예:
`product-${id}`
`products:${id}`
처럼 규칙이 제각각이면 Invalidation이 어려워진다.
예:
interface CacheService {
get<T>(key: string): Promise<T | null>;
set<T>(
key: string,
value: T,
ttl: number,
): Promise<void>;
delete(key: string): Promise<void>;
}
기존 아키텍처 원칙과 같다.
Application
↓
Cache Abstraction
↓
Redis Infrastructure
로 분리한다.
예:
@Cacheable()
을 모든 Method에 붙이면 Invalidation이 어디서 필요한지 알기 어려워질 수 있다.
예:
getProduct()
invalidateProduct()
처럼 한다.
DB에 쓰는 동시에 Cache에도 새 값을 쓴다.
Write
↓
DB
↓
Cache Set
이다.
다음 Read가 Cache Hit다.
또 Dual Write 문제다.
DB SUCCESS
Cache SET FAILED
가능하다.
TTL/Invalidate 전략이 필요하다.
Cache에 먼저 쓰고 나중에 DB에 반영한다.
성능은 좋지만 데이터 유실 위험이 높다.
주문/상담처럼 중요한 Write는:
DB 먼저
가 안전하다.
Application이 Cache API를 호출하면 Cache Layer가 Miss 시 DB Loader까지 호출한다.
Cache-Aside를 추상화한 형태다.
현재는 명시적 Cache-Aside가 더 이해하기 쉽다.
TTL 만료 전에 인기 Cache를 미리 갱신한다.
예:
TTL 10분
8분 시점
Background Refresh
한다.
항상 많이 읽히는 메인 페이지 데이터
처럼 Cache Miss가 거의 발생하면 안 되는 경우다.
복잡한 Refresh Scheduler까지 먼저 만들 필요 없다.
예:
새 상품 등록
하면:
product-list:all
category:iphone
brand:apple
등 여러 List Cache가 Stale해질 수 있다.
Detail은 즉시 Invalidate하고,
List는:
짧은 TTL
로 운영한다.
Cache Consistency 요구를 낮출 수 있는 데이터라면 TTL로 해결한다.
Product Update
Detail Cache
즉시 삭제
Category List
TTL 60초
Search Result
TTL 30초
처럼 중요도에 따라 나눈다.
이 경우:
가격 Cache 전용
으로 분리하거나 관련 List Invalidate를 강화한다.
하나의 Cache Entry에 여러 Source 데이터가 합쳐져 있을 수 있다.
예:
Product Detail
Product
Promotion
Review Summary
를 하나의 JSON으로 Cache한다.
따라서 변화 빈도가 다른 데이터를 너무 많이 묶지 않는다.
예:
product:123:base
product:123:price
product:123:review-summary
처럼 나눌 수 있다.
Trade-off다.
현재는 실제 병목이 생길 때 분리한다.
1001과 비슷한 문제다.
DB Update 후 Cache Invalidation이 몇 초 늦게 적용되면:
DB
new
Cache
old
상태가 잠깐 존재한다.
따라서:
얼마나 오래 Stale해도 되는가?
를 정의해야 한다.
예:
DB product price
1,450,000
Cache
1,500,000
불일치다.
예:
1000번 중 1번
Cache와 DB 동시 조회
값 비교
한다.
실제 응답은 Cache 값을 쓰되:
Background DB Compare
를 수행할 수 있다.
샘플링한다.
예:
cache_consistency_mismatch_total
을 기록한다.
예:
category:iphone:version=17
을 별도로 관리한다.
List Key:
category:iphone:v17:page:1
이다.
17 → 18
하면 새 요청은 새 Cache Key를 사용한다.
Bulk Key Delete를 하지 않아도 된다.
하지만 Key 수가 늘어나므로 TTL이 반드시 있어야 한다.
Frontend Asset에서 흔히 쓰는 방식이다.
예:
banner.jpg?v=123
또는 Hash 파일명.
배너/이미지를 같은 URL로 교체했는데 CDN Cache가 남아 있을 수 있다.
예:
banner-v1.webp
banner-v2.webp
또는 Content Hash를 사용한다.
새 URL은 자동으로 Cache Miss가 된다.
예:
Cache-Control:
max-age=31536000, immutable
같이 긴 Cache를 주고 파일명에 Hash를 넣는다.
HTML이 새 Asset을 가리켜야 하기 때문이다.
예:
CloudFront
→ Static Content
Redis
→ API Data
TanStack Query
→ Client Server State
역할이 다르다.
Frontend에서는 이미:
Query Cache
를 사용하고 있을 수 있다.
TanStack Query의:
staleTime
은 데이터가 얼마 동안 Fresh로 간주되는지를 결정한다.
Query가 사용되지 않을 때 Cache를 얼마 동안 유지할지 결정한다.
예:
Server Cache
5분
Frontend staleTime
10분
이면 실제 사용자는 최대 10분 이상 오래된 값을 볼 수 있다.
DB
↓
Redis 5분
↓
Frontend 5분
이라고 단순히 최대 5분이라고 생각하면 안 된다.
상황에 따라 더 길게 느껴질 수 있다.
예:
상품 가격
전체 시스템 최대 Stale 30초
라고 정한다.
그 안에서:
Redis 10초
Frontend 5초
처럼 배분한다.
예:
관리자가 주문 상태 변경
후 TanStack Query에서:
invalidateQueries(['orders'])
를 실행한다.
Backend Redis Cache와는 별개다.
사용자 경험을 위해 서버 응답 전 UI를 먼저 바꿀 수도 있다.
서버에서 상태 전이가 거부될 수 있다.
예:
WAITING
→ COMPLETED
가 정책상 실패했는데 UI만 완료로 보이면 혼란이 생긴다.
또는 Optimistic Update를 하더라도 실패 시 확실히 Rollback한다.
업무 위험도에 따라 구분한다.
현재 규모에서는:
1. TTL Jitter
2. 짧고 적절한 TTL
3. 필요한 곳만 Single Flight
4. SWR
정도가 현실적이다.
복잡도와 Lock 장애가 생긴다.
특정 Cache Key에 요청이 몰릴 수 있다.
예:
main-page
하나에 초당 수천 요청이 몰린다.
현재 규모에서는 큰 문제가 아닐 가능성이 높지만 개념은 알아둔다.
따라서:
SWR
Single Flight
가 효과적이다.
새 Cache Version으로 배포:
product:v3
하면 v3 Cache는 모두 비어 있다.
현재는 필요 시:
메인 페이지
인기 상품
정도만 Warm-up할 수 있다.
Cache는 보조 시스템이다.
필요하면 배포 후 Background로 Warm-up한다.
잘못된 데이터가 Cache에 들어가 오랫동안 제공되는 문제다.
예:
DB Query Bug
↓
잘못된 값 Cache
↓
수천 요청에 동일 오류 노출
이다.
따라서 Cache Set 전에:
Response Validation
이 중요할 수 있다.
상품 가격:
-1원
같은 비정상 값은 Cache하기 전에 검증한다.
예:
동일 상품 설명 분석
동일 Git Commit 분석
동일 Prompt Context
을 반복 호출한다면 Cache할 수 있다.
예:
model
promptVersion
inputHash
policyVersion
을 포함한다.
Model이나 System Prompt가 달라지면 결과 의미가 바뀐다.
0916에서 사용한:
task
commitSha
promptVersion
contextHash
model
을 Cache Key로 활용할 수 있다.
Token/API 비용 감소
로컬 LLM 실행 시간 감소
같은 분석 반복 방지
다.
Cache하면:
첫 번째 결과를 재사용
하는 것이다.
새 결과 생성 기회를 없앤다.
예:
Commit Summary
Cache 적합
반면:
창의적 문구 생성
은 매번 다른 결과가 필요할 수 있다.
Hash Key와 Artifact Reference 중심으로 한다.
예:
Commit 기반 Report
장기 가능
임시 질문 응답
짧게
업무에 따라 다르다.
1003에서 다뤘듯 Cache도 데이터 사본이다.
TTL 자체가 Retention Policy 역할을 한다.
가능하면 TTL을 둔다.
예외적으로 영구 Key가 필요하다면 명확한 관리 정책이 필요하다.
특히 Dynamic Data에서:
TTL 없음
은 Stale Data가 영구화될 수 있다.
새 Key는 계속 생기는데 TTL이 없으면 Memory가 계속 증가한다.
CacheService에서:
set(
key,
value,
ttl,
)
형태로 TTL 필수화한다.
예:
default
5분
을 둘 수 있지만 중요한 Cache는 명시적으로 설정한다.
라이브러리마다:
즉시 만료
무제한
의미가 다를 수 있다.
Wrapper에서 정책을 명확히 한다.
환경별로 Key가 섞이지 않게 한다.
예:
prod:togethermall:product:v1:123
staging:togethermall:product:v1:123
이다.
가능하면 Production은 별도 Resource가 더 안전할 수 있다.
서로 다른 Domain이 같은 Key를 사용하지 않도록 Prefix를 둔다.
큰 JSON을 Cache하면:
Network 비용
Serialization 비용
Memory 사용
이 커진다.
예:
5MB JSON
Redis GET
보다 DB에서 필요한 20개 Column만 조회하는 편이 나을 수 있다.
DTO 전체를 무조건 넣지 않는다.
아주 큰 Cache Value를 압축할 수도 있다.
하지만 CPU 비용과 복잡도가 있다.
현재는 필요할 때만 고려한다.
예:
Cache Key 폭증
↓
Redis Memory 부족
↓
Queue/Job 영향
이 생길 수 있다.
Queue 데이터가 Eviction되면 심각할 수 있다.
가능하면 Namespace/Instance 분리 또는 Resource 정책을 명확히 한다.
현재 Memory 사용량과 Eviction 정책을 먼저 확인한다.
예:
최대 512MB
등 제한을 둔다.
TTL 조정
큰 Entry 제거
불필요 Cache 제거
를 검토한다.
Memory 부족 시 어떤 Key를 제거할지 정책이다.
최근 사용되지 않은 데이터 제거.
사용 빈도가 낮은 데이터 제거.
하지만 Redis Instance가 Queue/Session을 같이 저장하면 신중해야 한다.
Production에서:
KEYS *
같이 전체 Key를 한 번에 조회하는 명령은 큰 Redis에서 부담이 될 수 있다.
점진적으로 조회한다.
굳이 전체 Redis Console을 관리자에게 열 필요는 없다.
예:
Cache 상태 조회
특정 Domain Invalidate
Hit Rate
정도만 제공한다.
Flush All
은 기본 UI에 두지 않는 편이 좋다.
예:
Product 123 Cache 삭제
정도는 운영상 유용할 수 있다.
관리자가 수동으로 Cache를 지웠다면:
actor
key/domain
reason
timestamp
정도를 남길 수 있다.
예:
product cache
v2 → v3
같은 변경은 Release와 연결할 수 있다.
DTO 변경 시:
Release A
v1 읽기 + v2 쓰기
같이 복잡하게 갈 필요 없이 Key Version 변경이 가장 단순할 수 있다.
현재 규모에서는:
Versioned Key
TTL
Warm-up
정도로 충분하다.
필요하다면 Scheduler를 이용해:
인기 상품 Top 100
을 주기적으로 Warm할 수 있다.
Metrics를 보고 결정한다.
단순 Miss에도 이유가 다르다.
NOT_FOUND
EXPIRED
EVICTED
VERSION_CHANGED
MANUAL_INVALIDATION
등이다.
중요한 장애 분석에 도움이 되는 수준만 본다.
Response에:
cachedAt
을 내부적으로 알고 있으면 Stale 분석이 쉽다.
내부 Observability 용도다.
내부 관리자 Debug Mode에서:
cache
HIT
age
12s
같은 Metadata를 볼 수도 있다.
Infra 구조가 드러날 수 있다.
중요 데이터에서는 주기적으로:
Cache Value
vs
DB Value
를 비교할 수 있다.
Cache Delete
로 복구한다.
Source of Truth인 DB를 Cache 값으로 덮어쓰지 않는다.
예:
Invalid JSON
Unsupported Cache Version
Consistency Mismatch
를 발견하면:
Entry 삭제
↓
다음 요청에서 재생성
한다.
Cache는 재생성 가능한 데이터이기 때문이다.
DB처럼 직접 수정하려고 하지 않는다.
Cache에서 가져온 JSON이 Parsing되지 않으면:
CACHE_DESERIALIZATION_FAILED
로 기록하고 Entry를 삭제한다.
CACHE_VERSION_UNSUPPORTED
이면 마찬가지로 Miss처럼 처리할 수 있다.
예:
Redis connection refused
를 고객에게 보여주지 않는다.
DB fallback이 가능하면 정상 응답한다.
예:
{
"event": "cache.error",
"domain": "product",
"operation": "get",
"errorCode": "CACHE_TIMEOUT"
}
정도다.
Value 자체는 로그하지 않는다.
예:
CACHE_TIMEOUT
CACHE_UNAVAILABLE
CACHE_SERIALIZATION_FAILED
CACHE_DESERIALIZATION_FAILED
CACHE_VERSION_UNSUPPORTED
CACHE_INVALIDATION_FAILED
정도로 분류할 수 있다.
예:
FAQ
TTL 5분
이면 Warning 정도일 수 있다.
도메인에 따라 Severity가 달라진다.
예:
cacheDomain
keyVersion
hit
latency
ttl
정도를 기록한다.
Key 안에 내부 ID가 너무 많거나 민감할 수 있다.
Domain + hashed key 형태도 가능하다.
API Trace 안에:
cache.get
db.query
cache.set
Span을 보면 병목을 알기 쉽다.
GET /products/123
120ms
cache.get
5ms
MISS
db.query
90ms
cache.set
4ms
이다.
GET
12ms
cache.get
4ms
HIT
처럼 보인다.
예:
p95
400ms → 120ms
DB QPS
500 → 120
처럼 확인한다.
Cache는 유지보수 비용이 있다.
효과가 측정됨
Invalidation이 단순함
Stale 허용 범위가 명확함
장애 시 fallback 가능
이다.
왜 있는지 모름
TTL이 무한대
Invalidation 위치를 모름
Redis 장애 시 서비스도 장애
PII가 복제됨
Metrics 없음
이다.
예:
CACHE_FIRST
DB_FIRST
CACHE_ONLY
STALE_WHILE_REVALIDATE
등을 개념적으로 나눌 수 있다.
Cache Miss가 곧 데이터 없음으로 판단되면 Source of Truth가 Cache가 되어버린다.
Cache First
Miss
→ DB
Set Cache
이다.
GET /products/:id
↓
product:v1:{id}
Hit
→ 반환
Miss
→ ProductRepository
→ Cache 5분
→ 반환
이다.
UpdateProductUseCase
↓
DB Transaction Commit
↓
product:v1:{id}
삭제
한다.
상품 Detail 변경마다 모든 List Key를 찾지 않고:
category product list
60초
정도로 둘 수 있다.
FeatureFlag Update
↓
관련 Local/Redis Cache Invalidate
한다.
Kill Switch는 짧은 TTL도 유지한다.
실시간성, 필터 조합, PII 등을 고려하면 DB Query 최적화가 더 단순할 수 있다.
정적 Asset / 이미지
CloudFront
Frontend TanStack Query
staleTime 정리
상품 / FAQ / 카테고리
Backend Cache
공통 코드 / 외부 API 응답
정말 필요한 고비용 Query
정도로 가는 것이 현실적이다.
다음이 실제로 발생할 때 가치가 커진다.
DB Read 부하 높음
동일 Query 반복 많음
서버 여러 대
외부 API 비용 큼
Local Cache 일관성 문제가 실제 발생
운영해야 할 Component가 하나 더 늘어난다.
Monitoring
Backup 여부
Memory
Eviction
Network
Version Upgrade
를 관리해야 한다.
예:
현재 Memory 사용량
maxmemory
eviction policy
Queue Data 구조
를 확인한다.
Queue 안정성을 해치지 않는지 확인한다.
변경이 거의 없는:
공통 코드
를 1분 Local Cache로 두는 정도는 매우 단순하다.
서버별 Cache 차이가 오래 지속되지 않게 한다.
나중에는:
L1 Local Cache
↓
L2 Redis
↓
DB
형태도 가능하다.
Cache Layer가 두 개면 Invalidation도 두 개를 처리해야 한다.
Browser
CDN
Frontend Query
Local
Redis
DB
어디의 Stale Data인지 찾아야 한다.
실제로 효과가 있는 곳만 사용한다.
값이 오래됐다는 신고가 들어오면:
DB 값
Redis 값
Frontend Query Cache
CDN
Browser
순서로 확인한다.
API 요청 Trace에서:
cache HIT/MISS
를 확인할 수 있으면 원인 파악이 쉽다.
흔한 원인:
Backend Redis
Frontend staleTime
CDN
Browser Cache
중 하나다.
예:
['orders']
['order', orderId]
둘 다 영향받을 수 있다.
예:
invalidateQueries(['orders'])
invalidateQueries([
'order',
orderId,
])
한다.
불필요한 Refetch가 폭증한다.
Backend Cache Key Registry와 비슷하게:
const orderKeys = {
all: ['orders'],
detail: (id: string) =>
['order', id],
};
처럼 관리한다.
예:
Product
Server TTL 5m
Client staleTime 1m
Order
Server Cache 없음
Client staleTime 0
처럼 표로 정리한다.
예:
interface CachePolicy {
domain: string;
ttlMs: number;
staleWhileRevalidateMs?: number;
cacheNegative?: boolean;
sensitive?: boolean;
}
정도로 관리할 수 있다.
0928과 연결된다.
PRODUCT_CACHE_TTL
을 설정으로 뺄 수 있다.
자주 바꿀 이유가 없다면 코드 상수로도 충분하다.
장애 시:
TTL 10분
→ 30초
로 빠르게 조절해야 할 수 있는 영역이다.
예:
300초 → 1초
로 잘못 설정하면 DB Traffic이 급증한다.
예:
min
5초
max
1시간
같은 범위를 둘 수 있다.
예:
Product TTL
300s → 60s
Reason
가격 변경 반영 단축
을 기록할 수 있다.
Cache 시스템 자체가 문제일 때:
product_cache_enabled=false
로 우회할 수도 있다.
Kill Switch를 누르면 DB가 버틸 수 있는지 알아야 한다.
Cache가 없어도 서비스가 살아야 한다고 설계했더라도 실제 DB Capacity가 부족하면 Cache 장애가 곧 서비스 장애가 될 수 있다.
Staging/Integration 환경에서:
Redis Down
을 가정한다.
API가 fallback하는가?
Timeout이 길지 않은가?
DB가 과부하되지 않는가?
Error가 고객에게 노출되지 않는가?
이다.
0925와 연결한다.
예:
Redis Timeout
Redis Connection Refused
Cache Corrupted JSON
Cache Miss Storm
을 테스트한다.
동시에:
100 requests
를 동일 Key로 보낸다.
Single Flight가 있다면 DB Query 수가 제한되는지 확인한다.
여러 Hot Key가 동시에 만료됐을 때 DB Load를 확인한다.
Jitter 효과를 볼 수 있다.
Fresh TTL이 끝났지만 Stale Window 안에서는:
기존 값 즉시 반환
Background refresh 1회
인지 확인한다.
예:
Request A
DB Old Read 시작
관리자
DB Update
Cache Delete
Request A
Old Data를 Cache Set
할 수 있다.
순서:
1. A DB에서 old value 읽음
2. B DB update
3. B cache delete
4. A old value cache set
결과 Cache가 다시 오래된 값이다.
예:
짧은 TTL
Version Check
Delayed Double Delete
Write-through
등이다.
개념적으로:
DB Update 전 Cache Delete
DB Update
잠깐 뒤 Cache Delete
를 수행한다.
이런 Race가 실제 문제로 나타날 가능성과 복잡도를 비교한다.
대부분은:
DB Commit 후 Delete
+
짧은 TTL
이면 충분하다.
Cache Entry에:
recordVersion
을 저장할 수 있다.
예:
{
"version": 12,
"data": {}
}
이다.
Old Cache를 발견하면 무효화할 수 있다.
하지만 매 Read마다 DB Version을 조회하면 Cache 이점이 줄어든다.
DB Update 후:
PRODUCT_UPDATED
Event를 발행하고 Cache Consumer가 삭제한다.
Server A Local Cache
Server B Local Cache
Server C Local Cache
모두 Event를 받고 삭제한다.
그래서 Local Cache TTL을 유지한다.
같은 Key를 두 번 삭제해도 최종 결과는 같다.
즉 매우 Idempotent한 작업이다.
이미 없음
→ 성공
으로 처리한다.
따라서 Update Event Consumer가 새 값을 Set하기보다:
Delete
만 하는 것이 단순할 수 있다.
이 방식이 Cache-Aside와 잘 맞는다.
검색 결과를 Cache하고 싶을 수 있다.
하지만 Query 조합과 변경 빈도가 높다.
예:
갤럭시
아이폰
폴드
같은 고빈도 검색만 고려할 수 있다.
Cache는 나중이다.
Page 1은 자주 조회되고 Page 50은 거의 조회되지 않을 수 있다.
모든 Page를 Cache하지 않는다.
예:
홈 상품 목록
첫 페이지
30초
정도다.
Cursor 값에 따라 Key가 많아질 수 있다.
관리자 목록 Cache에는 더 불리하다.
예:
오늘 주문 수
상태별 주문 수
같은 통계는 Cache 가치가 있다.
PostgreSQL Index/별도 집계가 더 나을 수도 있다.
Materialized View는 DB 내부에서 계산 결과를 저장한다.
Cache는 Application/Redis에서 결과를 저장한다.
다만 Refresh 전략이 필요하다.
현재 필요할 때 검토한다.
예:
외부 통신사 정보
환율이 아닌 정적 Provider 정보
모델 Metadata
같은 외부 API 결과를 Cache할 수 있다.
같은 데이터를 반복 호출하지 않는다.
예:
503
을 1시간 Cache하면 장애가 복구돼도 계속 실패로 보인다.
단:
404 Resource Not Found
는 Negative Cache가 유용할 수 있다.
외부 API Schema 변경이 있을 수 있다.
Cache에 Raw Response를 그대로 오래 저장하지 않는다.
외부 응답을 내부 구조로 변환한 뒤 Cache하는 편이 낫다.
DB뿐 아니라 외부 API에도 같은 문제가 생긴다.
TTL 만료 후 수백 요청이 Provider로 나갈 수 있다.
Single Flight가 특히 유용하다.
Cache Miss
↓
Single Flight
↓
External API
로 호출 수를 제한한다.
Refresh가 실패하면:
Old Cache 삭제
부터 해버리면 서비스가 완전히 Miss 상태가 된다.
기존 Stale 값은 유지한다.
Refresh SUCCESS
→ 새 값 교체
FAIL
→ 기존 값 유지
한다.
외부 API가 3일 장애라고 3일 전 가격 정보를 계속 보여주면 안 된다.
예:
Fresh
5분
Stale 허용
30분
30분 초과
→ Error / Fallback
처럼 한다.
Cache 장애가 서비스 Latency/Error Budget에 영향을 줄 수 있다.
Observability와 연결한다.
비즈니스 SLO:
상품 API p95
DB CPU
Error Rate
가 실제 목표다.
Hit Rate는 원인을 설명하는 보조 Metric이다.
예:
Product Detail p95
220ms
DB QPS
100
CPU
25%
을 기록한다.
p95
70ms
DB QPS
30
CPU
15%
라면 효과가 있다.
Redis 비용
운영 Complexity
Invalidation Bug
Debug 시간
이 성능 이득보다 크면 제거할 수 있다.
Cache마다 다음을 문서화한다.
Source of Truth
Key
TTL
Stale 허용 시간
Invalidation Trigger
Fallback
PII 여부
Metrics
Source
products table
Key
product:v2:{id}
TTL
300s
Invalidate
Product Update
Fallback
DB
PII
No
이다.
Source
DB
TTL
30s
Critical Kill Switch
5s
Fallback
Last Known / Fail Closed 정책별
이다.
Cache
사용 안 함
으로 결정하는 것도 좋은 Cache 설계다.
성능 문제가 없고 Consistency 비용이 크다면 DB 직접 조회가 낫다.
Cache 가능
우선 DB 최적화
기본 DB
Local/Redis Cache 가능
짧은 Cache
불필요 Cache 피하기
가능하면:
Frontend Query Cache
CloudFront
Backend Cache 일부
정도로 끝낸다.
예:
/docs/cache-strategy.md
어떤 Domain을 Cache하는가
TTL
Invalidation
Cache Key 규칙
Redis 장애 시 동작
민감 데이터 정책
정도다.
예:
Product Cache Key
v2 → v3
는 Cold Cache와 DB Load를 만들 수 있다.
큰 Cache 변경이 있으면:
Cold Start 영향
Warm-up 여부
DB Capacity
를 본다.
신규 API 일부만:
v3 Cache
를 사용하도록 할 수도 있다.
새 Query/Read Path Canary와 함께 자연스럽게 검증하면 된다.
Old App:
product:v1
New App:
product:v2
를 사용하면 동시에 배포돼도 서로 Payload를 오해하지 않는다.
1002의 Compatibility 문제를 단순하게 해결한다.
이전 Cache Version은 TTL이 지나면 자동 삭제된다.
별도 Migration이 필요 없다.
그래서 Cache TTL이 중요하다.
예:
Memory > 80%
Warning,
> 90%
Critical
같은 기준을 둘 수 있다.
정확한 기준은 환경에 맞춘다.
평소:
0
이던 Eviction이 갑자기 증가하면 Memory 부족 신호일 수 있다.
Redis 자체가 느려지면 모든 API에 영향을 줄 수 있다.
Local Cache:
microseconds 수준
Redis:
network round trip
이 필요하다.
아주 단순한 DB Query에는 Cache가 이득이 작을 수도 있다.
실제 측정한다.
DB query
3ms
Redis
2ms
이면 복잡도 대비 이득이 거의 없을 수 있다.
100ms 이상
복잡 JOIN
외부 API
큰 계산
처럼 비용이 큰 Query부터 본다.
PK 조회
1~2ms
변경 매우 빈번
강한 일관성 필요
등이다.
예:
100개 상품
상품마다 DB Query
100번
을 각각 Cache하는 것보다 Query 자체를 고친다.
전체 데이터를 Cache에 넣어 Application에서 잘라 쓰지 않는다.
DB Pagination이 우선이다.
원본 설계가 괜찮은 상태에서 추가하는 최적화다.
AI가:
Query 빈도
변경 빈도
응답 비용
일관성 요구
를 기준으로 후보를 정리할 수 있다.
코드만 보고:
이 Endpoint는 반드시 Redis Cache 필요
라고 결론내리면 안 된다.
GET /products/:id
Read Frequency
높을 가능성
Write Frequency
낮음
PII
없음
Staleness Tolerance
수분 가능
Candidate
YES
처럼 후보로만 표시한다.
예:
getProduct
cache 사용
updateProduct
cache delete 없음
을 찾아낼 수 있다.
예:
product:${id}
product-${id}
처럼 Key 규칙이 다른 부분이다.
분석 위주로 사용한다.
Production 전체 Cache 삭제는 DB 부하를 급증시킬 수 있다.
예:
product cache invalidate
정도다.
예:
READ_CACHE_METRIC
자동 가능
INVALIDATE_SINGLE_KEY
낮은 위험
INVALIDATE_DOMAIN
Approval
FLUSH_ALL
금지/강한 승인
처럼 나눌 수 있다.
AI가 장애를 분석해:
product cache stale 추정
까지는 할 수 있다.
실제 Domain Cache 전체 삭제는 승인을 받는다.
증상:
관리자가 가격 변경
DB에는 새 가격
고객 화면은 5분간 옛 가격
Product Update 후
Cache Invalidation 없음
TTL 10분
이다.
Product Update 후 Detail Cache 삭제
TTL 5분 유지
가격 Critical Cache TTL 30초 검토
같이 처리한다.
증상:
10:00 API Latency 급증
DB CPU 95%
원인:
10:00 Cache Warm Version 변경
모든 Key Cold
1,000 요청 동시 Miss
일 수 있다.
TTL Jitter
Hot Key Warm-up
Single Flight
Progressive Release
를 적용한다.
다음 질문을 본다.
Cache가 왜 Stale했는가?
Invalidation은 있었는가?
TTL은 적절했는가?
Redis 장애 시 DB가 버텼는가?
Cache가 없어도 서비스가 가능한가?
중요 Cache에서 이상이 감지되면:
Cache 삭제
→ Source of Truth로 재생성
이 기본 복구다.
DB와 달리 Cache Entry를 직접 수리하는 것보다:
Delete
Rebuild
이 단순하다.
대량 Cache 복구가 필요하면:
Batch Warm
Job을 만들 수 있다.
Concurrency/Rate Limit을 둔다.
고객 Traffic보다 우선하지 않는다.
예:
commitHash + promptVersion
기준으로 이미 Report Artifact가 존재하면 LLM 재실행을 피할 수 있다.
결과가 중요한 기록이므로:
Cache Eviction
에 맡기기보다 Run Artifact로 관리한다.
Cache:
사라져도 다시 계산 가능
Artifact:
실행 결과로 보존 가치 있음
이다.
중간 Git 분석 Result는 Cache가 될 수 있다.
1003과 연결된다.
현재 프로젝트에서는:
Cache 후보 분석
Cache Key Registry
TTL Policy
상품/FAQ 등 저위험 Cache
Update 후 Invalidation
Metrics
Hit/Miss/Latency
Stampede 대응
순서가 좋다.
CloudFront Cache
TanStack Query staleTime
NestJS Local Cache 일부
부터 시작한다.
즉:
Metric
→ 문제 확인
→ Cache 도입
순서다.
1. TanStack Query staleTime / invalidate 정리
2. CloudFront 정적 Asset Cache
3. 상품/FAQ Cache 후보 선정
4. Cache Key Version
5. DB Update 후 Detail Cache Invalidation
정도다.
현재 규모에서는:
다중 Redis Cluster
Multi-level L1/L2 Cache
복잡한 Tag Invalidation Engine
자동 Adaptive TTL
Distributed Single Flight Everywhere
전문 Cache Mesh
까지 필요 없다.
현재 React + TanStack Query + NestJS + PostgreSQL 기반 프로젝트의
Cache 사용 구조를 먼저 분석하고,
필요한 영역에만 최소한의 Cache Strategy를 적용해줘.
목표는 Redis를 무조건 도입하는 것이 아니라,
1. 이미 존재하는 Browser/CDN/TanStack Query Cache를 정리하고
2. 실제로 반복 조회 비용이 높은 Backend Read에만 Cache를 적용하며
3. Stale Data와 Cache Invalidation 문제를 제어하는 것이다.
먼저 현재 코드에서 다음을 확인해줘.
- TanStack Query queryKey 구조
- staleTime / gcTime
- mutation 후 invalidateQueries
- CloudFront / S3 Cache 설정
- NestJS Cache 사용 여부
- Redis 사용 여부
- Redis가 Queue 용도로 이미 사용 중인지
- 자주 호출되는 Product / FAQ / Category Query
- 관리자 Order Query
실제 Traffic Metric이 없는 영역은
Redis가 반드시 필요하다고 단정하지 않는다.
요구사항:
1. Cache 대상과 비대상을 구분한다.
우선 Cache 후보:
- Product Detail
- Product List 일부
- FAQ
- Category
- 공통 Metadata
초기 Cache 비추천:
- 주문 현재 상태
- 관리자 복잡 검색 결과
- 고객 개인정보
- 권한 상태
- 중요 재고/금전 데이터
2. Cache Key 생성 로직을 중앙화한다.
예:
CacheKeys.product(id)
CacheKeys.categoryProducts(categoryId, page)
문자열 Key를 각 Service에서 임의 생성하지 않는다.
3. Cache Key에 Namespace와 Version을 포함할 수 있게 한다.
예:
togethermall:prod:product:v1:{id}
4. CacheService 추상화를 만든다.
최소:
- get
- set
- delete
Redis Library를 Business Service가 직접 호출하지 않게 한다.
5. 모든 Cache Entry에는 TTL을 적용한다.
Dynamic Data에 No-Expiry Cache를 기본으로 허용하지 않는다.
6. Domain별 TTL 정책을 중앙 관리한다.
예:
- Product
- FAQ
- Category
- Feature Flag
실제 TTL 값은 현재 변경 빈도에 맞춰 보수적으로 설정한다.
7. Cache-Aside를 기본 전략으로 사용한다.
Cache HIT
→ 반환
MISS
→ Repository 조회
→ Cache 저장
→ 반환
8. Cache 장애 시
가능한 일반 Read는 DB로 Fallback한다.
Cache 오류 자체가 고객 API Error로 노출되지 않게 한다.
9. Cache 호출 Timeout은 짧게 설정할 수 있게 한다.
Cache 장애 때문에 API가 오래 대기하지 않게 한다.
10. Product Update 후
Product Detail Cache를 Invalidate한다.
DB Transaction이 성공한 뒤 Cache Delete를 수행한다.
11. Cache Invalidation 실패에도
TTL이 최종 복구 수단이 되도록 한다.
12. List/Search Cache는 Invalidation 복잡도를 고려한다.
초기에는 모든 관련 Key를 추적하는 복잡한 Tag Engine을 만들지 말고
짧은 TTL을 우선 고려한다.
13. Negative Cache를 지원할 수 있게 한다.
존재하지 않는 Product ID 반복 조회가
매번 DB까지 내려가지 않게 하되
Negative TTL은 짧게 둔다.
14. Cache Value에서
MISS와 cached null을 구분할 수 있게 한다.
15. Cache Payload 구조 변경 시
기존 Cache와 충돌하지 않도록
Cache Key Version을 사용한다.
16. Cache Payload에
불필요한 PII를 저장하지 않는다.
17. Cache Key에
전화번호, 이메일 등 개인정보를 직접 넣지 않는다.
18. Cache Value를 Log에 출력하지 않는다.
Structured Log에는 다음 정도만 남긴다.
- cache domain
- hit/miss
- latency
- key version
- errorCode
19. Cache Metric을 수집할 수 있게 한다.
- hit
- miss
- error
- latency
- set
- invalidation
Redis가 있다면 가능하면:
- memory
- evictions
도 확인한다.
20. Cache Stampede를 고려한다.
초기 대응:
- TTL Jitter
- Hot Key에 필요한 경우 Single Flight
모든 Cache Key에 Distributed Lock을 도입하지 않는다.
21. TTL Jitter는
정확한 만료 시점이 필요하지 않은 Cache에만 사용한다.
22. Single Flight는
동일 Key에 동시 Cache Miss가 집중되는
실제 Hot Query에만 적용할 수 있게 한다.
23. Stale-While-Revalidate는
FAQ / Product Description 등
Stale 허용 가능한 데이터에서만 고려한다.
Order / Permission 같은 데이터에는 적용하지 않는다.
24. SWR을 적용한다면
Fresh TTL과 Max Stale Time을 구분한다.
Stale Data를 무기한 반환하지 않는다.
25. Cache에서 Invalid JSON 또는
지원하지 않는 Version을 읽으면
DB 값을 덮어쓰지 말고
해당 Cache Entry를 삭제한 뒤 Miss로 처리한다.
26. TanStack Query queryKey를 정리한다.
예:
productKeys
orderKeys
Mutation 성공 후
관련 Detail/List Query만 정확하게 Invalidate한다.
전체 Query Cache를 무조건 Clear하지 않는다.
27. Order 같은 중요 관리자 데이터는
기본적으로 staleTime을 짧게 또는 0으로 유지하는 방향을 검토한다.
28. Product / FAQ처럼 변경 빈도가 낮은 데이터는
적절한 staleTime을 설정한다.
29. Backend Cache TTL과
Frontend staleTime이 겹쳐
실제 Staleness가 과도하게 길어지지 않는지 검토한다.
30. CloudFront / Static Asset은
가능하면 Content Hash 기반 Immutable Asset 전략을 유지한다.
같은 파일명을 덮어쓰고
전체 CDN Invalidation에 의존하는 구조는 피한다.
31. Redis가 현재 Queue 용도로 사용되고 있다면
Cache 추가가 Queue 안정성에 영향을 주지 않는지 확인한다.
특히:
- maxmemory
- eviction policy
- memory usage
를 점검한다.
32. Queue Data가 Eviction될 수 있는 위험이 있다면
Cache 용도를 무리하게 같은 Redis에 추가하지 않는다.
33. 테스트를 작성한다.
필수:
- Cache HIT
- Cache MISS → DB → Set
- Cache unavailable → DB fallback
- Product Update → Cache invalidate
- Cache invalidate 실패 후 TTL fallback 가능
- Negative Cache
- Cached null / Miss 구분
- Unsupported Cache Version
- Cache corrupted payload
- TTL 적용
- Query Mutation 후 TanStack Query invalidate
34. Reliability Test 가능 구조를 만든다.
Scenario:
- Redis Timeout
- Redis unavailable
- 동일 Key 동시 Miss
- Cache Cold Start
- Stale Entry
- Cache Invalidation 누락
35. Redis가 현재 프로젝트에서 실제 이득이 없다고 판단되면
무리하게 도입하지 말고
TanStack Query + CloudFront + DB 최적화 중심 대안을 제시해줘.
목표는 Cache 시스템을 크게 만드는 것이 아니라
실제 성능 병목을 줄이면서
Stale Data와 운영 복잡도를 최소화하는 것이다.
현재 코드베이스에서 Cache 적용 후보를 찾아줘.
코드를 수정하지 말고 분석만 수행한다.
다음 영역을 검토한다.
1. NestJS Controller / QueryService
2. Prisma Query
3. React / TanStack Query
4. S3 / CloudFront
5. 외부 API 호출
6. Redis 사용 코드
7. 반복 계산 로직
각 후보마다 다음을 정리한다.
## 대상
## 현재 Query / 호출 구조
## 변경 빈도
## 예상 Read 빈도
실제 Metric이 없으면
높음/낮음을 사실처럼 단정하지 않는다.
## Stale 허용 가능성
## PII 포함 여부
## Cache 추천 여부
- 추천
- 조건부
- 비추천
## 추천 Cache 위치
- Browser
- CDN
- TanStack Query
- Local Memory
- Redis
## TTL 방향
정확한 값보다는
짧음 / 중간 / 길음으로 먼저 분류한다.
## Invalidation Trigger
## Cache 장애 시 Fallback
## Cache 도입 전 먼저 해야 할 최적화
특히 다음은 Cache보다 Query 최적화를 우선 검토한다.
- 관리자 Order Search
- Pagination
- N+1
- Index 부족
- 불필요한 전체 Column 조회
마지막에 ROI 기준으로
P0 / P1 / P2로 정리한다.
P0:
Cache 또는 Query 최적화 효과가 클 가능성이 높음
P1:
Metric 확인 후 도입
P2:
현재는 Cache 불필요
현재 Cache 구조에 대해
Reliability Test 시나리오를 작성해줘.
Production 실제 Redis나 고객 데이터를 사용하지 않는다.
테스트할 상황:
1. 정상 Cache Hit
2. Cache Miss
3. Redis Timeout
4. Redis Connection Refused
5. Invalid JSON Cache
6. Unsupported Cache Version
7. Product Update 후 Invalidation
8. Invalidation 실패
9. 동일 Key 100개 동시 Miss
10. Hot Key TTL 만료
11. TTL Jitter
12. Negative Cache
13. Stale-While-Revalidate
14. Redis Cold Start
15. Cache Memory Eviction 상황
각 Scenario마다 다음을 확인한다.
- 사용자 Response 성공 여부
- DB Query 횟수
- Cache Hit / Miss
- Latency
- 중복 DB Query
- Stale Data 범위
- Error Code
- Structured Log
- Metric
특히 Cache 장애가
DB 과부하로 전이될 수 있는 Scenario를 구분해서 알려줘.
1003에서는:
데이터를 언제까지 보관하고
언제 안전하게 없앨 것인가
를 다뤘다.
1004에서는 반대로:
같은 데이터를
얼마나 효율적으로 반복해서 읽을 것인가
를 다뤘다.
Cache의 핵심은:
빠른 저장소
자체가 아니다.
가장 먼저:
Source of Truth
가 어디인지 명확해야 한다.
예:
PostgreSQL
=
원본
Redis
=
재생성 가능한 복제본
이다.
Cache 대상도 아무 Query나 고르는 것이 아니다.
좋은 후보는:
Read 많음
Write 적음
계산 비용 큼
약간 Stale해도 괜찮음
인 데이터다.
반면:
주문 상태
권한
중요 가격/재고
고객 개인정보
처럼 잘못된 값의 비용이 큰 데이터는 더 신중해야 한다.
가장 기본적인 전략은:
Cache Aside
다.
Cache 조회
↓
Hit
→ 반환
Miss
→ DB
→ Cache 저장
→ 반환
한다.
Write에서는:
DB Commit
↓
Cache Invalidate
방식이 이해하기 쉽다.
하지만 Cache Delete 자체도 실패할 수 있기 때문에:
Invalidation
+
TTL
을 같이 사용한다.
즉 TTL은 단순 만료 시간이 아니라:
Invalidation이 실패했을 때 오래된 데이터가 영원히 남는 것을 막는 최종 안전장치
이기도 하다.
또 많은 Cache가 동시에 만료되면:
Cache Miss
↓
수많은 DB Query
가 발생하는 Cache Stampede 문제가 있다.
현재 규모에서는 먼저:
TTL Jitter
Single Flight가 필요한 Hot Key만 적용
Stale-While-Revalidate
정도로 대응하면 충분하다.
특히 SWR은:
FAQ
상품 설명
배너
처럼 약간 오래돼도 되는 정보에 적합하고,
Order
Permission
가격 확정 상태
에는 함부로 사용하지 않는다.
또한 Cache Layer가 여러 개라는 점을 기억해야 한다.
Browser
CloudFront
TanStack Query
NestJS
Redis
DB
중 어디에서 Stale Data가 발생했는지 찾아야 하기 때문이다.
그래서 Cache Layer를 무조건 늘리는 것보다:
실제로 효과 있는 위치에만 Cache
하는 것이 중요하다.
현재 프로젝트에서는 우선:
CloudFront 정적 Asset
TanStack Query staleTime / invalidate 정리
상품 / FAQ / Category 일부
공통 Metadata
부터 보는 것이 현실적이다.
관리자 주문 Search처럼:
Filter
Pagination
Search
Sort
조합이 복잡한 기능은 Cache보다:
DB Index
Query Optimization
Pagination
이 먼저다.
그리고 Redis 역시:
있으면 좋아 보이는 기술
로 도입하는 것이 아니라,
반복 Query가 실제 병목이고
Cache 효과가 측정 가능하며
운영 복잡도보다 이득이 큰 경우
도입해야 한다.
전체 흐름을 연결하면:
Request
↓
Cache
↓
Hit / Miss
↓
Source of Truth
↓
Cache Populate
↓
Write
↓
Invalidate
↓
TTL
↓
Observe
↓
Drift / Failure
↓
Delete & Rebuild
이 된다.
결국 Cache Strategy의 핵심은
“얼마나 오래 값을 저장할까”가 아니라, 어떤 데이터는 잠시 오래돼도 괜찮고 어떤 데이터는 절대 그래서는 안 되는지 구분한 뒤, Cache가 없어지거나 틀려도 원본 상태를 안전하게 다시 만들어낼 수 있도록 설계하는 것
이다.