최근 MSA나 DB를 공부하던 중, DB와 캐시를 자주 사용하지만, 그 안의 트래픽 문제나 정합성 문제는 어떻게 해결을 해야하나 궁금증이 들게되었다. 그래서 다시 처음부터 집고 가자는 생각으로 정리하고자 한다.
1. DB 트래픽 과부화
먼저 DB 시스템은 확장하기 어려운 시스템이다.
주로 샤딩과 replica를 통해 확장하지만, 일관성/가용성/분할 내성 셋을 모두 만족 시킬수 없다는 CAP 이론이 널리 알려져있다.
그래서 DB의 부하를 최소화하여 확장 필요성을 줄이는 것이 제일 첫번쨰로 생각해야할 일이다.
이를 위해 우리는 빠르고 사용하기 쉬운 Redis와 같은 인메모리 저장소로 캐시 시스템을 구축하여 사용한다.
하지만 이때 몇가지 고려해야할 사항이 있다.
1. 캐시 쇄도(Cache Stampede)

캐시는 일정 기간동안 키를 저장해둔다.
하지만 다량의 키가 동시에 만료되고, 이에 대한 조회가 갑자기 늘어난다면 DB로의 조회 요청이 증가할 것이다.
특히 키를 매일 자정마다 갱신한다고 가정하면, 이에대해 갱신이 동시에 이루어지며 트래픽이 증가할 것이다.
이를 위해 우리는 지터를 사용한다.
지터는 만료시간을 각각 조금씩 다르게 하여, 부하를 분산시키는 것이다.
예를들어 자정에 동시에 키를 갱신하는 것이 아닌, 랜덤하게 각 키마다 0~60초의 지연시간을 추가해두면 DB로의 갱신 요청 부하는 분산될 것이다.
2. 캐시 관통(Cache Penetration)

보통 캐시에 없다면 DB로 조회 요청을 보낸다
이때 db에 값이 있다면 캐시에 값을 갱신하고 값을 돌려줄수도 있다.
하지만, db에 데이터가 없다고, 캐시가 없다고만 반환하면 매번 db로의 조회가 반복될 것이다.
그래서 '값이 없다'고 나타내기 위한 값을 정하고 이를 반환해줄수 있다.
예를 들어 양수만 반환해야하는데, 0을 반환하는 방식이 있을 것이다.
혹은 블룸 필터를 통해, 캐시에서 한번 값이 없는지 1차적으로 조회하는 방식을 사용할수도 있다.
하지만 블룸 필터는 db와의 정합성이 깨질 경우 모든 데이터에 대해서 다시 조회해야하는 문제가 생길수 있으므로 조심해야한다.
(물론 실제 사례를 보면 블룸 필터를 통해 많은 성능 향상을 이룬 경우도 많다.)
3. 캐시 시스템 장애

캐시에 장애가 생긴다고해서 모든 요청을 db로 보내도 문제가 생길 수 있다.
db에 너무 많은 부하가 생기게 되므로, 캐시가 복구될때 까지 꼭 필요한 기능만 작동하도록 failover를 정해둬야한다.
4. 핫(Hotkey) 만료

보통 조회가 자주 생기는 핫키가 자주 생기게 된다.
그래서 핫키가 만료될 경우, 캐시로 갱신되기전에 동시에 조회 요청이 들어와서 db에 부담을 증가 시킬 수 있다.
이를 위해 분산 락을 통해 중복 조회를 방지하여, 부담을 낮출수 있다.
(참고 자료)
https://toss.tech/article/25301
캐시 문제 해결 가이드 - DB 과부하 방지 실전 팁
대용량 트래픽 환경에서 캐시를 사용할 때 주의해야할 위험 상황과 예방법을 소개합니다.
toss.tech
2. 그러면 DB와 캐시/replica 사이의 불일치는 어떻게 해결할 것인가?
캐시를 붙이면 DB 트래픽은 확실히 줄어들지만, 그 다음부터는 자연스럽게 이런 문제가 생긴다.
- DB에는 최신 값이 반영되었는데 캐시는 이전 값을 들고 있다.
- replica는 아직 반영이 안 돼서 더 오래된 값을 준다.
- 어떤 요청은 최신을 보고, 어떤 요청은 오래된 값을 본다.
즉, 캐시/replica를 쓰는 순간 “속도”는 얻지만 “정합성”이 깨질 가능성이 생긴다.
그리고 이는 단순히 “캐시가 늦게 갱신된다” 정도가 아니라, 운영 상황에 따라 장애로 이어질 수 있다.
2.1 캐시 불일치(Cache Inconsistency)의 형태
1. Stale Cache (오래된 데이터)
- DB에는 최신 값이 반영됨
- 캐시는 이전 값을 반환
- TTL 만료로 자연 복구되기도 한다
2. Permanent Inconsistency (영구 불일치)
- DB 변경 이후 캐시 갱신/삭제가 실패
- TTL이 길거나 무한이면 영구적으로 틀린 값 유지
- 실제 장애로 이어지는 가장 위험한 형태다
3. Partial Inconsistency
- 단건 엔티티는 맞는데
- 목록, 정렬 결과, 집계 캐시는 틀린 상태
- 캐시를 단순 key-value가 아니라 “파생 데이터”로 쓰면 이 문제가 더 자주 생긴다
2.2 왜 이런 문제가 발생하는가?
아래 같은 상황이 대표적으로 발생한다.
1. DB UPDATE 성공
2. Cache invalidate(무효화) 실패
3. → 캐시는 이전 값 유지
또는 반대로,
1. Cache UPDATE 성공
2. DB UPDATE 실패
3. → 캐시는 존재하지 않는 새로운 값을 저장
2.3 해결 전략들
캐시 불일치 해결 방법은 크게 4가지 축으로 나뉜다.
- 애플리케이션이 직접 책임지는 방식
- 캐시의 수명을 제한하는 방식(TTL)
- 이벤트/비동기 기반으로 전파하는 방식
- 캐시를 완전히 신뢰하지 않는 방식
2.4 애플리케이션이 직접 책임지는 방식
2.4.1 Cache-aside (Lazy Loading)

가장 널리 쓰이는 방식이다.
- 읽기: 캐시 조회 → miss면 DB 조회 → 캐시에 적재
- 쓰기: DB UPDATE → 캐시 삭제(invalidate) 또는 갱신
장점
- 구현이 단순하다
- 캐시에 문제가 생겨도, 캐시를 거치지 않고 DB에서 직접 데이터를 조회해 서비스는 계속 동작할 수 있다
- 대부분의 프레임워크가 기본 지원한다
한계
- DB는 성공했는데 invalidate가 실패하면 stale이 된다
- invalidate 누락이 쌓이면 영구 불일치로 이어질 수 있다
- 목록/검색/집계 캐시는 invalidate 범위가 커져서 관리가 어렵다
그래서 보통 다음과 같이 보완해서 사용한다고 한다.
- 캐시 삭제 실패 시 재시도 로직을 둔다
- 짧은 TTL과 같이 운영해서 “최대 불일치 시간”을 제한한다
2.4.2 Write-through Cache

- 캐시에 먼저 쓰고
- 캐시가 DB까지 동기 반영하는 방식이다
장점
- 읽기 시 항상 최신 캐시를 유지하기 쉽다
- 캐시-DB 불일치 가능성이 줄어든다
단점
- 쓰기 지연이 증가한다
- 캐시 장애가 DB write까지 막을 수 있다
- 분산 환경에서는 운영 난이도가 높다
쓰기 트래픽이 낮고 읽기 비중이 매우 높을 때 고려하는 편이다.
2.4.3 Write-back (Write-behind)

- 캐시에만 쓰고
- DB 반영은 비동기로 처리한다
장점
- 쓰기 성능이 매우 빠르다
- burst 트래픽 흡수에 강하다
치명적인 단점
- 캐시 장애 시 데이터 손실 위험이 있다
- 강한 내구성이 필요한 도메인에는 부적합하다
세션이나 임시 카운터처럼 “복구 가능한 데이터”에 쓰는 경우가 많다.
2.5 TTL(Time To Live)로 불일치를 제한하는 방식
TTL은 불일치를 막는 것이 아니라,
불일치가 유지되는 최대 시간을 제한하는 장치다.
장점
- invalidate가 실패해도 시간이 지나면 결국 복구된다
- 구현이 단순하고 운영 부담이 적다
한계
- TTL 동안은 반드시 stale이 발생할 수 있다
- 짧으면 캐시 효율이 떨어지고
- 길면 장애 지속 시간이 길어진다
그래서 보통은
- 핵심 데이터: TTL을 짧게
- 비핵심 데이터: TTL을 길게
- TTL + invalidate를 같이 쓰는 형태로 운영한다
2.6 이벤트 기반 캐시 동기화
시스템 규모가 커지면, 애플리케이션에서 invalidate를 직접 다루는 방식은 점점 한계가 온다.
이때는 “DB 변경을 이벤트로 흘려보내고 캐시가 그걸 구독”하는 구조를 사용한다.
2.6.1 애플리케이션 이벤트 방식
- DB 변경 후 애플리케이션이 이벤트 발행
- 캐시가 이벤트를 구독해 invalidate/update
장점
- 캐시 갱신이 중앙화된다
- 캐시가 여러 종류여도 확장 가능하다
치명적 문제
- DB 커밋과 이벤트 발행이 분리된다
- DB는 성공했는데 이벤트 발행이 실패하면 캐시는 영구 불일치가 된다
이 문제를 해결하기 위해 Outbox 패턴를 사용한다.
2.6.2 Outbox Pattern

핵심 아이디어는 단순하다.
- DB 트랜잭션 안에서 (즉, 한번에)
- 비즈니스 테이블 변경
- outbox 테이블에 이벤트 기록
- 별도 워커가 outbox를 읽어서 이벤트를 발행하고 캐시를 갱신한다
장점
- DB 변경과 이벤트 기록이 원자적으로 묶인다
- 이벤트 유실을 방지할 수 있다
- 재처리도 가능하다
단점
- 구현이 복잡해진다
- outbox 테이블 관리가 필요하다
- 이벤트 처리 지연은 여전히 존재한다
2.7 CDC 기반 캐시 동기화

애플리케이션 이벤트/Outbox는 “애플리케이션이 이벤트를 만들고 통제”하는 방식이라면,
CDC는 “DB의 변경 로그를 읽어서 커밋된 변경을 관찰”하는 방식이다.
- DB commit 이후의 변경만 관찰한다
- 애플리케이션 코드와 분리된다
- 오프셋 기반으로 재시작/재처리가 가능하다
CDC를 실제로 구현한 대표적인 도구로는 Debezium이 있다.
CDC는 DB의 변경 로그를 읽어 row 단위로 변경 사항을 이벤트로 전달하는 방식이기 때문에, “회원 탈퇴”, “결제 완료”와 같은 도메인 의미를 직접적으로 담기보다는 "데이터가 어떻게 바뀌었는지"에 초점을 둔다.
이로 인해 이벤트를 소비하는 쪽에서는 중복 처리에 대비한 멱등성 보장이 필요하고, 변경 사항이 비동기로 전파되기 때문에 일정 수준의 지연이 발생하는 eventual consistency 특성을 갖는다.
그럼에도 불구하고 여러 서비스가 하나의 DB를 공유하고 있거나, 레거시 시스템이 포함되어 애플리케이션 단에서 이벤트를 일관되게 발행하기 어려운 경우, 또는 캐시뿐만 아니라 검색 인덱스나 분석용 저장소까지 함께 동기화해야 하는 환경에서는 CDC가 가장 현실적이고 안정적인 선택이 되기도 한다.
2.8 캐시를 완전히 신뢰하지 않는 전략
캐시가 틀리면 안 되는 데이터가 있다.
예를 들어
- 결제 상태
- 권한 정보
- 보안 관련 데이터
이 경우 캐시는 참고용으로만 쓰고, 최종 판단은 DB에서 하도록 설계한다.
또는 캐시 값과 DB version을 비교해서 mismatch면 캐시를 무시하는 Cache Validation도 가능하지만, DB 접근이 늘어나 캐시 효과가 줄어든다.
3. 마무리
캐시 불일치는 피할 수 없는 문제이기에, 결국 “완전히 없애는 것”이 아니라, 운영 가능한 수준으로 통제하는 것이 중요하다고 한다.
- 작은 시스템: Cache-aside + TTL + 보완(재시도, 키 설계)
- 규모가 커질수록: 이벤트 기반(Outbox) 또는 CDC로 전파를 안정화
- 그리고 항상: backfill/reconciliation 같은 복구 수단을 준비
(Backfill: 과거 데이터를 다시 읽어 캐시나 검색 인덱스, 읽기 전용 데이터 저장소를 채워 넣는 작업)
(Reconciliation: DB와 캐시 또는 검색 시스템의 데이터를 비교해 서로 다른 부분을 맞추는 정합성 복구 작업)