Post

Cache Strategies

Cache Strategies

Read Strategies

Cache Aside

Cache Aside1 이미지

  • 동작 방식: 캐시와 DB 사이에 별도의 중개자가 없고, 애플리케이션이 캐시와 DB를 둘 다 직접 다룬다.
    1. 애플리케이션이 먼저 캐시를 조회한다.
    2. 캐시에 없으면(cache miss)
    3. 애플리케이션이 직접 DB를 조회하고
    4. 데이터를 받아서
    5. 애플리케이션이 직접 그 결과를 캐시에 채워 넣는다(update cache).
      • 캐시는 그저 “키-값 저장소”일 뿐이고, “미스가 나면 어디서 채울지”에 대한 로직은 전부 애플리케이션 코드에 있다. Redis/Memcached 앞단에 GET 후 없으면 SET하는 흔한 패턴이 바로 이것이다.
  • 이런 상황에서 쓰면 좋다:
    • 캐시 서버(Redis, Memcached)가 DB 조회 로직을 알 필요 없이 범용 key-value 스토리지로만 존재해도 되는 대부분의 웹 서비스. 구현이 단순하고 별도 미들웨어 없이 애플리케이션 코드만으로 완결된다.
    • 캐시할 데이터의 조회 로직이 상황마다 다르거나(여러 테이블 조인, 조건별 분기) 캐싱 여부를 세밀하게 제어하고 싶을 때 — 캐시 계층이 몰라도 되므로 애플리케이션이 자유롭게 결정할 수 있다.
    • 캐시 서버가 잠깐 죽어도 서비스 자체는 (느려지더라도) DB로 직접 폴백해 계속 동작해야 하는 경우.
  • 주의할 점:
    • 캐시 미스가 나면 요청마다 “캐시 조회 → DB 조회 → 캐시 갱신”이라는 왕복 지연이 그대로 발생한다. 첫 요청뿐 아니라 TTL 만료 직후에도 매번 이 지연을 감수해야 한다.
    • 같은 키에 대해 캐시가 동시에 만료된 상태에서 요청이 몰리면, 다수의 요청이 동시에 캐시 미스를 겪고 전부 DB로 몰려가는 캐시 스탬피드(cache stampede, thundering herd) 가 발생할 수 있다 — 분산 락이나 짧은 시간차를 둔 TTL(jitter)로 완화한다.
    • 데이터를 쓸 때(write) 캐시를 누가, 언제 무효화(invalidate)할지는 이 패턴에 포함되어 있지 않다. DB 업데이트 후 캐시를 지우는 로직을 애플리케이션이 빠뜨리면 캐시와 DB 사이에 정합성이 어긋난 채로 남는다.

Read Through

Read Through 이미지

  • 동작 방식: 애플리케이션은 오직 캐시하고만 대화하고, DB 조회는 캐시(캐시 라이브러리/프로바이더)가 대신 처리한다.
    1. 애플리케이션이 캐시에 read 요청을 보낸다.
    2. 캐시에 없으면(cache miss)
    3. 캐시가 직접 DB를 조회하고
    4. 데이터를 받아서
    5. 캐시가 스스로 자신을 갱신한 뒤 애플리케이션에 응답한다.
      • Cache Aside와 그림은 비슷해 보이지만 화살표의 주체가 다르다 — DB 조회와 캐시 갱신을 애플리케이션이 아니라 캐시 계층이 캡슐화하고 있다. 애플리케이션 입장에서는 캐시가 DB처럼 보이는 하나의 조회 인터페이스가 된다.
  • 이런 상황에서 쓰면 좋다:
    • 여러 애플리케이션(혹은 여러 서비스)이 동일한 데이터를 캐싱해야 해서, “미스 시 DB에서 채우는 로직”을 각 애플리케이션마다 중복 구현하고 싶지 않을 때 — 로직을 캐시 계층 한 곳에 모아두면 일관성을 유지하기 쉽다.
    • Read/Write 패턴이 대칭적으로 필요한 경우, 즉 Write Through와 짝을 지어 “캐시가 곧 데이터 접근의 단일 창구”가 되는 아키텍처를 만들고 싶을 때(예: Ehcache, Guava LoadingCache, 일부 CDN/캐시 프레임워크의 loader 패턴).
    • 캐시 무효화/채우기 로직을 인프라·라이브러리 단으로 표준화해 애플리케이션 코드를 단순하게 유지하고 싶은 팀.
  • 주의할 점:
    • 이 패턴을 쓰려면 캐시가 DB 조회 방법(쿼리, 커넥션)을 알고 있어야 한다 — Redis/Memcached 자체는 이런 기능이 없으므로, 보통 애플리케이션 프레임워크나 별도 라이브러리가 “캐시 미스 시 실행할 로더(loader) 함수”를 등록하는 방식으로 흉내 낸다. 즉 완전한 캐시 서버 기능이 아니라 캐시를 감싼 추상화 계층이 필요하다.
    • Cache Aside와 마찬가지로 첫 조회 시 지연과 캐시 스탬피드 문제는 그대로 남는다. 캐시가 DB 조회를 대신해준다고 해서 미스 자체가 사라지는 것은 아니다.
    • DB에 있는 모든 데이터가 아니라 실제로 조회된 데이터만 캐시에 채워지는 lazy-loading 특성은 Cache Aside와 동일하므로, 최초 워밍업이 안 된 상태의 콜드 캐시 구간에서는 여전히 느리다.
    • 캐시 계층이 DB 접근 로직까지 갖고 있는 만큼 결합도가 높아진다 — DB 스키마나 조회 조건이 바뀌면 애플리케이션 코드가 아니라 캐시 계층(로더)을 수정해야 하므로, 변경 지점이 어디인지 팀 내에서 명확히 공유되어야 한다.

Write Strategies

Write Around

Write Around 이미지

  • 동작 방식: 쓰기는 캐시를 거치지 않고 DB로 직행하고, 캐시는 오직 읽기 미스가 났을 때만 채워진다.
    1. 애플리케이션이 DB에 바로 write한다 — 캐시는 건드리지 않는다.
    2. 이후 읽기 요청이 오면 먼저 캐시를 조회하고(read from cache, if exists)
    3. 캐시에 없으면(방금 쓴 데이터라 아직 캐시에 없는 경우 포함) DB에서 읽어오고
    4. 그 결과로 캐시를 채운다(update cache).
      • 즉 쓰기 경로와 읽기 경로가 완전히 분리되어 있다. 읽기 쪽만 보면 Cache Aside와 동일한 lazy-loading이고, Write Around는 여기에 “쓰기는 절대 캐시를 갱신하지 않는다”는 규칙을 더한 것이다.
  • 이런 상황에서 쓰면 좋다:
    • 한 번 쓰고 나서 곧바로 다시 읽히지 않는 데이터: 로그 적재, 대량 배치 삽입처럼 write 직후 read가 거의 발생하지 않는 워크로드. 어차피 안 읽힐 데이터를 캐시에 미리 채워봐야 공간만 차지하고 히트율에 기여하지 못한다.
    • 쓰기가 매우 빈번한데 읽기 대비 캐시 히트 가치가 낮은 경우: 매 write마다 캐시까지 갱신하는 비용을 아낄 수 있어, Write Through 대비 쓰기 처리량을 확보하기 좋다.
  • 주의할 점:
    • 데이터를 쓴 직후 곧바로 조회하면(write-then-read) 캐시에는 아직 옛날 값(또는 값 없음)만 있어서 반드시 캐시 미스가 나고 DB까지 가게 된다 — 쓰기 직후 읽기가 잦은 워크로드에는 오히려 불리하다.
    • 캐시가 최신 상태를 보장하지 않으므로, 쓰기와 읽기 사이에 존재하는 이 “캐시가 stale한 구간”을 서비스가 감수할 수 있는지 먼저 따져봐야 한다.

Write Back

Write Back 이미지

  • 동작 방식: 애플리케이션은 캐시에만 쓰고(write constantly), DB 반영은 캐시가 나중에, 주기적으로 몰아서 처리한다(write to DB once in a while).
    1. 애플리케이션은 매 쓰기마다 캐시에 즉시 기록한다. DB는 이 시점에 전혀 관여하지 않는다.
    2. 캐시가 일정 주기(또는 배치 크기, 특정 트리거)마다 쌓인 변경분을 모아 DB에 한 번에 반영한다.
      • “Write Behind”라고도 부른다. 애플리케이션이 체감하는 쓰기 지연은 캐시(주로 메모리) 쓰기 시간뿐이라 매우 빠르다.
  • 이런 상황에서 쓰면 좋다:
    • 쓰기가 극도로 빈번하고 지연에 민감한 워크로드: 좋아요 수, 조회수 카운터처럼 매 이벤트마다 DB에 UPDATE를 날리면 락 경합·쓰기 처리량 한계에 부딪히는 경우, 캐시에서 우선 누적시키고 DB에는 배치로 반영한다.
    • 여러 번의 쓰기를 하나로 합쳐도(coalescing) 무방한 데이터: 짧은 시간 안에 같은 키가 여러 번 갱신되면 마지막 값만 DB에 반영해도 되는 경우, 중간 값들에 대한 개별 DB write를 생략해 쓰기 자체를 줄일 수 있다.
  • 주의할 점:
    • DB에 아직 반영되지 않은 변경분이 캐시에만 존재하는 구간이 항상 있다 — 이 상태에서 캐시가 장애로 죽으면 그 사이의 쓰기가 전부 유실된다. 세 가지 전략 중 데이터 유실 위험이 가장 크다.
    • DB를 캐시와 별도로 직접 조회하는 다른 클라이언트/배치 작업이 있다면, 그 클라이언트는 아직 반영 안 된 stale한 데이터를 보게 된다 — 캐시가 유일한 진입점이라는 전제가 깨지면 정합성이 어긋난다.
    • 유실 위험을 줄이려면 캐시 자체를 이중화하거나(Redis AOF/RDB, 리플리케이션), flush 주기를 촘촘히 가져가는 트레이드오프가 필요하다 — 주기를 촘촘히 할수록 유실 위험은 줄지만 DB 부하 절감 효과는 약해진다.

Write Through

Write Through 이미지

  • 동작 방식: 애플리케이션은 캐시에만 쓰고, 캐시가 그 자리에서 즉시 동기적으로 DB에도 반영한다.
    1. 애플리케이션이 캐시에 write한다.
    2. 캐시가 즉시(immediately) 같은 트랜잭션 흐름 안에서 DB에도 write한다 — 이 write가 끝나야 애플리케이션에 응답이 돌아온다.
      • Write Back과 그림이 비슷해 보이지만 핵심 차이는 “즉시(synchronous)”냐 “나중에 몰아서(asynchronous, batched)”냐다. Write Through는 캐시와 DB가 항상 같은 값을 가진다는 것이 보장되고, Write Back은 그 보장이 없는 대신 더 빠르다.
  • 이런 상황에서 쓰면 좋다:
    • 캐시와 DB의 정합성이 무엇보다 중요한 데이터: 결제 상태, 재고 수량처럼 캐시에서 읽은 값이 절대 stale하면 안 되는 경우. 쓰기가 끝난 순간 캐시와 DB가 항상 같은 값을 보장하므로 별도의 무효화 로직이 필요 없다.
    • 이후의 읽기가 항상 최신 값을 캐시에서 바로 가져갈 수 있어야 하는 read-heavy 워크로드 — 쓰기 시점에 미리 캐시를 데워두는 효과도 있어 캐시 미스 자체가 잘 발생하지 않는다.
  • 주의할 점:
    • 매 쓰기마다 DB write를 동기로 기다려야 하므로, 캐시만 쓰는 Write Back보다 쓰기 지연이 크다 — 결국 병목이 DB 쓰기 속도로 수렴한다.
    • 한 번도 읽히지 않을 데이터까지 쓰는 시점에 무조건 캐시에 채워 넣으므로, 읽기 히트율에 기여하지 않는 콜드 데이터로 캐시 공간을 낭비할 수 있다 — 이 경우 Write Around가 더 적합할 수 있다.
    • 캐시 쓰기는 성공했는데 DB 쓰기가 실패하는(혹은 그 반대) 부분 실패 상황을 어떻게 처리할지 — 두 저장소에 걸친 쓰기이므로 원자성을 어떻게 보장할지 설계가 필요하다.
This post is licensed under CC BY 4.0 by the author.

Trending Tags