캐시 전략, 무효화와 스탬피드 대응

2026. 10. 14. 13:00·IT
728x90
반응형

캐시를 붙이면 빨라진다. 그런데 캐시를 붙이고 나면 새로운 문제가 생긴다. 데이터가 갱신됐는데 옛날 값이 보이고, 캐시가 만료되는 순간 데이터베이스가 몰려드는 요청에 쓰러진다. 이 글은 캐시를 도입할 때 반드시 결정해야 하는 무효화 전략, 만료 설계, 동시 만료 대응 세 가지를 정리한다.

읽기 캐시의 세 가지 패턴

먼저 어디에 캐시를 두고 누가 채우는지를 정해야 한다.

Cache-Aside(Lazy Loading)가 가장 흔하다. 애플리케이션이 캐시를 먼저 보고, 없으면 데이터베이스에서 읽어 캐시에 넣는다.

def get_user(uid):
    v = cache.get(f"user:{uid}")
    if v is not None:
        return v
    v = db.query_user(uid)
    cache.set(f"user:{uid}", v, ttl=300)
    return v

장점은 단순하고, 실제로 요청된 것만 캐시된다는 것이다. 단점은 첫 요청이 항상 느리다는 것과, 캐시와 데이터베이스가 어긋날 수 있다는 것이다.

Write-Through는 쓸 때 캐시와 데이터베이스를 함께 갱신한다. 일관성이 좋지만 쓰기가 느려지고, 읽히지 않을 데이터까지 캐시에 쌓인다.

Write-Behind는 캐시에 쓰고 나중에 비동기로 데이터베이스에 반영한다. 가장 빠르지만 장애 시 데이터가 날아갈 수 있다. 유실을 감당할 수 있는 데이터에만 쓴다.

대부분의 웹 서비스에서는 Cache-Aside로 시작하는 것이 맞다. 나머지는 특정 문제가 생겼을 때 고려한다.

무효화는 삭제가 갱신보다 안전하다

데이터가 바뀌었을 때 캐시를 어떻게 할 것인가. 두 방법이 있다.

# 방법 1: 갱신
db.update_user(uid, data)
cache.set(f"user:{uid}", data, ttl=300)

# 방법 2: 삭제
db.update_user(uid, data)
cache.delete(f"user:{uid}")

삭제가 더 안전하다. 이유는 동시성이다. 두 요청이 거의 동시에 갱신하면, 캐시에 쓰는 순서가 데이터베이스에 쓰는 순서와 뒤바뀔 수 있다. 그러면 캐시에 오래된 값이 영구히 남는다. 삭제 방식은 다음 읽기에서 데이터베이스를 다시 읽으므로 이 문제가 없다.

순서도 중요하다. 데이터베이스를 먼저 쓰고 캐시를 지운다. 반대로 하면, 캐시를 지운 직후 데이터베이스 쓰기 전에 다른 요청이 들어와 옛 값을 다시 캐시에 넣을 수 있다.

그래도 완벽하지는 않다. 캐시 삭제가 실패하면 낡은 값이 남는다. 그래서 TTL은 항상 함께 둔다. 무효화가 실패해도 TTL이 지나면 자동으로 사라진다. TTL은 최후의 안전망이다.

동시 만료가 데이터베이스를 무너뜨린다

인기 있는 키의 캐시가 만료되는 순간, 그 키를 찾던 요청 수천 개가 동시에 데이터베이스로 간다. 캐시 스탬피드(cache stampede) 또는 dogpile 문제다.

더 나쁜 경우는 여러 키가 같은 시각에 만료되는 것이다. 배포 시점에 캐시를 한꺼번에 채웠다면 TTL도 같이 끝난다.

대응은 세 가지다.

TTL에 무작위 값을 더한다. 300초 대신 300초에서 360초 사이로 흩어 놓으면 동시 만료가 사라진다. 가장 쉽고 효과가 크다.

ttl = 300 + random.randint(0, 60)

한 요청만 갱신하게 한다. 캐시 미스가 났을 때 분산 락을 잡고, 락을 못 잡은 요청은 잠시 기다렸다 다시 캐시를 본다. 데이터베이스로 가는 것은 한 요청뿐이다.

만료 전에 미리 갱신한다. TTL이 끝나기 전에 백그라운드로 새 값을 채운다. 사용자 요청은 항상 캐시 적중이 된다. 구현이 복잡하지만 효과가 확실하다.

무엇을 캐시하지 말아야 하는가

캐시를 붙이기 전에 확인할 것이 있다. 모든 것을 캐시할 필요는 없다.

자주 바뀌는 데이터는 캐시 효율이 낮다. 적중률이 낮으면 캐시 조회 비용만 추가된다. 사용자마다 다른 데이터도 마찬가지다. 사용자 백만 명의 개인화 데이터를 캐시하면 메모리는 쓰는데 적중률은 바닥이다.

정확성이 중요한 데이터는 신중해야 한다. 잔액, 재고 수량, 권한 정보는 낡은 값이 보이면 사고로 이어진다. 재고가 0인데 캐시에 10으로 남아 있으면 초과 판매가 된다. 이런 데이터는 캐시하지 않거나, TTL을 아주 짧게 잡고 결제 직전에는 반드시 원본을 확인한다.

반대로 캐시가 잘 맞는 것은 읽기가 압도적으로 많고, 잠깐 낡아도 괜찮은 데이터다. 상품 상세, 카테고리 목록, 설정값, 집계 통계 같은 것들이다.

먼저 쿼리를 고치는 것이 순서다

마지막으로 순서를 짚고 싶다. 느린 API에 캐시를 붙이는 것은 종종 문제를 덮는 일이 된다.

느린 원인이 인덱스를 안 타는 쿼리거나 N+1이라면, 그걸 고치는 것이 먼저다. 캐시를 붙이면 당장은 빨라지지만, 캐시가 안 듣는 경로(새 데이터, 캐시 장애, 배포 직후)에서 원래의 느림이 그대로 드러난다. 그때는 트래픽이 몰려 있으므로 더 나쁜 상황이 된다.

캐시는 이미 최적화된 쿼리를 더 빠르게 만드는 도구다. 최적화되지 않은 쿼리를 감추는 도구가 아니다. 이 순서를 지키면 캐시 장애가 서비스 장애로 번지지 않는다.

참고 자료

  • Redis: Client-side caching — 캐시 무효화 방식과 일관성 고려사항
  • AWS: Caching best practices — Cache-Aside, Write-Through 등 패턴 비교
  • PostgreSQL: Using EXPLAIN — 캐시 전에 쿼리부터 확인할 때 쓰는 도구
728x90
반응형

'IT' 카테고리의 다른 글

구조화 로깅, 장애 때 찾을 수 있는 로그 남기기  (0) 2026.10.15
N+1 쿼리 문제, 발견하고 고치는 법  (0) 2026.10.13
인덱스를 만들었는데 쿼리가 느린 이유  (0) 2026.10.12
AI 코딩 에이전트 시대의 코드 리뷰  (0) 2026.10.11
Git 브랜치 전략, rebase와 merge 중 무엇을 쓸까  (0) 2026.10.10
'IT' 카테고리의 다른 글
  • 구조화 로깅, 장애 때 찾을 수 있는 로그 남기기
  • N+1 쿼리 문제, 발견하고 고치는 법
  • 인덱스를 만들었는데 쿼리가 느린 이유
  • AI 코딩 에이전트 시대의 코드 리뷰
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (451) N
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (54) N
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    rust linux
    jsp include
    nginx
    Java Array
    lang rust
    에디터 팁
    spring java
    css tb
    자바 for문
    자바 객체 지향
    vscode
    docker
    proxy pass
    자바 배열
    자바 exception
    jsp parameter
    nginx ssl 적용
    nginx ssl 설정
    프런트엔드
    AI 반도체
    css table
    자바 생성자
    React Compiler
    클론코딩
    오류
    css float
    Rust lang
    css perspective
    vue cli
    ssl 인증서 발급
    mysql db
    Node
    리눅스 설치
    AI 코딩 에이전트
    svelte
    브라우저 자동화
    Extension Bisect
    제미나이
    ubuntu
    css transition
    servlet class
    VS Code 팁
    vue 설치
    러스트
    SSL 인증서
    API 설계
    gitlab 설치
    티스토리 자동화
    자바 클래스
    java casting
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
캐시 전략, 무효화와 스탬피드 대응
상단으로

티스토리툴바