캐시 패턴 완벽 가이드 - 개념부터 실전 선택 기준까지

2025년 12월 05일

experience

시리즈캐시 패턴#1
# Cache# Redis# 캐시 패턴# Cache-Aside# Write-Through# Write-Behind

들어가며

백엔드 개발을 하다 보면 “캐시 좀 붙여야겠다”는 말을 자주 하게 됩니다. 그런데 막상 캐시를 적용하려고 하면 여러 고민이 생깁니다.

  • 캐시와 DB 데이터가 달라지면 어떡하지?
  • 캐시를 갱신해야 할까, 삭제해야 할까?
  • Write-Through? Write-Behind? 뭐가 다른 거지?

이 글에서는 캐시 패턴의 핵심 개념을 정리하고, 실전에서 어떤 기준으로 전략을 선택해야 하는지 알아봅니다.

ℹ️

이번 글의 범위

  • 다루는 것: 캐시 패턴 4가지 비교, 데이터 특성별 전략 선택 기준
  • 다루지 않는 것: Spring Boot 구현 코드 (2편에서 다룸), Thundering Herd 문제 (5편에서 다룸)

캐시를 왜 쓰나요?

가장 기본적인 질문부터 시작해봅시다.

// 그냥 DB에서 읽으면 안 되나?
fun getProduct(id: Long): Product {
    return productRepository.findById(id)
}

캐시의 트레이드오프

  • DB 부하 감소 - 반복 조회를 캐시가 흡수
  • 빠른 응답 속도 - Disk I/O(ms) vs Memory(μs), 수십~수백 배 차이
DB 조회
5-50ms
Disk I/O 포함
Redis 조회
0.1-1ms
Memory only

캐시 갱신 vs 삭제: 동시성 문제

DB를 수정할 때 캐시도 같이 처리해야 합니다. 크게 두 가지 방식이 있습니다.

❌ Before
// 방식 A: 캐시 갱신
fun updatePrice(id: Long, price: Int) {
    productRepository.updatePrice(id, price)
    cache.put(id, newProduct)  // 새 값 저장
}
✅ After
// 방식 B: 캐시 삭제
fun updatePrice(id: Long, price: Int) {
    productRepository.updatePrice(id, price)
    cache.delete(id)  // 캐시 삭제
}
언뜻 갱신이 좋아 보이지만, 동시성 문제가 있습니다

동시성 문제: 갱신 방식의 함정

두 개의 요청이 거의 동시에 들어온 상황을 생각해봅시다:

시간순서:
1. 요청A: 가격을 15,000원으로 수정 시작
2. 요청B: 가격을 20,000원으로 수정 시작
3. 요청A: DB 업데이트 완료 (15,000원)
4. 요청B: DB 업데이트 완료 (20,000원)
5. 요청A: 캐시 갱신 (15,000원)  ← 네트워크 지연
6. 요청B: 캐시 갱신 (20,000원)
7. 요청A: 캐시 갱신 (15,000원)  ← 지연됐던 게 실행

최종 결과:

  • DB: 20,000원
  • 캐시: 15,000원 ❌
🚨

불일치 발생! 사용자는 캐시 TTL이 만료될 때까지 잘못된 가격을 보게 됩니다.

삭제 방식은 안전한가?

같은 상황에서 갱신 대신 삭제를 한다면:

3. 요청A: 캐시 삭제  ← 캐시 비어있음
4. 요청B: 캐시 삭제  ← 캐시 비어있음
5. 요청A: 캐시 삭제  ← 지연됐어도 삭제니까 동일

삭제가 3번 실행되든 순서가 바뀌든, 결과는 캐시가 비어있는 상태입니다.

| 방식 | 동시성 문제 | 결과 | |-----|-----------|------| | 캐시 갱신 | 순서 꼬이면 | DB와 캐시 불일치 가능 | | 캐시 삭제 | 순서 꼬여도 | 다음 조회 시 최신값 로드 |

💡

데이터 정합성 관점에서는 삭제가 더 안전합니다.

삭제 방식도 완벽하지 않다

삭제 방식이 갱신보다 안전하지만, 조회와 갱신이 동시에 일어나면 race condition이 발생합니다:

⚠️

인기 상품처럼 동시 요청이 많은 경우 이런 상황이 자주 발생할 수 있습니다. 해결책(분산락, 캐시 워밍 등)은 5편에서 다룹니다.


4가지 캐시 패턴

패턴 비교 요약

Cache-Aside
가장 일반적
앱이 직접 관리
Read-Through
로더 강제
캐시가 DB 조회
Write-Through
동기 저장
느림, 정합성 좋음
Write-Behind
비동기
빠름, 유실 위험

가장 일반적인 패턴입니다. 애플리케이션이 캐시와 DB를 직접 관리합니다.

// 읽기
fun getProduct(id: Long): Product {
    val cached = cache.get(id)
    if (cached != null) return cached

    val product = db.findById(id)
    cache.put(id, product)
    return product
}

// 쓰기
fun updateProduct(id: Long, data: ProductUpdateRequest) {
    db.update(id, data)
    cache.delete(id)  // 갱신이 아닌 삭제!
}

장점: 구현 단순, 정합성 관리 쉬움

단점: 캐시 미스 구간 존재, 실수 가능성 (캐시 삭제 깜빡)


실전: 데이터 특성별 전략 선택

상품 상세 페이지 예시

data class Product(
    val id: Long,
    val name: String,
    val price: Int,
    val viewCount: Int,    // 조회수
    val stock: Int         // 재고
)

Cache-Aside 권장

  • 자주 안 바뀜
  • 읽기가 많음
  • 잠깐 옛날 데이터 보여줘도 치명적이지 않음
안전TTL 30분

Write-Behind 적합성 체크


분산락 기반 캐시 전략

재고처럼 동시성 제어 + 정확성이 둘 다 필요한 경우:

fun decreaseStock(id: Long, quantity: Int) {
    val lockKey = "lock:stock:$id"

    redisLock.tryLock(lockKey) {
        val stock = redis.get("stock:$id")
        if (stock < quantity) throw OutOfStockException()

        redis.decrease("stock:$id", quantity)  // Redis 차감
        db.decreaseStock(id, quantity)         // DB도 바로 저장
    }
}
💡

Redis 싱글스레드 특성으로 동시성 제어하고, DB에 동기 저장해서 유실 방지!

| | DB 락 (SELECT FOR UPDATE) | Redis 분산락 | |—|-------------------------|--------------| | 속도 | 느림 (Disk I/O) | 빠름 (Memory) | | 동시성 처리량 | 낮음 | 높음 | | 정합성 | DB가 보장 | 앱이 관리 | | 장애 시 | 트랜잭션 롤백 | 유실 가능 |


마치며

캐시 패턴 선택의 핵심은 **“이 데이터가 틀리면 얼마나 큰일인가?”**입니다.

틀려도 OK
Write-Behind
성능 극대화
틀리면 안됨
Cache-Aside
삭제 방식
동시성 중요
분산락
Redis Lock

다음 편에서는 Spring Boot + Redis 환경에서 실제로 Cache-Aside 패턴을 구현해봅니다.


시리즈 목차

  1. 캐시 패턴 개요 및 비교 (현재 글)
  2. Spring Boot + Redis 설정 및 Cache-Aside 구현
  3. Write-Through / Write-Behind 패턴 구현
  4. Cache Invalidation 전략
  5. Thundering Herd / Cache Stampede 해결
  6. 실전 시나리오별 캐시 전략 가이드
© 2025, 미나리와 함께 만들었음