원본 호출이 비싸서 캐시를 둔다. 그런데 캐시를 둔다는 결정 하나로는 끝나지 않는다. 그 결정 뒤에 곧바로 두 가지 질문이 따라온다 — 캐시 miss 때 누가 캐시를 채우는가, 그리고 쓰기는 어디로 가는가.

캐시 디자인 패턴 정전 5종 (Cache-aside, Read-through, Write-through, Write-back, Refresh-ahead) 은 결국 이 두 축의 조합이다. 읽기 채움 책임이 application 에 있느냐 캐시 자체에 있느냐, 쓰기가 캐시를 통과하느냐 우회하느냐 비동기로 흘리느냐. 두 축을 분리해서 결정하면 패턴 선택이 따라온다.

Cache-aside

가장 기본 패턴이고 가장 흔하게 보이는 결이다. application 이 직접 캐시를 조회하고, miss 면 원본을 조회한 뒤 캐시에 채워 넣는다. 쓰기는 원본에 직접 쓰고 캐시는 application 이 무효화하거나 갱신한다.

flowchart LR
    APP["App"] -->|"1. 조회"| C["캐시"]
    C -->|"2. miss"| APP
    APP -->|"3. 조회"| DB[("원본")]
    DB -->|"4. 데이터"| APP
    APP -->|"5. 채움"| C

읽기 채움 책임을 application 이 진다. 구현은 단순하지만, 일관성 책임이 application 코드 곳곳에 분산된다. 캐시와 원본이 어긋나는 stale 윈도우를 application 이 직접 관리해야 하고, 쓰기 시점에 캐시를 무효화할지 갱신할지도 application 결정이다.

Read-through

읽기 채움을 캐시가 책임진다. application 은 캐시만 호출한다. miss 가 발생하면 캐시가 원본을 조회해서 자기 자신을 채우고 application 에 돌려준다.

flowchart LR
    APP["App"] -->|"1. 조회"| C["캐시"]
    C -->|"2. miss → 조회"| DB[("원본")]
    DB -->|"3. 데이터"| C
    C -->|"4. 데이터"| APP

Cache-aside 와의 차이는 채움 책임이 어디에 있는가 한 가지다. application 코드는 단순해지지만, 캐시가 원본을 알아야 한다. 보통 캐시에 원본 조회 함수를 등록해서 캐시가 자체적으로 원본을 부르게 한다. 캐시 계층과 데이터 계층의 결합이 Cache-aside 보다 커진다.

Write-through

쓰기 시 캐시와 원본을 동기로 함께 갱신한다. application 이 쓰기를 캐시로 보내면 캐시가 원본까지 갱신한 뒤 응답한다.

flowchart LR
    APP["App"] -->|"1. 쓰기"| C["캐시"]
    C -->|"2. 쓰기"| DB[("원본")]
    DB -->|"3. ack"| C
    C -->|"4. ack"| APP

캐시와 원본이 항상 일치한다. 강한 일관성이 자연스럽게 따라온다. 대신 쓰기 응답에 캐시 + 원본 두 곳의 write latency 가 모두 포함된다. Read-through 와 결합되면 read/write 모두 캐시를 거치는 구조가 된다. 자주 읽지 않는 항목까지 캐시에 들어가서 적중률이 떨어지는 패턴 (cold-cache pollution) 도 같이 따라온다.

Write-back

쓰기를 캐시에만 동기로 반영하고, 원본은 비동기로 따라간다. write-behind 라는 이름으로도 불린다.

flowchart LR
    APP["App"] -->|"1. 쓰기"| C["캐시"]
    C -->|"2. ack"| APP
    C -.->|"3. 비동기 flush"| DB[("원본")]

쓰기 응답이 빨라진다. write-heavy 워크로드에서 원본 부담을 큰 폭으로 낮춘다. 다만 캐시와 원본의 일관성은 eventual 이 된다. 캐시가 죽으면 아직 flush 되지 않은 쓰기는 손실된다. 그래서 보통 캐시 쪽에 영속 큐, 배치 flush, replication 같은 내구성 보강을 함께 둔다. 쓰기 손실을 어느 정도 받아들일 수 있는 도메인에서 선택지가 된다.

Refresh-ahead

만료 시점이 임박한 항목을 캐시가 선제적으로 다시 가져온다. read miss 자체를 줄이는 predictive 한 접근이다.

flowchart LR
    APP["App"] -->|"조회"| C["캐시"]
    C -->|"hit"| APP
    C -.->|"비동기 (만료 임박)"| DB[("원본")]
    DB -.->|"갱신"| C

자주 읽힐 항목을 만료 전에 갱신해두면 application 입장에서는 거의 항상 hit 이 된다. 만료 직후의 burst latency 를 회피한다. 대신 “자주 읽힐 항목” 을 예측해야 한다. TTL(Time To Live) 임박 임계로 단순히 trigger 하거나, 접근 빈도 기반 heuristic 으로 trigger 한다. 예측이 빗나가면 불필요한 원본 호출이 누적된다.

비교 매트릭스

패턴읽기 채움쓰기 경로일관성적합 워크로드
Cache-asideapplication원본 직접 (캐시 무효화/갱신은 app 책임)약 (app 관리)읽기 多 / 쓰기 패턴 다양
Read-through캐시(별도 패턴과 조합)약~중읽기 多 / app 코드 단순화 우선
Write-through(별도 패턴과 조합)캐시 + 원본 동기일관성 우선 / 쓰기 빈도 낮음
Write-back(별도 패턴과 조합)캐시 동기 + 원본 비동기eventual쓰기 多 / 손실 허용 가능
Refresh-ahead캐시 선제(별도 패턴과 조합)약~중읽기 多 / 접근 패턴 예측 가능

읽기 패턴 (Cache-aside, Read-through, Refresh-ahead) 과 쓰기 패턴 (Write-through, Write-back) 은 직교라 조합이 가능하다. Read-through + Write-through 가 가장 흔한 짝이고, Refresh-ahead 는 어떤 쓰기 패턴과도 함께 둘 수 있다.

도입에서 던졌던 두 질문 — 읽기 채움 책임은 어디에 있는가, 쓰기는 어디로 가는가 — 이 결정 두 개가 패턴 선택보다 먼저 와야 자연스럽다. “Cache-aside 인가 Read-through 인가” 라는 질문은 그 다음에 의미를 가진다.