서비스가 느려졌을 때 데이터베이스만 증설하면 문제가 해결될까요? 읽기 요청이 폭증하는 쇼핑몰 상품 상세 페이지나 인기 게시글 목록에서는 DB 성능을 높이는 것보다, 반복해서 읽는 데이터를 더 가까운 곳에 두는 편이 훨씬 효과적일 수 있습니다. 이때 필요한 설계가 바로 캐싱 구조입니다. 캐시는 원본 저장소보다 빠르게 접근 가능한 저장 계층이며, 자주 쓰는 데이터의 일부를 임시로 보관해 재사용합니다.
다만 캐시는 마법 같은 가속 장치가 아닙니다. 읽기 비중이 높고 같은 데이터가 반복 조회될 때 효과가 크지만, 자주 바뀌는 데이터나 개인화 응답에는 오히려 위험이 생길 수 있습니다. 캐싱 구조를 제대로 이해하려면 캐시 히트, 캐시 미스, TTL, 무효화, 교체 정책, 계층별 캐시의 역할을 함께 봐야 합니다.
캐싱 구조란 무엇인가
캐싱 구조의 핵심은 원본 데이터 저장소 앞에 더 빠른 저장 계층을 두고, 반복 접근되는 데이터를 재사용하는 것입니다. 예를 들어 상품 상세 API가 매번 DB에서 상품명, 가격, 대표 이미지, 리뷰 수를 읽는다면 요청이 많아질수록 DB 부하가 커집니다. 반대로 같은 응답을 짧은 시간 동안 캐시에 저장하면 다음 요청은 DB까지 가지 않고 캐시에서 응답할 수 있습니다.
원본 저장소는 데이터의 정합성과 영속성을 책임집니다. DB, 외부 API, 파일 저장소, 복잡한 계산 로직이 여기에 해당합니다. 캐시 계층은 원본보다 빠른 응답을 제공하지만, 보통 제한된 용량과 제한된 유효 시간을 갖습니다. 따라서 캐시는 원본을 대체하는 저장소가 아니라, 느린 경로 앞에서 요청을 흡수하는 고속 완충 계층으로 보는 것이 안전합니다.
캐싱이 성능을 높이는 원리는 단순합니다. 네트워크 왕복, 디스크 접근, 조인 쿼리, 외부 API 호출처럼 비용이 큰 작업을 반복하지 않게 만드는 것입니다. 특히 메모리 기반 캐시는 디스크 기반 저장소보다 접근 시간이 짧기 때문에 응답 지연과 원본 저장소 부하를 함께 줄일 가능성이 큽니다.
캐싱 구조를 이해하는 핵심 용어
캐시 히트는 요청한 데이터가 캐시에 있어 바로 응답할 수 있는 상태입니다. 캐시 미스는 캐시에 데이터가 없어 원본 저장소나 계산 로직을 다시 거쳐야 하는 상태입니다. 캐시 히트율이 높을수록 원본 저장소 접근이 줄어들고 평균 응답 시간이 낮아질 가능성이 큽니다. 그래서 실무에서는 단순히 캐시를 넣었는지가 아니라, 어떤 키에서 얼마나 히트가 나는지를 측정해야 합니다.
TTL은 캐시 항목의 유효 시간을 정하는 방식입니다. 예를 들어 인기 게시글 목록을 60초 동안 캐시하면 60초가 지나기 전까지는 같은 결과를 재사용합니다. TTL이 너무 짧으면 캐시 미스가 늘고, 너무 길면 오래된 데이터가 사용자에게 보일 수 있습니다.
캐시 무효화는 데이터가 더 이상 유효하지 않을 때 캐시에서 제거하거나 갱신하는 과정입니다. TTL이 시간 기반 만료라면, 무효화는 데이터 변경 이벤트나 운영 명령에 의해 더 적극적으로 캐시를 비우는 전략입니다. 상품 가격이 변경되었는데 기존 가격 캐시가 남아 있다면 큰 문제가 될 수 있으므로, 쓰기 작업과 무효화 흐름은 반드시 함께 설계해야 합니다.
캐시 교체 정책은 메모리 제한이 있을 때 어떤 데이터를 제거할지 정하는 규칙입니다. 최근에 덜 사용한 데이터를 지우는 LRU, 덜 자주 사용한 데이터를 지우는 LFU, 임의 제거, 먼저 들어온 데이터를 먼저 지우는 FIFO, TTL이 가까운 항목을 우선 제거하는 방식 등이 있습니다.
대표적인 캐시 계층 구조
캐시는 한 지점에만 존재하지 않습니다. 브라우저, CDN, 애플리케이션 서버, 분산 캐시, 데이터베이스 내부까지 여러 계층에 놓일 수 있습니다. 좋은 캐싱 구조는 어느 계층에서 어떤 데이터를 맡길지 결정하는 과정입니다.
브라우저 캐시는 이미지, CSS, JavaScript, 일부 API 응답처럼 사용자의 기기에 저장할 수 있는 데이터를 재사용합니다. HTTP 캐싱에서는 Cache-Control, max-age, ETag, Last-Modified, Vary 같은 헤더가 중요합니다. 특히 no-cache는 저장하지 말라는 뜻이 아니라 재사용 전에 재검증하라는 의미이며, 저장 자체를 막고 싶다면 no-store를 사용해야 합니다. 자세한 HTTP 캐시 동작은 MDN의 HTTP caching 문서가 유용합니다.
CDN과 엣지 캐시는 사용자와 가까운 네트워크 지점에서 정적 리소스나 공개 응답을 제공합니다. 글로벌 서비스에서 이미지, 동영상 썸네일, 공개 상품 목록을 캐싱하면 원본 서버까지 가는 요청을 크게 줄일 수 있습니다. 단, 개인화 응답이나 민감한 정보는 공유 캐시에 저장되지 않도록 private, no-store 같은 정책을 신중하게 적용해야 합니다.
애플리케이션 로컬 캐시는 각 서버 인스턴스 메모리에 데이터를 보관합니다. 네트워크 호출이 없어 빠르지만, 인스턴스별로 서로 다른 사본을 가질 수 있어 일관성 문제가 생깁니다. 반면 분산 캐시는 Redis나 Memcached 같은 별도 캐시 클러스터를 여러 애플리케이션 인스턴스가 공유하는 방식입니다. 공유성과 무효화 관리에는 유리하지만 네트워크 비용, 장애 대응, 운영 복잡도가 추가됩니다.
데이터베이스 캐시는 DB 엔진 내부의 버퍼 풀, 쿼리 실행 계획 캐시, 페이지 캐시처럼 저장소 가까이에 있는 최적화 계층입니다. 애플리케이션 캐시와 달리 개발자가 직접 키를 설계하지 않는 경우가 많지만, 인덱스와 쿼리 패턴에 따라 효과가 크게 달라집니다.
캐싱 구조의 일반적인 동작 흐름
읽기 요청의 기본 흐름은 캐시 조회에서 시작합니다. 데이터가 있으면 캐시 히트로 응답하고, 없으면 캐시 미스로 원본 저장소를 조회합니다. 이후 조회 결과를 캐시에 저장하고 사용자에게 응답합니다. 이 구조는 API 응답 캐싱, 인기 게시글 조회, 상품 상세 페이지에서 자주 사용됩니다.
쓰기 요청은 더 조심해야 합니다. 일반적으로 원본 저장소를 먼저 갱신한 뒤 관련 캐시를 삭제하거나 새 값으로 갱신합니다. 순서가 뒤섞이면 사용자는 이미 변경된 데이터 대신 오래된 캐시를 볼 수 있습니다. 특히 결제 상태, 재고 수량, 권한 정보처럼 정확성이 중요한 데이터는 단순 TTL에만 의존하기 어렵습니다.
실무에서 널리 쓰이는 Cache-Aside 패턴은 애플리케이션이 캐시 확인, 미스 시 원본 조회, 캐시 저장, 응답 흐름을 직접 담당하는 방식입니다. 쓰기에서는 원본 저장소를 갱신하고 캐시 항목을 무효화하는 형태가 흔합니다. 이 패턴은 단순하고 유연하지만 항상 강한 일관성을 보장하지는 않는다는 점을 이해해야 합니다. 흐름을 더 깊게 보려면 Microsoft Azure의 Cache-Aside pattern 설명을 참고할 수 있습니다.
캐싱 전략별 장단점
Cache-Aside는 애플리케이션이 캐시를 명시적으로 제어하므로 도입이 쉽고 데이터별 정책을 다르게 적용하기 좋습니다. 대신 코드 곳곳에 캐시 처리 로직이 퍼질 수 있고, 미스 순간에는 원본 저장소 부하가 발생합니다.
Read-Through는 애플리케이션이 캐시만 조회하고, 캐시 계층이 원본 로딩을 담당하는 방식입니다. 호출 코드는 단순해지지만 캐시 인프라가 원본 저장소 접근까지 알아야 합니다. Write-Through는 쓰기 시 캐시와 원본을 함께 갱신해 읽기 일관성을 높이지만 쓰기 지연이 늘 수 있습니다.
Write-Behind는 캐시에 먼저 쓰고 나중에 원본 저장소에 반영합니다. 쓰기 성능에는 유리하지만, 캐시 장애나 비동기 반영 실패 시 데이터 손실 위험이 있으므로 감사 로그와 재시도 설계가 필요합니다. Pre-warming은 배포 직후나 이벤트 시작 전에 인기 데이터를 미리 캐시에 채워 넣어 초기 캐시 미스 폭증을 줄이는 방식입니다.
캐시 무효화와 데이터 일관성 문제
오래된 데이터는 원본 데이터가 바뀌었는데 캐시가 그대로 남아 있을 때 발생합니다. TTL이 지나면 언젠가 사라지겠지만, 그 시간 동안 사용자는 잘못된 가격, 변경 전 프로필, 삭제된 게시글을 볼 수 있습니다. 그래서 TTL은 기본 안전장치일 뿐, 모든 문제를 해결하는 만능 수단이 아닙니다.
이벤트 기반 무효화는 상품 수정, 게시글 삭제, 권한 변경 같은 도메인 이벤트가 발생했을 때 관련 캐시 키를 삭제하는 방식입니다. 명령 기반 무효화는 운영자나 배치 작업이 특정 키, 특정 사용자, 특정 그룹의 캐시를 명시적으로 비우는 방식입니다. 대규모 서비스에서는 키 네이밍 규칙, 태그 또는 버전 값을 함께 사용해 관련 캐시를 묶어 관리하기도 합니다.
민감한 데이터는 캐싱 여부부터 다시 검토해야 합니다. 개인정보, 인증 토큰, 사용자별 가격, 권한에 따라 달라지는 화면은 공유 캐시에 저장되면 보안 문제가 됩니다. 캐싱 구조 설계에서 성능보다 먼저 확인할 것은 이 데이터가 다른 사용자에게 노출되어도 되는가라는 질문입니다.
캐시 교체 정책과 메모리 관리
캐시는 보통 메모리 비용이 비싸고 용량이 제한됩니다. 따라서 모든 데이터를 담을 수 없으며, 어떤 항목을 남기고 어떤 항목을 제거할지 정해야 합니다. LRU는 최근에 사용되지 않은 데이터를 제거하므로 시간적 지역성이 강한 서비스에 잘 맞습니다. LFU는 사용 빈도가 낮은 데이터를 제거하므로 장기간 인기 데이터가 뚜렷한 서비스에 유리합니다.
Random은 구현이 단순하고 특정 상황에서 예측 가능한 오버헤드가 적지만, 중요한 데이터가 제거될 수 있습니다. FIFO는 먼저 들어온 항목을 먼저 제거하므로 이해하기 쉽지만 실제 접근 패턴을 반영하지 못할 수 있습니다. Redis 같은 인메모리 캐시에서는 noeviction, allkeys-lru, allkeys-lfu, allkeys-random, volatile-ttl 같은 정책을 워크로드에 맞게 선택합니다. 또한 keyspace_hits와 keyspace_misses 같은 지표로 히트율을 계산해 정책이 적절한지 확인해야 합니다.
좋은 캐싱 구조를 설계하기 위한 체크리스트
- 캐시할 데이터와 캐시하지 말아야 할 데이터를 구분합니다. 반복 조회가 많고 약간의 지연된 최신성을 허용할 수 있는 데이터가 좋은 후보입니다. 자주 바뀌거나 민감하거나 사용자별로 크게 달라지는 데이터는 신중해야 합니다.
- 히트율을 측정합니다. 캐시를 넣었는데 미스가 대부분이라면 메모리만 낭비할 수 있습니다. 전체 히트율뿐 아니라 키, API, 사용자 그룹, 시간대별 히트율을 봐야 병목을 찾을 수 있습니다.
- TTL 기준을 정합니다. 데이터 변경 빈도, 사용자에게 허용 가능한 오래된 시간, 원본 저장소 부하를 함께 고려합니다. TTL은 짧을수록 안전하고 길수록 빠르다는 식의 단순 공식으로 정하면 안 됩니다.
- 쓰기 후 캐시 처리 순서를 명확히 합니다. 원본 저장소를 먼저 갱신한 뒤 캐시를 삭제할지, 새 값으로 갱신할지 정해야 합니다. 실패 시 재시도와 보상 처리도 필요합니다.
- 장애 시 우회 경로를 준비합니다. 캐시 장애가 전체 서비스 장애로 전파되지 않도록 원본 저장소로 우회할지, 일부 기능을 제한할지, 서킷 브레이커를 둘지 결정합니다.
- 캐시 스탬피드와 핫키를 대비합니다. 하나의 인기 키가 만료되는 순간 수많은 요청이 동시에 원본으로 몰릴 수 있습니다. 락, 요청 병합, TTL 분산, 사전 예열, 핫키 분산 전략을 검토해야 합니다.
캐싱 구조를 잘못 설계했을 때 생기는 문제
가장 흔한 문제는 데이터 불일치입니다. 상품 가격, 재고, 권한 같은 데이터가 원본과 캐시에서 다르게 보이면 사용자 경험뿐 아니라 비즈니스 사고로 이어질 수 있습니다. 캐시 스탬피드는 대량 미스가 원본 저장소를 압박해 장애를 키우는 현상입니다. 핫키 병목은 특정 인기 데이터 하나에 요청이 집중되어 캐시 서버나 네트워크가 병목이 되는 상황입니다.
메모리 낭비도 무시할 수 없습니다. 히트율이 낮은 데이터를 무작정 캐싱하면 비용만 증가합니다. 개인정보 노출 위험은 더 치명적입니다. 개인화된 응답을 CDN이나 공유 캐시에 저장하면 다른 사용자에게 노출될 수 있으므로, 캐시 키에 사용자 구분이 필요한지, 아예 저장하지 말아야 하는지 검토해야 합니다.
캐싱 구조는 속도와 일관성의 균형이다
좋은 캐싱 구조는 단순히 Redis를 붙이거나 TTL을 설정하는 것으로 완성되지 않습니다. 어떤 데이터를 어느 계층에 둘지, 캐시 히트율을 어떻게 측정할지, 원본 변경 시 어떻게 무효화할지, 장애와 핫키를 어떻게 처리할지까지 포함한 설계입니다. 캐시는 읽기 성능을 높이는 강력한 도구이지만, 데이터 일관성과 보안까지 함께 관리할 때 비로소 실무에서 안전한 성능 최적화가 됩니다.