Redis ‐ Redis 캐싱 전략 - thought-corner/Backend-PlayGround GitHub Wiki
캐시(Cache), 캐싱(Caching)이란?
- 캐시(Cache) : 원본 저장소보다 빠르게 가져올 수 있는 임시 데이터 저장소를 의미한다.
- 캐싱(Caching) : 캐시에 접근해서 데이터를 빠르게 가져오는 방식을 의미한다.
Cache Aside Strategy
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에 문제가 생겨도 서비스가 완전히 죽지 않는다.
- 자원 효율성 : 실제로 자주 조회되는 데이터가 캐시에 올라가므로 메모리를 효율적으로 사용한다.
- 단점
- 데이터 정합성(Data Consistency) : DB 원본이 수정되었는데 캐시가 그대로 남아있다면 사용자는 과거의 데이터를 보게 된다. DB 데이터를 업데이트할 때마다 관련 캐시를 삭제하거나 TTL을 짧게 설정한다.
- 캐시 설람(Cache Stampede/Thundering Herd) : 데이터 캐시가 만료되는 순간 동시에 "캐시 미스"를 겪으며 DB에 요청이 몰릴 수 있다. TTL을 무작위로 설정하거나 데이터 갱신을 백그라운드에서 미리 수행하는 방식을 고민해야 한다.
Write Around Strategy
1단계 : 캐시에 먼저 기록(SET key value)
- 애플리케이션이 데이터를 저장하거나 수정할 때, 가장 먼저 Redis에 데이터를 쓴다.
2단계 : DB에 즉시 동기화(INSERT/UPDATE)
- 캐시에 저장된 직후 혹은 저장과 동시에 DB에도 같은 데이터를 기록한다.
3단계 : 성공 및 완료 반환
- DB까지 저장이 성공하면 애플리케이션에 최종 완료 신호를 보낸다.
Write Around Strategy Trade Off
- 장점
- 데이터 일관성 보장 : 캐시가 항상 최신 데이터를 들고 있기 때문에 DB는 바뀌었는데 캐시가 옛날 데이터인 정합성 문제가 발생하지 않는다.
- 읽기 성능 극대화 : 데이터가 생성될 때, 이미 캐시에 있기 때문에 첫 번째 조회부터 바로 Cache Hit이 발생한다.
- 단점
- 쓰기 지연 증가 : 한 번의 쓰기 요청에 캐시와 DB 두 곳에 업데이트를 해야하므로 단순히 DB에만 저장할 때보다 전체적인 쓰기 속도가 느려진다.
- 리소스 낭비 가능성 : 한 번 저장되고 다시는 조회되지 않는 데이터까지 모두 캐시에 저장된다. 이로 인해 불필요한 메모리 점유가 발생할 수 있다.
Write Behind Strategy
1단계 : 캐시에 즉시 저장(SET key value)
- 애플리케이션은 모든 데이터를 Redis에 먼저 쓴다.
- Redis는 메모리 기반이라 쓰기 속도가 압도적으로 빠르기 때문에 애플리케이션은 저장 완료 신호를 받고 다음 일을 하러 간다.
2단계 : 큐(Queue)를 통한 비동기 전달
- 캐시에 쌓인 데이터들은 바로 DB로 가지 않고 메시지 큐나 Redis 내부 버퍼에 잠시 머문다.
3단계 : 나중에 DB 기록(Batch INSERT/UPDATE)
- 시스템이 한가할 때나 일정 시간이 지나면 큐에 쌓인 데이터들을 모아서 한꺼번에 DB에 반영한다.
- 100건의 수정을 100번 호출하는 것이 아니라 1번의 Bulk Insert로 처리하므로 DB 부하가 획기적으로 줄어든다.
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 접근 로직을 애플리케이션 코드에 작성할 필요가 없어진다.
Read Through Strategy Trade Off
- 장점
- 코드 결합도 감소 : 애플리케이션 코드에서 DB 조회 로직이 사라지기 때문에 비즈니스 로직에만 집중할 수 있다.
- 데이터 일관성 유지 : 데이터 로딩 방식이 캐시 레이어에 일원화되어 있어, 데이터 정합성을 관리하기가 더 수월하다.
- Look-Aside 상위 호환 : 첫 요청에 캐시 미스가 나더라도 자동화된 로직 덕분에 클라이언트는 일관된 인터페이스로 데이터를 받을 수 있다.
- 단점
- 구현 난이도 높음 : 많은 Redis 라이브러리가 이 Read Through 패턴을 기본 제공하지 않기에 이를 지원하는 별도의 캐시 프로바이더를 쓰거나 직접 커스텀 레이어를 구현해야 한다.
- 캐시 레이어 의존성 증가 : 캐시 레이어가 죽으면 DB로 가는 통로 자체가 막힐 수 있으므로 캐시 시스템의 고가용성이 매우 약해진다.
Cache Stampede Strategy
1단계 : 캐시 만료 직후 (The Gap)
- 평소에 모든 요청이 Redis에서 처리되다가, 설정한 TTL이 만료되어 데이터가 삭제된다.
2단계 : 동시다발적 Cache Miss
- 클라이언트 1 ~ 3이 동시에 같은 데이터를 요청하지만 Redis는 이 요청 모두에게 데이터 없음이라고 응답한다.
3단계 : 중복된 SELECT 쿼리 폭탄
- 데이터가 없다고 판단한 클라이언트가 각각 동일한 원본 데이터를 찾고자 DB를 호출한다.
- 똑같은 연산을 애플리케이션 파드 수만큼 여러 번 수행하게 된다.
4단계 : DB 과부하 및 시스템 장애
- DB가 갑자기 수백, 수천 개의 중복 쿼리를 받게 되면서 DB 응답이 느려지게 된다.
- DB 응답이 느려지면 애플리케이션 쓰레드가 점유되어 전체 시스템이 마비되는 연쇄 장애로 이어지게 된다.
해당 현상을 방지하기 위해 다음과 같은 기술을 사용하게 된다.
- 분산 락(Distributed Lock) : DB에 접근하기 전에 Redis 등을 이용해 지금 DB에서 데이터를 가져오는 애플리케이션이 하나뿐인지를 체크하고 먼저 도착한 요청 하나만 DB에 가고, 나머지는 잠시 대기했다가 캐시가 채워진 뒤 캐시에서 읽어간다.
- PER 알고리즘(Probabilistic Early Recomputation) : 캐시가 만료되기 직전에 확률적으로 미리 갱신하는 방법이다. 만료되기 10초 전쯤 누군가 요청을 보내면 어차피 곧 만료될거라 미리 갱신하라고 처리하여 공백 시간 자체를 없앤다.
- 만료 시간 무작위화(Jitter) : 여러 데이터 만료 시간이 한꺼번에 겹치지 않도록 TTL을 설정할 때,
n(min) + plus alpha(sec)와 같은 방식으로 분산시킨다.- 백그라운드 갱신 : 데이터 만료와 상관없이 별도의 스케줄러가 캐시를 주기적으로 최신화한다.