캐싱은 웹 서비스 성능 향상을 위한 핵심 기술이며, 특히 Redis와 같은 인메모리 DB를 활용하면 데이터베이스보다 훨씬 빠른 응답 시간을 확보할 수 있습니다.
📌 캐시의 개념과 중요성
캐시(Cache)는 자주 사용되는 데이터를 메모리(RAM)에 저장하여 빠르게 접근할 수 있도록 하는 기술입니다. 데이터베이스 조회보다 메모리 접근이 훨씬 빠르기 때문에 서비스 성능을 크게 향상시킬 수 있습니다.
TIP
캐시에 저장하기 좋은 데이터:
- 자주 조회되는 데이터
- 결과값이 자주 변동되지 않는 데이터
- 조회하는데 연산이 많이 필요한 데이터
그러나 메모리는 용량 제한이 있어(보통 16~32GB) 모든 데이터를 캐시에 저장할 수는 없습니다. 따라서 어떤 데이터를 캐시에 저장할지, 얼마나 오래 보관할지, 언제 제거할지에 대한 전략이 중요합니다.
📌 캐시 관련 주요 개념
- Cache Hit: 캐시 스토어(Redis)에 요청한 데이터가 있어 바로 가져오는 경우 (빠름)
- Cache Miss: 캐시에 데이터가 없어 DB에서 가져와야 하는 경우 (느림)
- 데이터 정합성: 캐시와 DB에 저장된 같은 데이터의 값이 서로 다른 상태
📚 캐시 전략 패턴 종류
1. 읽기 전략 (Read Cache Strategy)
✅ Look Aside (Cache Aside) 패턴
- 특징:
- 데이터 조회 시 먼저 캐시 확인, 없으면 DB 조회 후 캐시에 저장
- 가장 일반적으로 사용되는 기본 캐시 전략
- 반복적인 읽기가 많은 서비스에 적합
- 캐시 장애 발생해도 DB를 통해 서비스 계속 가능
- 장점:
- 캐시와 DB가 분리되어 관리되므로 캐시 장애 시에도 서비스 유지
- 필요한 데이터만 캐시에 저장 가능
- 단점:
- 캐시와 DB 간 정합성 문제 발생 가능
- 초기 조회 시 무조건 DB 호출 필요 (Cache Miss)
- Redis 장애 시 DB로 요청이 몰려 부하 발생 가능
TIP
Cache Warming: 서비스 시작 전에 미리 캐시에 DB 데이터를 적재하는 작업 서비스 초기에 Cache Miss로 인한 DB 부하(Thundering Herd)를 방지할 수 있습니다.
✅ Read Through 패턴
- 특징:
- 캐시에서만 데이터를 읽어오는 전략 (Inline Cache)
- 데이터 동기화를 라이브러리나 캐시 제공자에게 위임
- Look Aside와 비슷하지만 애플리케이션이 아닌 캐시 계층이 DB와 통신
- 장점:
- 캐시와 DB 간 데이터 동기화 관리 필요 없음
- 캐시에서 데이터를 못 찾으면 자동으로 DB에서 로드
- 단점:
- 전체적인 속도가 Look Aside보다 느릴 수 있음
- 캐시 장애 시 서비스 중단 가능성 (캐시에 전적으로 의존)
- 캐시의 가용성을 높이기 위한 추가 구성 필요
2. 쓰기 전략 (Write Cache Strategy)
✅ Write Back (Write Behind) 패턴
- 특징:
- 데이터를 DB에 바로 쓰지 않고 캐시에 모아서 일정 주기로 배치 작업
- 캐시가 일종의 Queue 역할을 겸함
- Write가 빈번하면서 Read 비용이 많이 드는 서비스에 적합
- 장점:
- DB 쓰기 횟수와 부하 감소
- 데이터베이스 일시적 장애에도 서비스 계속 가능
- 정합성 확보 용이
- 단점:
- 캐시 장애 시 데이터 영구 손실 가능성
- 불필요한 리소스 저장 가능성 (TTL 설정 필요)
✅ Write Through 패턴
- 특징:
- 데이터를 캐시와 DB에 동시 저장
- 캐시에 저장 후 바로 DB에 저장 (Write Back과 달리 바로 저장)
- 데이터 유실이 발생하면 안 되는 상황에 적합
- 장점:
- 캐시와 DB 항상 동기화되어 최신 상태 유지
- 데이터 일관성 유지 용이
- 안정적인 데이터 관리
- 단점:
- 매 요청마다 두 번의 Write 발생으로 성능 저하 가능
- 불필요한 리소스 저장 가능성 (TTL 설정 필요)
- 빈번한 생성/수정 시 성능 이슈 발생
✅ Write Around 패턴
- 특징:
- 모든 데이터를 DB에만 저장 (캐시 갱신 안 함)
- Cache Miss 발생 시에만 DB 조회 후 캐시에 저장
- Write Through보다 빠름
- 장점:
- 쓰기 작업이 빠름
- 일회성 쓰기 데이터로 캐시가 오염되지 않음
- 단점:
- 캐시와 DB 내 데이터 불일치 가능성 높음
- 읽기 시 Cache Miss 가능성 높음
3. 읽기 + 쓰기 전략 조합
✅ Look Aside + Write Around 조합
- 가장 일반적으로 사용되는 조합
- 읽기는 캐시 먼저 확인, 쓰기는 DB에만 수행
✅ Read Through + Write Around 조합
- DB에만 쓰고, 캐시에서 읽을 때 항상 DB와 동기화
- 데이터 정합성 문제 해결에 효과적
✅ Read Through + Write Through 조합
- 쓰기와 읽기 모두 캐시를 통해 처리
- 항상 최신 데이터 보장 및 정합성 유지
- AWS DynamoDB Accelerator(DAX)가 이 패턴 사용
📌 캐시 설계 지침
성공적인 캐시 시스템을 구축하려면 다음 5가지 핵심 지침을 고려해야 합니다.
1. 캐시 저장 방식 지침
캐시에는 자주 사용되면서 자주 변경되지 않는 데이터를 저장하는 것이 가장 효율적입니다. 파레토 법칙(80:20 법칙)에 따르면, 전체 데이터의 20%만 캐싱해도 서비스의 80% 요청을 커버할 수 있습니다.
INFO
파레토 법칙(80:20 법칙): 전체 결과의 80%가 전체 원인의 20%에서 일어나는 현상을 의미합니다. 서비스 관점에서는 20%의 데이터만 캐시해도 대부분의 요청을 처리할 수 있다는 의미입니다.
Redis와 같은 인메모리 솔루션은 공간이 제한적이므로(수십 GB), 어떤 데이터를 캐시에 저장하고 제거할지 지속적으로 고민해야 합니다. 또한 캐시는 휘발성이므로 중요한 정보나 민감 정보는 저장하지 않는 것이 좋습니다.
2. 캐시 제거 방식 지침
캐시된 데이터는 대부분 DB에 저장된 데이터의 복사본이므로, 두 저장소 간 데이터 동기화가 중요합니다. 적절한 만료 정책(TTL)을 설정하여 오래된 데이터가 계속 사용되는 것을 방지해야 합니다.
캐시 만료 시간(TTL)은 신중하게 설정해야 합니다:
- 너무 짧으면: 데이터가 너무 빨리 제거되어 캐시 이점 감소
- 너무 길면: 데이터 변경 가능성 및 메모리 부족 현상 발생
💥 Cache Stampede 현상
대규모 트래픽 환경에서 TTL 값이 너무 짧으면 Cache Stampede 현상이 발생할 수 있습니다. 이는 캐시 키가 만료되는 순간 여러 서버가 동시에 DB에 접근하여 duplicate read/write가 발생하는 문제입니다. 이로 인해 DB에 부하가 급증하고 서비스가 느려질 수 있습니다.
3. 캐시 공유 지침
여러 애플리케이션 인스턴스에서 캐시를 공유할 때 데이터 충돌을 방지하는 방법을 고려해야 합니다:
- 낙관적 방식: 캐시 데이터 변경 직전에 데이터가 변경되지 않았는지 확인 후 업데이트
- 업데이트가 드물고 충돌이 적은 상황에 적합
- 비관적 방식(Lock): 캐시 데이터 업데이트 전에 Lock을 사용
- 데이터 크기가 작고 빈번한 업데이트가 발생하는 상황에 적합
4. 가용성 및 성능 확보 지침
캐시의 주 목적은 빠른 성능과 데이터 전달이지, 데이터 영속성이 아닙니다. 따라서:
- 데이터 영속성은 DB에 위임하고, 캐시는 읽기에 집중
- 캐시 서버 장애 시에도 서비스가 계속되도록 설계 (캐시 없이 DB로 대체)
- 성능은 저하될 수 있지만 서비스 중단은 피해야 함
5. Redis 특화 설계 지침
Redis는 단일 쓰레드로 동작하는 인메모리 DB로, 몇 가지 중요한 설계 고려사항이 있습니다:
a. 단일 쓰레드 환경 관리
- Long Time Query 주의: keys * 대신 scan 명령어 사용, flushall 사용 시 async 옵션 고려
- Collection 활용: 단일 Collection에 저장하는 데이터는 1만건 미만으로 제한
- Multi Instance 구성: 멀티코어 활용을 위해 코어 수의 절반 이하로 인스턴스 실행
b. 리소스 활용
- 메모리 관리: maxmemory 설정으로 메모리 사용량 제한, 스왑 메모리 사용 방지
- 네트워크 임계점: Master-Slave 간 데이터 복제 시 네트워크 부하 고려
- Persistence 볼륨: RDB(Snapshot)와 AOF를 적절히 조합하여 사용
c. 복제와 가용성
- Master/Slave Replication: 1개 Master에 N개 Slave 구성 가능
- Redis SENTINEL: 장애 감지 및 자동 복구를 위한 도구
- Sharding: 데이터 분산 저장으로 수평 확장
Redis 메모리 정책과 제거 알고리즘
Redis가 최대 메모리(maxmemory)에 도달하면 새로운 데이터를 저장하기 위해 어떤 데이터를 제거할지 결정해야 합니다. Redis는 다양한 메모리 정책(eviction policy)을 제공합니다:
1. noeviction
- 캐시를 지우지 않는 정책
- 메모리가 maxmemory 이상 사용되면 error 발생
- 새 데이터 추가 시 "Out of memory" 오류 발생
2. allkeys 기반 정책 (모든 키 대상)
allkeys-lru
- LRU(Least Recently Used) 알고리즘 기반
- 가장 최근에 사용되지 않은 키부터 제거
- 일반적인 캐시 용도로 가장 많이 사용됨
allkeys-lfu
- LFU(Least Frequently Used) 알고리즘 기반
- 가장 적게 사용된 키부터 제거
- Redis 4.0 이상에서 지원
allkeys-random
- 무작위로 키를 제거
- 모든 키의 중요도가 비슷할 때 유용
3. volatile 기반 정책 (만료시간이 설정된 키만 대상)
volatile-lru
- 만료시간이 설정된 키 중 LRU 알고리즘으로 제거
- 만료시간이 설정된 키가 없으면 쓰기 작업 실패
volatile-lfu
- 만료시간이 설정된 키 중 LFU 알고리즘으로 제거
- Redis 4.0 이상에서 지원
volatile-ttl
- TTL이 짧은 순으로 키 제거
- 만료 시간이 가까운 항목부터 제거
volatile-random
- 만료시간이 설정된 키 중 무작위로 제거
참고: LRU와 LFU 알고리즘은 Redis에서 정확한 구현이 아닌 근사치 알고리즘을 사용합니다. 이는 성능을 위한 선택으로, maxmemory-samples 옵션으로 정확도를 조정할 수 있습니다.
메모리 관리 최적화 전략
1. 메모리 단편화 관리
- activedefrag: Redis 4.0 이상에서 메모리 조각화 방지
- activedefrag yes로 설정하면 백그라운드에서 메모리 조각화 완화
2. 메모리 사용량 모니터링
- INFO memory 명령으로 메모리 사용 현황 확인
- used_memory, used_memory_rss, mem_fragmentation_ratio 지표 활용
3. 데이터 구조 최적화
- Hash, Sorted Set 등 데이터 구조별 메모리 사용량 차이 이해
- 압축 리스트(ziplist) 활용하여 소형 컬렉션 최적화
4. 키 만료 관리
- Redis의 키 만료는 두 가지 방식으로 동작:
- Active expiration: 요청된 키가 호출될 때 만료 여부 확인
- Passive expiration: 정기적으로 무작위로 키를 스캔하여 만료된 키 제거
- 만료된 키가 자동으로 즉시 삭제되지 않을 수 있음을 인지
- 필요한 경우 SCAN과 TTL 명령을 활용한 명시적 관리 고려
📌 Redis 운영 시 주의사항
1. 메모리 관리
- maxmemory 설정: 시스템 메모리의 60~70% 정도로 설정하는 것이 안전
- eviction policy 선택: 대부분의 캐시 용도는 allkeys-lru가 적합
- 메모리 모니터링: INFO memory 명령으로 정기적인 모니터링 필요
2. 백업 및 복구 전략
- RDB: 특정 시점 스냅샷, 적은 I/O, 장애 시 데이터 손실 가능
- AOF: 모든 쓰기 명령 로그, 더 안전하지만 I/O 증가
- 혼합 사용: RDB + AOF 혼합 구성으로 안정성과 성능 균형
3. 보안 관리
- protected-mode: 외부 접근 차단
- requirepass: 강력한 암호 설정
- 네트워크 분리: 내부 네트워크에서만 접근 가능하도록 구성
- 모니터링: 비정상적인 접근 패턴 감시
참고 자료:
- Redis 공식 문서: https://redis.io/documentation
- Redis 캐시 설계 전략: https://inpa.tistory.com/entry/REDIS-📚-캐시Cache-설계-전략-지침-총정리