Redis ‐ Redis 캐싱 전략 - thought-corner/backend-roadmap GitHub Wiki

캐시(Cache), 캐싱(Caching)이란?

  • 캐시(Cache) : 원본 저장소보다 빠르게 가져올 수 있는 임시 데이터 저장소를 의미한다.
  • 캐싱(Caching) : 캐시에 접근해서 데이터를 빠르게 가져오는 방식을 의미한다.

Cache Aside Strategy(Look-Aside)

1단계 : 캐시 확인(GET key)

  • 애플리케이션은 필요한 데이터가 Redis(캐시)에 있는지 먼저 확인한다.
  • Cache Hit : 데이터가 있다면 DB를 호출하지 않고 즉시 반환한다.
  • Cache Miss : 데이터가 없다면 다음 프로세스로 넘어간다.

2단계 : 데이터베이스 조회(SELECT *)

  • 캐시에 데이터가 없으므로 애플리케이션이 직접 DB에 쿼리를 날려 데이터를 가져온다.

3단계 : 캐시 저장(SET key value)

  • DB에서 가져온 데이터를 그냥 반환하고 끝내는 것이 아니라 Redis에 저장을 한다.
  • 이제 해당 데이터를 '캐싱'된 상태라고 할 수 있다.

4단계 : 데이터 반환 및 이후 요청

  • 애플리케이션이 사용자에게 최종 데이터를 전달한다.
  • Redis에 데이터가 적재된 상태기 때문에 매우 빠른 응답이 가능해진다.

Cache Aside Strategy Trade Off

  • 장점
    • 직관적이고 단순하다 : 구현이 쉽고 대부분의 서비스에 적합하다.
    • 장애 대응성 : Redis에 문제가 생겨도 애플리케이션이 DB로 직접 가면 되므로 서비스가 완전히 죽지 않는다.
    • 자원 효율성 : 실제로 자주 조회되는 데이터만 캐시에 올라가므로 메모리를 효율적으로 사용한다.
  • 단점
    • 데이터 정합성(Data Consistency) : DB 원본이 수정되었는데 캐시가 그대로 남아있다면 사용자는 과거의 데이터를 보게 된다. DB 데이터를 업데이트할 때마다 관련 캐시를 삭제하거나 TTL을 짧게 설정한다.
      • 이 때 순서는 "DB 갱신 → 캐시 삭제"가 기본이다. 반대로 캐시를 먼저 지우면, 삭제와 DB 갱신 사이에 들어온 조회가 옛 데이터를 다시 캐시에 채워 넣는 문제가 생길 수 있다.
    • 캐시 쇄도(Cache Stampede / Thundering Herd) : 캐시가 만료되는 순간 다수의 요청이 동시에 "캐시 미스"를 겪으며 DB에 몰릴 수 있다.

Write Through Strategy

1단계 : 캐시에 먼저 기록(SET key value)

  • 애플리케이션이 데이터를 저장하거나 수정할 때, 가장 먼저 Redis에 데이터를 쓴다.

2단계 : DB에 즉시 동기화(INSERT/UPDATE)

  • 캐시에 저장된 직후 혹은 저장과 동시에 DB에도 같은 데이터를 기록한다.

3단계 : 성공 및 완료 반환

  • DB까지 저장이 성공하면 애플리케이션에 최종 완료 신호를 보낸다.

Write Through Strategy Trade Off

  • 장점
    • 데이터 일관성 보장 : 캐시가 항상 최신 데이터를 들고 있기 때문에 DB는 바뀌었는데 캐시가 옛날 데이터인 정합성 문제가 발생하지 않는다.
    • 읽기 성능 극대화 : 데이터가 생성될 때 이미 캐시에 있기 때문에 첫 번째 조회부터 바로 Cache Hit이 발생한다.
  • 단점
    • 쓰기 지연 증가 : 한 번의 쓰기 요청에 캐시와 DB 두 곳에 업데이트를 해야 하므로 단순히 DB에만 저장할 때보다 전체적인 쓰기 속도가 느려진다.
    • 리소스 낭비 가능성 : 한 번 저장되고 다시는 조회되지 않는 데이터까지 모두 캐시에 저장된다. 이로 인해 불필요한 메모리 점유가 발생할 수 있다.

Write Around Strategy

1단계 : 쓰기는 캐시를 우회(Around)해서 DB로만

  • 데이터를 저장·수정할 때 캐시는 건드리지 않고 DB에만 기록한다. (기존 캐시가 있다면 삭제해서 무효화)

2단계 : 캐시는 읽기 시점에 채워짐

  • 이후 해당 데이터가 실제로 조회될 때, Cache Aside(또는 Read Through) 경로를 통해 그제야 캐시에 적재된다.

Write Behind Strategy Trade Off

  • 장점
    • 극한의 쓰기 성능 : DB 응답을 기다릴 필요가 없어 쓰기 요청이 폭주하는 상황에 적용된다.
    • DB 부하 감소 : DB 입장에서 1000개를 개별 처리하는 것보다 1개를 처리하는게 훨씬 효율적이다.
  • 단점
    • 휘발성 존재 : 데이터를 캐시에만 써놓고 아직 DB에 옮기지 않았을 경우에 장애가 발생하면 그동안 쌓인 데이터가 증발한다.
    • 복잡성 야기 : 비동기로 데이터를 옮기는 로직을 별도로 구현해야 하므로 시스템 구조가 복잡해진다.

Read Through Strategy

1단계 : 캐시 요청(GET key)

  • 애플리케이션은 캐시 레이어에만 데이터를 달라고 요청한다. 애플리케이션이 DB 존재를 몰라도 상관없다.

2단계 : 캐시 레이어의 자체 판단

  • 캐시 히트 케이스 : 데이터가 있으면 바로 반환
  • 캐시 미스 케이스 : 데이터가 없으면 캐시 레이어가 직접 DB에 SELECT 쿼리를 날린다.

3단계 : 자동 저장 및 반환

  • DB에서 읽어온 데이터를 캐시 레이어가 자신의 메모리에 스스로 저장한다.
  • 저장 직후 애플리케이션에 데이터를 전달한다.

4단계 : 애플리케이션의 단순성

  • DB 접근 로직을 애플리케이션 코드에 작성할 필요가 없어진다.
  • Cache Aside와의 차이는 "캐시 미스 시 DB에 다녀오는 책임"이 애플리케이션(Aside)에 있느냐, 캐시 레이어(Through)에 있느냐에 있다.

Read Through Strategy Trade Off

  • 장점
    • 코드 결합도 감소 : 애플리케이션 코드에서 DB 조회 로직이 사라지기 때문에 비즈니스 로직에만 집중할 수 있다.
    • 데이터 일관성 유지 : 데이터 로딩 방식이 캐시 레이어에 일원화되어 있어, 데이터 정합성을 관리하기가 더 수월하다.
    • Look-Aside 상위 호환 : 첫 요청에 캐시 미스가 나더라도 자동화된 로직 덕분에 클라이언트는 일관된 인터페이스로 데이터를 받을 수 있다.
  • 단점
    • 구현 난이도 높음 : 많은 Redis 라이브러리가 이 Read Through 패턴을 기본 제공하지 않기에 이를 지원하는 별도의 캐시 프로바이더를 쓰거나 직접 커스텀 레이어를 구현해야 한다.
    • 캐시 레이어 의존성 증가 : 캐시 레이어가 죽으면 DB로 가는 통로 자체가 막히므로 캐시 시스템의 고가용성이 필수 전제가 된다.

Cache Stampede(캐시 쇄도) 문제와 대응

1단계 : 캐시 만료 직후 (The Gap)

  • 평소에 모든 요청이 Redis에서 처리되다가, 설정한 TTL이 만료되어 데이터가 삭제된다.

2단계 : 동시다발적 Cache Miss

  • 클라이언트 1 ~ 3이 동시에 같은 데이터를 요청하지만 Redis는 이 요청 모두에게 데이터 없음이라고 응답한다.

3단계 : 중복된 SELECT 쿼리 폭탄

  • 데이터가 없다고 판단한 클라이언트가 각각 동일한 원본 데이터를 찾고자 DB를 호출한다.
  • 똑같은 연산을 애플리케이션 파드 수만큼 여러 번 수행하게 된다.

4단계 : DB 과부하 및 시스템 장애

  • DB가 갑자기 수백, 수천 개의 중복 쿼리를 받게 되면서 DB 응답이 느려지게 된다.
  • DB 응답이 느려지면 애플리케이션 쓰레드가 점유되어 전체 시스템이 마비되는 연쇄 장애로 이어지게 된다.

❗해당 현상을 방지하기 위해 다음과 같은 기술을 사용하게 된다.

  • 분산 락(Distributed Lock) : DB에 접근하기 전에 Redis 등을 이용해 지금 DB에서 데이터를 가져오는 애플리케이션이 하나뿐인지를 체크하고(SET lock nx ex), 먼저 도착한 요청 하나만 DB에 가고, 나머지는 잠시 대기했다가 캐시가 채워진 뒤 캐시에서 읽어간다.
  • PER 알고리즘(Probabilistic Early Recomputation) : 캐시가 만료되기 직전에 확률적으로 미리 갱신하는 방법이다. 만료되기 10초 전쯤 누군가 요청을 보내면 어차피 곧 만료될거라 미리 갱신하라고 처리하여 공백 시간 자체를 없앤다.
  • 만료 시간 무작위화(Jitter) : 여러 데이터 만료 시간이 한꺼번에 겹치지 않도록 TTL을 설정할 때, n(min) + plus alpha(sec)와 같은 방식으로 분산시킨다.
  • 백그라운드 갱신 : 데이터 만료와 상관없이 별도의 스케줄러가 캐시를 주기적으로 최신화한다.

전략 선택 요약

전략 담당 영역 한 줄 요약 적합한 상황
Cache Aside 읽기 앱이 직접 캐시 확인 → 미스면 DB 조회 후 캐시 적재 대부분의 읽기 중심 서비스 (기본값)
Read Through 읽기 캐시 레이어가 DB 조회까지 대행 캐시 로직을 인프라로 일원화하고 싶을 때
Write Through 쓰기 캐시와 DB에 동시 기록 쓴 직후 바로 읽는 데이터, 강한 읽기 정합성
Write Around 쓰기 DB에만 쓰고 캐시는 읽힐 때 적재 쓰기 많고 재조회 적은 데이터 (로그성)
Write Behind 쓰기 캐시에 먼저 쓰고 DB엔 나중에 배치 반영 쓰기 폭주 + 일부 유실 허용 (집계·카운터)
  • 거의 대부분 Cache Aside + Write Around 조합이며, 쓰기 정합성·성능 요구에 따라 Write Through / Write Behind를 선택적으로 도입한다.