-
Notifications
You must be signed in to change notification settings - Fork 1
캐싱 ROI로 TanStack Query와 BFF를 결정한 과정
프로필을 수정했음에도 화면에 즉시 반영되지 않았다. 새로고침을 해야 변경 사항이 확인되는 상태였다.
TanStack Query를 사용하고 있었기 때문에 기술적인 해결은 어렵지 않았다.
queryClient.setQueryData(['user','me'], updatedUser)
queryClient.invalidateQueries(['user','me'])기술적으로는 이 지점에서 종료해도 무방했다.
그러나 이 작업을 통해 하나의 불편함이 남았다.
우리는 지금까지 어떤 기준으로 캐싱을 결정해왔는가?
- 자주 바뀌지 않으니 캐싱하자
- 중요한 데이터니 항상 최신으로 가져오자
- 느리니 캐싱하면 좋겠다
이러한 판단은 경험에 의존한 직관이었다. 팀 차원에서 합의 가능한 설계 기준이라고 보기에는 부족했다.
따라서 질문을 바꾸었다.
“이 API를 캐싱할 수 있는가?” 가 아니라 “이 API를 캐싱했을 때, 실제로 얼마나 이득이 발생하는가?”
이 시점부터 캐싱을 기술 선택이 아닌 ROI(Return on Investment)의 문제로 정의하였다.
캐싱의 가치는 다음 세 가지로 정리된다.
- 중복 요청 제거 (네트워크 낭비 감소)
- 사용자 체감 성능 개선 (대기 시간 감소)
- 장애 전파 완화 (안정성 향상)
이 세 가지 중 하나라도 수치로 설명할 수 없다면, 그 캐싱은 설계적 결정이 아니라 감각적 최적화에 불과하다.
따라서 최소한의 계측 지표를 정의하고, 이를 기반으로 판단하기로 하였다.
선정한 지표는 다음 여섯 가지이다.
| 지표 | 의미 |
|---|---|
| calls | 세션 내 총 호출 수 |
| duplicateRate | 중복 호출 비율 |
| rehitWithin30sRate | 30초 내 재호출 비율 |
| avgMs | 평균 응답 시간 |
| p95Ms | 95퍼센타일 응답 시간 |
| errorRate | 오류 비율 |
- SRE 용량 계획 (Capacity Planning)
- 트래픽 기반 비용 분석
- API Gateway / Cloud Billing 분석
모든 인프라 최적화는 기본적으로 다음 구조를 따른다.
Impact ≈ Volume × Cost
호출이 거의 없는 API는 최적화해도 ROI가 낮다. 반대로 호출이 많은 API는 작은 개선도 큰 효과를 낸다.
이는 Google SRE Book, AWS Cost Optimization, CDN 최적화 가이드에서 공통적으로 등장하는 원리다.
📌 즉, calls는 ROI 계산의 결과값이 아니라 전제 조건이다.
본 프로젝트는 SPA + SSR 혼합 구조이며, 동일 데이터를 여러 화면에서 재사용하는 패턴이 존재한다.
따라서 호출 수는 단순 사용 빈도가 아니라 구조적 중복 가능성의 신호로 해석하였다.
- Web Performance Best Practices
- React Query / SWR 철학
- CDN Cache Hit Ratio 개념
캐싱의 본질은 다음과 같다.
동일 데이터를 반복적으로 요청하는 낭비를 줄이는 것
CDN에서 가장 중요한 지표는 Cache Hit Ratio이다. duplicateRate는 그 반대 개념이다.
- duplicateRate 높음 → 캐시 기회가 큼
- duplicateRate 낮음 → 캐싱해도 효과가 적음
이 개념은 캐싱 시스템 설계의 핵심 축이다.
resumes, users/me에서 duplicateRate가 50~75% 수준이었다.
이는 단순 성능 문제가 아니라 설계 레벨의 캐시 공유 실패를 의미했다.
따라서 TanStack Query 통합이 우선순위로 도출되었다.
- UX Research (사용자 행동 패턴 분석)
- SPA Navigation Patterns
- 브라우저 Back/Forward Cache 전략
사용자는 다음 행동을 반복한다.
- 리스트 → 상세 → 뒤로 가기
- 탭 이동 → 복귀
이 패턴은 대부분 10~30초 이내에 발생한다.
CDN에서는 TTL을 트래픽 패턴 기반으로 설정한다. 여기서는 이를 프론트엔드 세션 단위로 적용하였다.
30초 내 재호출 비율이 높다는 것은,
사용자 경험 관점에서 불필요한 네트워크 왕복이 반복되고 있다는 의미
이는 staleTime 조정으로 해결 가능한 영역이다.
- Google SRE Book
- Service Level Objectives (SLO)
- Web Vitals 철학
평균 지연 시간(avgMs)은 전체 비용을 설명한다. 그러나 사용자 경험은 평균이 아니라 tail latency가 결정한다.
Google SRE에서도 다음을 강조한다.
- p50은 “보통”
- p95/p99는 “체감 품질”
p95는 다음을 의미한다.
100번 중 5번은 이만큼 느리다.
그 5번이 사용자 이탈을 만든다.
따라서 p95Ms는 캐싱의 UX ROI를 판단하는 핵심 지표로 사용하였다.
experts/recommendations의 p95는 약 1856ms였다.
이는 단순 최적화가 아니라 UX 구조를 위협하는 수준이었다.
- SRE Error Budget
- Circuit Breaker 패턴
- Resilience Engineering
에러율이 높아지는 순간, 캐싱은 성능 개선이 아니라 안정성 수단이 된다.
예를 들어:
- errorRate = 50%
- stale cache 반환 가능
→ 사용자는 실패를 거의 경험하지 않게 된다.
이는 CDN의 stale-if-error 전략과 동일한 개념이다.
experts/recommendations는 errorRate가 75%에 달했다.
이는 단순 지연 문제가 아니라 홈 페이지 전체 붕괴 가능성을 의미했다.
따라서 이 API는 TanStack Query가 아닌 BFF 레벨 캐싱 전략으로 대응하였다.
앞서 정의한 지표들은 단순한 관찰 도구가 아니다. 의사결정을 위한 계산 도구다.
캐싱을 통해 줄일 수 있는 요청 수는 다음과 같이 계산할 수 있다.
savedCalls = calls × duplicateRate
예를 들어:
- 세션 내 5회 호출
- duplicateRate = 80%
이라면,
savedCalls = 5 × 0.8 = 4
즉, 4회는 구조적으로 줄일 수 있었던 요청이다.
이 수치는 단순한 네트워크 절감 수치가 아니다. 캐싱의 기본 ROI가 된다.
사용자 경험 개선 효과는 다음과 같이 계산할 수 있다.
savedWorstTime = savedCalls × p95Ms
여기서 평균(avgMs)이 아니라 p95를 사용하는 이유는 명확하다.
사용자는 평균 응답 시간을 체감하지 않는다. 그러나 다음과 같은 경험은 명확히 기억한다.
“가끔 2초 이상 멈춘다.”
Google SRE에서도 강조하듯이, 사용자 경험은 평균이 아니라 tail latency가 결정한다.
따라서 캐싱의 UX ROI는 평균이 아니라 p95 기준 절감 시간으로 판단하였다.
일부 API는 단순히 느린 것이 아니라, 실패율이 높았다.
이 지점에서 캐싱은 성능 개선의 문제가 아니라 시스템 방어 전략의 문제로 전환된다.
보호 가능한 호출 수는 다음과 같이 계산할 수 있다.
protectedCalls = calls × errorRate
예:
- calls = 8
- errorRate = 50%
이라면,
4회는 실패 가능성이 있는 요청이다.
이 요청을 캐시로 흡수할 수 있다면, 상위 페이지 전체의 렌더링 실패를 방지할 수 있다.
이 시점에서 캐싱은 단순 최적화가 아니라 아키텍처 안정성 설계의 영역으로 이동한다.
위 계산을 종합하여 단순하지만 실용적인 점수 모델을 정의하였다.
ROI Score = calls × duplicateRate × p95Ms
해석은 간단하다.
- 호출이 많고
- 중복이 많으며
- 응답이 느릴수록
캐싱의 가치가 높다.
그러나 모든 데이터가 동일한 리스크를 가지는 것은 아니다.
참조 데이터와 실시간 데이터는 다르다. 따라서 최신성 리스크를 보정 계수로 반영하였다.
ROI Score = (calls × duplicateRate × p95Ms) / freshnessRisk
| 데이터 유형 | freshnessRisk |
|---|---|
| 참조 데이터 | 1 |
| 유저 프로필 | 2 |
| 리스트 상태 | 3 |
| 실시간 데이터 | 5 |
이 모델은 절대값을 의미하지 않는다. 우선순위 비교 도구이다.
| 항목 | resumes | users/me | experts/recommendations |
|---|---|---|---|
| duplicateRate | 75% | 50% | 75% |
| 30초 내 재호출 | 100% | 100% | - |
| p95 | 낮음 | 중간 | 1856ms |
| errorRate | 낮음 | 낮음 | 75% |
| 호출 위치 | CSR | CSR | SSR 포함 |
| 문제 유형 | 구조적 중복 | 구조적 중복 | 성능 + 장애 전파 |
| 해결 레이어 | TanStack Query | TanStack Query | BFF 캐싱 |
| 설계 목적 | 중복 제거 | 재요청 억제 | 홈 붕괴 방지 |
- duplicateRate가 높음
- 30초 내 재호출 비율 높음
- CSR 영역에 한정
→ 구조적 중복 문제
해결:
- TanStack Query queryKey 통합
- staleTime 조정
- duplicateRate 높음
- p95 = 1856ms
- errorRate = 75%
- SSR 경로 포함
이는 단순 캐싱 문제가 아니었다.
홈 전체가 실패할 수 있는 구조적 위험
따라서 선택한 전략은:
- BFF 레벨 TTL 캐시
- stale-if-error 전략
이것은 성능 최적화가 아니라 홈 붕괴 방지 설계였다.
이번 작업을 통해 얻은 가장 큰 교훈은 단순하다.
캐싱은 “할 수 있느냐”의 문제가 아니라 “해야 할 근거가 충분하냐”의 문제다.
이번 작업을 통해 깨달은 점은 단순하다.
그동안 우리는 “느린 것 같다”, “자주 쓰는 것 같다”는 감각으로 캐싱을 결정해왔다. 그러나 실제로는 호출 수, 중복률, p95, 에러율을 확인하는 것만으로도 어디를 손대야 할지가 훨씬 명확해졌다.
측정 환경을 갖추는 순간, 최적화는 감이 아니라 판단이 된다.