🌏 캐시
✅ 캐시의 필요성
대규모 서비스를 운영할 때 가장 큰 문제는 데이터베이스의 부하입니다. 사용자가 많아질수록 모든 요청이 DB로 향하게 되고, DB는 디스크 기반으로 동작하기 때문에 속도가 한계에 부딪힙니다. 특히 동일한 데이터를 여러 사용자가 반복해서 조회하는 경우, 매번 DB를 거치는 것은 불필요한 낭비입니다.
이때 캐시(Cache)는 자주 조회되는 데이터를 메모리(RAM)에 저장해, 다음 요청 때는 DB가 아닌 캐시에서 바로 반환하도록 하는 구조입니다.
결과적으로 DB 부하를 크게 줄이고, 응답 속도를 몇십 배까지 향상시킬 수 있습니다. 웹 서비스에서는 상품 목록, 배너, 사용자 정보처럼 변경이 적고 조회가 잦은 데이터들이 대표적인 캐시 대상입니다. 이런 데이터들은 DB보다는 캐시에서 처리하는 것이 훨씬 효율적입니다.
✅ 특징
Redis는 메모리 기반의 저장소로, DB보다 빠르고 애플리케이션 메모리보다는 느린 중간 계층입니다.
[애플리케이션 메모리] → [Redis 캐시] → [데이터베이스]
(가장 빠름) (빠름) (느림)
애플리케이션 내부 메모리는 빠르지만 서버의 전원이 내려가거나 서버가 종료되면 사라지게 되고(휘발성!!!), 서버가 늘어나면 서버끼리의 데이터 공유가 어렵습니다. DB는 데이터를 안전하게 저장하지만 속도가 느리고 부하에 취약합니다. Redis는 이 둘의 장점을 절충한 형태로, 빠른 조회 속도와 일정 수준의 데이터 공유, 확장성을 모두 제공합니다.
🌏 캐시 전략
✅ Cache Aside (Lazy Loading)

Cache Aside는 가장 일반적인 캐시 전략으로, 데이터를 필요할 때 캐시에 채워 넣는 방식입니다.
- 요청이 들어오면 먼저 캐시에서 데이터를 찾습니다. → 캐시에 있으면 그대로 반환합니다.
- 캐시에 없다면(Cache Miss) DB에서 데이터를 가져옵니다.
- 가져온 데이터를 캐시에 저장한 뒤, 사용자에게 응답을 보냅니다. (후속 요청부터는 캐시 히트)
이렇게 동작하기 때문에 캐시에 자주 쓰는 데이터만 남고, 잘 쓰이지 않는 데이터는 자연스럽게 제거됩니다.
다만 캐시가 비어 있을 때(서비스 재시작 직후 등)는 DB를 계속 조회해야 해서 첫 요청이 느릴 수 있습니다.
✅ Write Through

Write Through는 데이터를 DB와 캐시에 동시에 저장하는 방식입니다.
쓰기 과정에서 두 저장소가 항상 같은 값을 유지하므로, 언제 캐시를 읽어도 최신 데이터가 보장됩니다.
- 클라이언트가 데이터를 수정하거나 추가합니다.
- 애플리케이션이 캐시와 DB에 동시에 쓰기를 수행합니다.
- 이후 읽기 요청은 캐시에서 바로 처리합니다.
항상 최신 데이터가 캐시에 있으므로 읽기 속도가 빠르고, DB와의 불일치가 없습니다.
하지만 매번 두 곳에 쓰기 때문에 속도가 다소 느려질 수 있고, DB가 장애일 경우 쓰기 전체가 실패할 수 있습니다.
주로 주문 내역이나 결제 상태처럼 정확도가 중요한 서비스에서 사용됩니다.
✅ Write Back (Write Behind)

Write Back은 쓰기 요청을 먼저 캐시에만 저장하고, 나중에 일정 시간마다 DB로 한꺼번에 저장하는 방식입니다.
쓰기 성능이 매우 빠르고, 순간적인 트래픽 폭주를 막을 수 있습니다.
- 클라이언트가 데이터를 저장하거나 변경합니다.
- 애플리케이션은 Redis 캐시에만 데이터를 기록하고, 사용자에게 바로 응답합니다.
- Redis가 일정 주기마다(혹은 버퍼가 찼을 때) DB로 데이터를 일괄 전송합니다.
이 방식은 빠르지만, 캐시에 장애가 생기면 DB에 반영되지 않은 데이터가 사라질 수 있습니다.
그래서 완전한 안정성보다 속도를 중시하는 로그, 통계, 세션 정보 등에 자주 사용됩니다.
✅ Read Through

Read Through는 캐시가 DB를 스스로 조회해서 갱신하는 방식입니다.
Cache Aside와 비슷하지만, 차이점은 애플리케이션이 DB를 직접 접근하지 않는다는 점입니다.
- 요청이 들어오면 캐시를 먼저 확인합니다.
- 캐시에 없을 경우, 캐시 자체가 DB에서 데이터를 읽어옵니다.
- 캐시가 그 값을 저장하고 사용자에게 반환합니다.
캐시 계층이 DB 조회 로직까지 담당하기 때문에, 애플리케이션은 DB 접근 코드를 신경 쓸 필요가 없습니다.
Spring에서 @Cacheable 같은 어노테이션을 쓰는 경우가 여기에 해당합니다.
🌏 TTL / Eviction 정책
Redis는 메모리에 데이터를 저장하기 때문에, 언제 데이터를 지우고, 어떤 데이터를 남길지가 굉장히 중요합니다. 이 두 가지를 결정하는 핵심 요소가 바로 TTL(Time To Live) 과 Eviction 정책입니다.
✅ TTL
TTL은 각 데이터(Key)에 설정된 유효 기간으로, 데이터가 언제까지 살아있을지를 정하는 것입니다.
예를 들어 상품 목록을 5분마다 갱신해야 한다면 TTL을 300초로 설정하고, 5분이 지나면 Redis가 해당 데이터를 자동으로 삭제합니다.
TTL은 캐시의 신선도(freshness)를 보장해줍니다. 한 번 저장된 데이터가 너무 오래 남아 있으면, 이미 DB 값은 바뀌었는데 캐시는 예전 데이터를 반환하는 문제가 생기는데, 이걸 방지하기 위해 TTL을 설정해 일정 주기마다 데이터를 갱신하게 만드는 것입니다.
하지만 TTL이 너무 짧게 설정되면 매번 캐시가 지워지고, 다시 DB에서 데이터를 가져와야 하므로 캐시 미스(Cache Miss)가 늘어납니다.
즉, TTL은 너무 짧으면 DB 부하가 증가하고, 너무 길면 데이터가 낡기 때문에 서비스 특성에 맞게 조정해야 합니다.
✅ Eviction
Eviction은 Redis의 메모리가 꽉 찼을 때, 어떤 데이터를 지울지 결정하는 방식입니다.
Redis는 기본적으로 메모리에 모든 데이터를 저장하므로, 메모리 용량을 넘기면 오래된 데이터부터 삭제하는데, 이걸 자동으로 관리해주는 것이 바로 Eviction Policy입니다.
Redis는 여러 가지 Eviction 방식을 지원합니다.
- noeviction : 메모리가 가득 차면 더 이상 데이터를 추가하지 않고 오류를 반환합니다. (Redis를 캐시가 아닌 데이터 저장소로 쓸 때 설정)
- allkeys-lru : 모든 키 중 가장 오랫동안 사용되지 않은(Key access가 가장 오래된) 데이터를 삭제합니다. 가장 일반적으로 많이 쓰이는 설정입니다.
- volatile-lru : TTL이 설정된 키들 중에서만 LRU 정책으로 삭제합니다.
- allkeys-lfu : 가장 적게 사용된(Key access 빈도가 낮은) 데이터를 삭제합니다. 최근보다는 ‘얼마나 자주 사용되었는가’를 기준으로 판단합니다.
- random : 임의의 키를 무작위로 삭제합니다. (테스트용이나 단순 환경에서만 사용)
여기서 LRU(Least Recently Used) 는 “최근에 사용되지 않은 데이터”를 지우는 방식이고, LFU(Least Frequently Used) 는 “사용 빈도가 낮은 데이터”를 지우는 방식입니다. LRU는 단기적으로 자주 쓰는 데이터가 유리한 구조(뉴스 메인 페이지 캐시)에 적합하고, LFU는 일정 기간 동안 꾸준히 요청되는 데이터(인기 상품 목록)에 더 효율적입니다.
✅ Cache Hit / Cache Miss
Redis 캐시가 얼마나 잘 작동하고 있는지를 판단할 때 가장 중요한 지표가 바로 캐시 적중률(Cache Hit Ratio) 입니다. 이 수치는 전체 요청 중에서 얼마나 많은 요청이 캐시에서 바로 처리되었는지를 의미합니다. 적중률이 높을수록 DB 접근 횟수가 줄고, 응답 속도도 크게 개선됩니다.
반대로 Cache Miss는 캐시에 데이터가 없어 DB를 직접 조회해야 하는 상황입니다. 이 경우 네트워크 지연과 디스크 I/O가 추가로 발생하기 때문에 전체 응답 시간이 길어지고, 트래픽이 많을 때는 DB 부하가 급격히 증가합니다.
캐시 미스가 자주 발생하는 이유는 다양합니다.
- TTL이 너무 짧게 설정된 경우
데이터가 너무 자주 만료되면, 캐시가 데이터를 저장해도 금방 지워져서 매번 DB를 조회하게 됩니다.
(이럴거면 캐시 왜 만듦?ㅋㅋ) - 키의 다양성이 지나치게 높은 경우
사용자 입력 검색어처럼 매번 다른 키로 캐시를 저장하면 재사용률이 낮아지고, Hit율은 떨어집니다. - Cold Start(콜드 스타트)
서버가 재시작되면 캐시 메모리는 비워지기 때문에 초기 몇 분 동안은 캐시가 쌓이지 않아 DB 요청이 몰립니다. - Eviction(데이터 자동 삭제) 이 자주 일어나는 환경
Redis 메모리가 부족하면 오래된 데이터를 제거하기 때문에 캐시에 있어야 할 데이터가 이미 삭제된 상태일 수 있습니다. - 키 네이밍 불일치
user:1과User:1,user_01처럼 동일한 데이터를 서로 다른 키로 저장하면 캐시가 여러 개로 분산되어 Hit율이 낮아집니다.
Cache Miss가 왜 발생하는지를 주기적으로 점검하고, TTL, 키 설계, 메모리 정책을 함께 조정하는 것이 Redis 캐시 효율을 극대화하는 핵심입니다.
캐시를 언제 도입하면 좋을까요?
🌏 캐시를 언제 써야 하는가
캐시는 모든 서비스에 무조건 필요한 기술이 아닙니다. 득일지 실일지를 잘 생각한 후 도입해야 합니다.
✅ 캐시가 필요한 상황
“자주 조회되지만 자주 바뀌지 않는 데이터”
- 읽기(Read) 요청이 매우 많은 서비스
- 예: 상품 목록, 게시글 조회, 랭킹 페이지 등
- 동일한 데이터를 여러 사용자가 반복적으로 요청한다면, 캐시가 없을 때 DB 부하가 급격히 늘어납니다.
- 데이터가 자주 바뀌지 않는 경우
- 예: 카테고리 목록, 설정 정보, 배너 이미지 등
- 변경 빈도가 낮은 데이터는 한 번만 DB에서 가져와 캐시에 보관하는 것이 훨씬 효율적입니다.
- 외부 API나 복잡한 연산 결과를 재사용할 때
- 예: 외부 환율 API, 추천 알고리즘 결과 등
- 매번 계산하거나 외부 호출을 하는 대신, 결과를 캐싱해두면 시스템 부하를 크게 줄일 수 있습니다.
✅ 캐시가 문제를 일으킬 상황
캐시는 잘못 쓰면 오히려 시스템을 불안정하게 만들 수 있습니다.
- 데이터 일관성이 중요한 시스템
- 예: 금융 거래, 결제, 주문 상태
- 캐시가 DB보다 늦게 갱신되면 “입금했는데 잔액이 그대로” 같은 오류가 발생할 수 있습니다.
- 데이터 변경이 너무 자주 일어나는 경우
- 예: 실시간 센서 데이터, 채팅 메시지 등
- 캐시를 갱신하는 비용이 DB 조회보다 커져 오히려 손해입니다.
- TTL 설정이 부적절할 때
- 너무 짧으면 캐시가 자주 비워지고, 너무 길면 낡은 데이터가 남아 있습니다.
- 이로 인해 캐시 Hit율이 떨어지고, DB와의 불일치가 심해집니다.
- 캐시 서버 장애 시 의존성 과다
- 캐시 서버가 죽으면 모든 요청이 DB로 몰리며 폭주가 일어날 수 있습니다.
- 따라서 캐시 장애 시에도 서비스가 최소한으로 버티도록 Fallback 전략을 설계해야 합니다.
🌏 Redis는 왜 빠른가?
✅ 싱글 스레드: 락 충돌과 스레드 전환 비용이 없음
Redis는 싱글 스레드 기반으로 동작합니다. 하나의 스레드만 사용하므로 겉보기에는 멀티스레드보다 느릴 것처럼 보이지만, 실제로는 대부분의 경우 오히려 훨씬 빠르게 처리됩니다. 관련해서는 이 포스팅을 보면 자세한 내용을 확인할 수 있습니다.

싱글 스레드 구조는 락(lock) 충돌이나 컨텍스트 스위칭이 발생하지 않는다는 장점이 있습니다.
멀티스레드 환경에서는 여러 스레드가 같은 데이터를 동시에 접근하며 락을 걸어야 하지만, Redis는 한 번에 하나의 명령만 순서대로 처리하기 때문에 이런 병목이 없습니다.
✅ 메모리 기반 연산
Redis가 모든 연산을 메모리(RAM)에서 수행하기 때문입니다. 디스크 접근이 없고, 명령 실행 속도가 거의 O(1)에 가까워서 단일 스레드로도 초당 수만~수십만 건의 요청을 처리할 수 있습니다.
✅ 이벤트 루프 기반의 비동기 처리(epoll)
여기에 Redis는 epoll 기반의 이벤트 루프(Event Loop) 를 사용합니다.
요청이 들어올 때마다 새로운 스레드를 생성하지 않고, 하나의 스레드가 수천 개의 클라이언트 요청을 비동기적으로 처리할 수 있습니다. 즉, I/O가 많은 환경에서도 효율적으로 동작합니다.
결과적으로 Redis는 구조가 단순하지만, 락이 없고, 컨텍스트 스위칭이 없고, I/O가 빠른 최적의 설계 덕분에
멀티스레드보다 더 빠른 응답 속도와 안정성을 제공합니다.
🌏 캐시 일관성(Cache Consistency) 문제
✅ 개념
캐시 일관성 문제란, DB와 캐시의 데이터가 일시적으로 불일치하는 현상을 말합니다. 즉, DB의 값은 변경되었지만 캐시에는 이전 값이 남아 있는 상황이죠.
이 문제는 주로 Cache Aside 패턴을 사용할 때 발생합니다. 일반적으로 DB → 캐시 순으로 데이터 흐름이 이루어지는데, DB를 업데이트한 직후 캐시를 지우기 전에 다른 요청이 들어오면 캐시가 다시 오래된 값을 DB에서 가져와 저장할 수 있습니다.
이런 현상을 Cache Stampede(캐시 스탬피드) 또는 데이터 정합성 문제라고 부릅니다.
✅ 발생 원인
- DB 업데이트와 캐시 삭제 사이의 시간 차이
- 트랜잭션이 완료되기 전, 다른 요청이 캐시를 갱신해버리는 경우
- TTL이 만료되어 캐시 미스 발생
- 동시에 여러 요청이 들어오면 수십, 수백 개의 DB 쿼리가 한꺼번에 발생 (Thundering Herd 현상)
- 비동기 처리나 큐 기반 캐시 갱신 지연
- 캐시 반영 속도가 느릴 경우 최신 데이터 반영이 늦어짐
✅ 해결 방법
- 뮤텍스 키(Mutex Key) 사용
캐시가 비어 있을 때(DB에서 데이터를 가져오는 중) 다른 요청이 동시에 DB에 접근하지 못하도록 Lock을 겁니다.
첫 번째 요청만 DB에서 데이터를 가져오게 하고, 나머지는 대기하거나 캐시 데이터가 채워질 때까지 기다립니다. - Thundering Herd 방지 로직
캐시 미스 시 모든 요청이 동시에 DB로 향하지 않도록 하나의 요청만 허용하고, 나머지는 짧은 지연(백오프) 후 재시도하도록 설계합니다. - TTL 분산(Random TTL) 적용
모든 캐시 키가 동시에 만료되지 않도록 TTL에 ± 몇 초의 랜덤 값을 더해 캐시 만료 시점을 분산합니다.
예를 들어 TTL을 300초로 설정했다면,300 ± 30초형태로 설정해 트래픽 집중을 완화합니다.
🌏 Redis 장애 대응 및 모니터링
Redis는 메모리 기반 시스템이기 때문에 속도는 매우 빠르지만, 운영 환경에서는 메모리 초과나 서버 장애 복구 속도 같은 이슈가 빈번하게 발생할 수 있습니다. 이런 문제를 예방하기 위해서는 주요 지표를 꾸준히 모니터링하고, 각 상황에 맞는 대응 전략을 미리 설계해야 합니다.
✅ 주요 모니터링 지표
- Hit Ratio (캐시 효율)
캐시에서 바로 응답된 요청 비율입니다.
값이 낮아지면 캐시가 자주 비워지거나 TTL이 너무 짧을 가능성이 있습니다.
TTL을 늘리거나, 자주 사용하는 데이터를 사전 로딩(pre-warming)하는 방식으로 개선합니다. - Memory Usage (메모리 사용량)
Redis가 사용 중인 메모리 양입니다.
사용률이 90% 이상으로 올라가면 Eviction(자동 삭제)이 자주 발생해 캐시 효율이 떨어집니다.
불필요한 키 정리, TTL 설정 강화, 또는 메모리 증설이 필요합니다. - Evicted Keys (자동 삭제된 키 수)
Redis가 메모리 부족으로 삭제한 키의 개수를 의미합니다.
값이 급격히 증가한다면 캐시 데이터가 강제로 밀려나고 있는 상태입니다.
maxmemory-policy를 점검하고, LRU 대신 LFU 정책으로 전환하거나 TTL 정책을 재설계합니다. - Connected Clients (동시 연결 수)
현재 Redis에 접속 중인 클라이언트 수입니다.
수치가 비정상적으로 많다면 커넥션 누수나 서비스 과부하 가능성이 높습니다.
커넥션 풀 크기 조정, maxclients 제한 설정, 타임아웃(timeout) 값 점검으로 대응합니다. - Replication Lag (복제 지연)
Master와 Replica 간 데이터 동기화 지연 시간입니다.
Replica가 뒤처지면 장애 시 데이터 유실 가능성이 커집니다.
네트워크 대역폭 확인, 복제 버퍼(repl-backlog-size) 확장, Sentinel 알림 활성화로 예방합니다.
✅ 장애 유형별 해석과 대응 방법
1) 메모리 초과 (Out of Memory)
- 현상: Redis가
OOM command not allowed when used memory > 'maxmemory'오류를 반환 - 원인: 캐시 데이터가 너무 많거나 TTL이 없어 만료되지 않음
- 대응 방법
maxmemory설정값 상향maxmemory-policy확인 (volatile-lru,allkeys-lfu등 적절히 조정)- 불필요한 키 삭제 및 TTL 설정 철저히 적용
2) Eviction 폭주 (자동 삭제 급증)
- 현상: 캐시 Hit율 급격히 하락, DB 부하 증가
- 원인: 메모리 부족, LRU 정책으로 인해 자주 쓰는 키도 삭제됨
- 대응 방법
- TTL 조정 → 너무 짧으면 캐시 자주 만료
- 캐시 대상 최소화 → 자주 조회되는 데이터만 캐싱
- 필요 시 메모리 증설 및 LFU 정책 전환
3) Replication Lag (복제 지연)
- 현상: Master에서 Write는 성공했는데 Replica가 늦게 반영
- 원인: 네트워크 대역폭 부족, Replica CPU 부하
- 대응 방법
- Replica 노드 수 조정 (읽기 분산)
repl-backlog-size확장- 네트워크 대역폭 및 스냅샷 주기 점검
4) Sentinel 장애 전환 (Failover)
- 현상: Master 장애 시 자동으로 Replica가 Master로 승격되지 않음
- 원인: Sentinel 설정 오류, quorum 불일치
- 대응 방법
- Sentinel 최소 3대 이상 구성 (과반수 투표 구조)
- 각 노드 간
sentinel monitor,quorum값 확인 down-after-milliseconds값 적정 조정 (너무 짧으면 오탐, 너무 길면 전환 지연)
🌏 분산 환경에서 Redis 사용하기
Redis는 단일 인스턴스로는 한계가 있기 때문에, 대규모 트래픽 환경에서는 복제와 클러스터링으로 확장성을 확보합니다.
- Master–Replica 구조
- Master가 쓰기, Replica가 읽기를 담당
- Replica 장애 시 자동 승격 가능 (Sentinel 사용)
- Redis Sentinel
- Master의 상태를 감시하고 장애 시 Replica를 자동 승격
- 고가용성(HA) 구조의 핵심
- Redis Cluster
- 데이터를 여러 노드에 분산 저장 (샤딩)
- 최대 16,384개의 슬롯을 기반으로 분할 처리
- 노드 일부 장애 시에도 서비스 지속 가능
🌏 휘발성 Redis 캐시를 데이터베이스로 사용하기?
기본적으로 Redis는 메모리 기반이기 때문에 서버가 꺼지면 데이터가 사라집니다. 하지만 캐시를 설정을 통해 영속형 데이터베이스로 활용할 수도 있습니다.
✅ RDB (Redis Database Snapshot)
Redis의 메모리 상태를 주기적으로 디스크에 스냅샷으로 저장합니다. 예를 들어 save 900 1 설정을 하면, 900초(15분) 동안 최소 1개의 변경이 있을 때 스냅샷을 생성합니다. 따라서 서버가 재시작되면 마지막 저장 시점의 데이터를 복원할 수 있습니다.
- 장점: 백업 용도로 간단하고 빠름
- 단점: 마지막 저장 시점 이후 변경된 데이터는 복구되지 않음 (계속 스냅샷을 때려?)
✅ AOF (Append Only File)
Redis가 처리한 모든 쓰기 명령을 순차적으로 파일에 기록합니다. 장애 발생 시, 파일을 읽어 모든 명령을 다시 실행함으로써 데이터 복구가 가능합니다. 또 appendfsync always | everysec | no 설정으로 기록 주기를 조절할 수 있습니다.
- 장점: 데이터 손실 가능성이 거의 없음
- 단점: 디스크 I/O가 늘어나고 파일 크기가 커짐
✅ RDB + AOF 함께 사용
두 가지를 병행 사용한다면 훨씬 안정적입니다.
RDB는 빠른 재시작용 스냅샷으로 활용하고, AOF는 세밀한 데이터 복구용 로그로 사용합니다.
Redis 7.0 이후에는 두 기능이 결합된 Mixed Persistence 모드도 지원되어, 더 빠르고 안정적인 영속성 확보가 가능합니다.

도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'아키텍처+MSA' 카테고리의 다른 글
| [MSA] MSA 운영에서 발생하는 이슈들: 데이터 일관성/트랜잭션 적용 불가/장애 전파/이벤트 불일치/조회 성능 (0) | 2025.11.27 |
|---|---|
| [아키텍처/캐시] CDN 완전 정복: 개념·헤더·보안, CloudFront vs Cloudflare 비교까지 (0) | 2025.11.21 |
| [아키텍처] 로드밸런싱 완전 정복: L4/L7 구조부터 SPOF 해결까지 (0) | 2025.11.20 |
| [아키텍처/Kafka] Kafka로 강한 결합 탈출하기: 회원가입 비동기 처리 미니 프로젝트 (0) | 2025.11.05 |
| [아키텍처/Kafka] 메세지 전송 보장 방식(At most once, At least once, Exactly once): 특징과 EOS 구현 방법 (0) | 2025.09.17 |