[Daily morning study] Redis ์บ์‹œ ์ „๋žต (Cache-Aside, Write-Through, Write-Behind)

#daily morning study

Image


์™œ ์บ์‹œ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€

DB์— ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆด ๋•Œ๋งˆ๋‹ค ๋””์Šคํฌ I/O๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜๋ณตํ•ด์„œ ์ฝ๋Š” ํŒจํ„ด์ด ์žˆ๋‹ค๋ฉด, ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•ด๋‘๊ณ  ๋ฐ”๋กœ ๋Œ๋ ค์ฃผ๋Š” ๊ฒŒ ํ›จ์”ฌ ๋น ๋ฅด๋‹ค. Redis๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ ๋ฐ์ดํ„ฐ ์Šคํ† ์–ด๋กœ, ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„์ด 1ms ๋ฏธ๋งŒ์ด๋ผ ์ด๋Ÿฐ ์บ์‹œ ๋ ˆ์ด์–ด๋กœ ๋งŽ์ด ์“ฐ์ธ๋‹ค.

์บ์‹œ๋ฅผ ๋„์ž…ํ•  ๋•Œ ํ•ต์‹ฌ ๊ฒฐ์ • ์‚ฌํ•ญ์€ ๋‘ ๊ฐ€์ง€๋‹ค.

  1. ์ฝ๊ธฐ ์ „๋žต: ์บ์‹œ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์„ ๋•Œ(Cache Miss) ์–ด๋–ป๊ฒŒ ์ฑ„์šธ ๊ฒƒ์ธ๊ฐ€
  2. ์“ฐ๊ธฐ ์ „๋žต: ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ์บ์‹œ์™€ DB๋ฅผ ์–ด๋–ป๊ฒŒ ๋™๊ธฐํ™”ํ•  ๊ฒƒ์ธ๊ฐ€

์ฝ๊ธฐ ์ „๋žต

Cache-Aside (Lazy Loading)

๊ฐ€์žฅ ํ”ํ•˜๊ฒŒ ์“ฐ์ด๋Š” ํŒจํ„ด์ด๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์บ์‹œ๋ฅผ ์ง์ ‘ ๊ด€๋ฆฌํ•œ๋‹ค.

1. ์•ฑ โ†’ Redis ์กฐํšŒ
2. Cache Hit โ†’ ๋ฐ”๋กœ ๋ฐ˜ํ™˜
3. Cache Miss โ†’ DB ์กฐํšŒ โ†’ ๊ฒฐ๊ณผ๋ฅผ Redis์— ์ €์žฅ โ†’ ๋ฐ˜ํ™˜
def get_user(user_id: str):
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.setex(f"user:{user_id}", 3600, json.dumps(user))  # TTL 1์‹œ๊ฐ„
    return user

์žฅ์ 

  • ์‹ค์ œ๋กœ ์ฝํžˆ๋Š” ๋ฐ์ดํ„ฐ๋งŒ ์บ์‹ฑ๋˜๋ฏ€๋กœ ๋ฉ”๋ชจ๋ฆฌ ๋‚ญ๋น„๊ฐ€ ์ ๋‹ค
  • Redis๊ฐ€ ๋‹ค์šด๋˜์–ด๋„ DB๋กœ ํด๋ฐฑํ•  ์ˆ˜ ์žˆ์–ด ์žฅ์•  ๋‚ด์„ฑ์ด ๋†’๋‹ค

๋‹จ์ 

  • ์ฒ˜์Œ ์š”์ฒญ(Cache Miss)์€ ํ•ญ์ƒ ๋А๋ฆฌ๋‹ค (Cold Start)
  • DB ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ TTL์ด ๋งŒ๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€ ์บ์‹œ๋Š” ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค (Stale Data ๋ฌธ์ œ)

Read-Through

Cache-Aside์™€ ๋น„์Šทํ•˜์ง€๋งŒ ์บ์‹œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ๋ณ„๋„ ๋ ˆ์ด์–ด๊ฐ€ DB ์กฐํšŒ๊นŒ์ง€ ๋‹ด๋‹นํ•œ๋‹ค. ์•ฑ ์ฝ”๋“œ๋Š” ํ•ญ์ƒ ์บ์‹œ์—๋งŒ ์š”์ฒญํ•œ๋‹ค.

์•ฑ โ†’ ์บ์‹œ ๋ ˆ์ด์–ด โ†’ (Miss๋ฉด) DB ์กฐํšŒ โ†’ ์บ์‹œ ์ €์žฅ โ†’ ๋ฐ˜ํ™˜

์•ฑ ์ฝ”๋“œ๊ฐ€ ๋‹จ์ˆœํ•ด์ง€์ง€๋งŒ, ์บ์‹œ ๋ ˆ์ด์–ด๊ฐ€ DB ์Šคํ‚ค๋งˆ๋ฅผ ์•Œ์•„์•ผ ํ•˜๋ฏ€๋กœ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋†’์•„์ง„๋‹ค.


์“ฐ๊ธฐ ์ „๋žต

Write-Through

๋ฐ์ดํ„ฐ๋ฅผ ์“ธ ๋•Œ ์บ์‹œ์™€ DB์— ๋™์‹œ์— ์ €์žฅํ•œ๋‹ค.

์•ฑ โ†’ Redis ์ €์žฅ โ†’ DB ์ €์žฅ โ†’ ์™„๋ฃŒ ์‘๋‹ต
def update_user(user_id: str, data: dict):
    # ์บ์‹œ ๋จผ์ € ์—…๋ฐ์ดํŠธ
    redis.setex(f"user:{user_id}", 3600, json.dumps(data))
    # DB๋„ ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธ
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)

์žฅ์ 

  • ์บ์‹œ์™€ DB๊ฐ€ ํ•ญ์ƒ ๋™๊ธฐํ™”๋˜์–ด Stale Data ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค
  • Cache-Aside์˜ ์ฒซ Miss ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค

๋‹จ์ 

  • ์“ฐ๊ธฐ๊ฐ€ ๋‘ ๋ฒˆ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ ์“ฐ๊ธฐ ๋ ˆ์ดํ„ด์‹œ๊ฐ€ ๋Š˜์–ด๋‚œ๋‹ค
  • ์‹ค์ œ๋กœ ๋‹ค์‹œ ์ฝํžˆ์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๋„ ์บ์‹œ์— ์Œ“์ธ๋‹ค (๋ถˆํ•„์š”ํ•œ ์บ์‹œ ์˜ค์—ผ)

Write-Behind (Write-Back)

์บ์‹œ์—๋งŒ ๋จผ์ € ์“ฐ๊ณ , DB ๋ฐ˜์˜์€ ๋น„๋™๊ธฐ๋กœ ๋‚˜์ค‘์— ์ฒ˜๋ฆฌํ•œ๋‹ค.

์•ฑ โ†’ Redis ์ €์žฅ โ†’ ์™„๋ฃŒ ์‘๋‹ต
               โ†“ (๋น„๋™๊ธฐ)
             DB ์ €์žฅ

์žฅ์ 

  • ์“ฐ๊ธฐ ์‘๋‹ต์ด ๋งค์šฐ ๋น ๋ฅด๋‹ค
  • DB์— ์“ฐ๊ธฐ๊ฐ€ ๋ชฐ๋ฆฌ๋Š” ์ƒํ™ฉ(Write-Heavy)์—์„œ ๋ถ€ํ•˜๋ฅผ ๋ถ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค

๋‹จ์ 

  • Redis๊ฐ€ DB์— ๋ฐ˜์˜ํ•˜๊ธฐ ์ „์— ๋‹ค์šด๋˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ์‹ค๋œ๋‹ค
  • ๊ตฌํ˜„ ๋ณต์žก๋„๊ฐ€ ๋†’๋‹ค (๋น„๋™๊ธฐ ํ, ์‹คํŒจ ์žฌ์‹œ๋„ ๋กœ์ง ํ•„์š”)
  • ๊ธˆ์œต ํŠธ๋žœ์žญ์…˜์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ ๋‚ด๊ตฌ์„ฑ์ด ์ค‘์š”ํ•œ ์‹œ์Šคํ…œ์—๋Š” ๋ถ€์ ํ•ฉํ•˜๋‹ค

Write-Around

์“ฐ๊ธฐ ์‹œ ์บ์‹œ๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ  DB์—๋งŒ ์ €์žฅํ•œ๋‹ค. ์ฝ์„ ๋•Œ Cache Miss๊ฐ€ ๋‚˜๋ฉด ๊ทธ๋•Œ ์บ์‹œ๋ฅผ ์ฑ„์šด๋‹ค.

์ผํšŒ์„ฑ ๋ฐ์ดํ„ฐ๋‚˜ ์ž์ฃผ ์ฝํžˆ์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ์— ์ ํ•ฉํ•˜๋‹ค. ๋Œ€์šฉ๋Ÿ‰ ๋กœ๊ทธ ์ €์žฅ, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ ๋“ฑ์ด ์˜ˆ์‹œ๋‹ค.


์ „๋žต ๋น„๊ต

์ „๋žต์ฝ๊ธฐ ์„ฑ๋Šฅ์“ฐ๊ธฐ ์„ฑ๋Šฅ์ผ๊ด€์„ฑ์žฅ์•  ๋‚ด์„ฑ
Cache-AsideCache Hit ์‹œ ๋น ๋ฆ„๋ณดํ†ต๋‚ฎ์Œ (Stale ๊ฐ€๋Šฅ)๋†’์Œ
Read-ThroughCache Hit ์‹œ ๋น ๋ฆ„๋ณดํ†ต๋‚ฎ์Œ (Stale ๊ฐ€๋Šฅ)์ค‘๊ฐ„
Write-Through๋น ๋ฆ„๋А๋ฆผ๋†’์Œ์ค‘๊ฐ„
Write-Behind๋น ๋ฆ„๋งค์šฐ ๋น ๋ฆ„์ค‘๊ฐ„๋‚ฎ์Œ
Write-Around์ฒซ ์ฝ๊ธฐ ๋А๋ฆผ๋ณดํ†ต๋ณดํ†ต๋†’์Œ

์บ์‹œ ๋ฌดํšจํ™” (Cache Invalidation)

์บ์‹œ ์ „๋žต์—์„œ ๊ฐ€์žฅ ์–ด๋ ค์šด ๋ถ€๋ถ„์ด ๋ฌดํšจํ™”๋‹ค. ์œ ๋ช…ํ•œ ๋ง์ด ์žˆ๋‹ค.

โ€œThere are only two hard things in Computer Science: cache invalidation and naming things.โ€

TTL (Time-To-Live)

redis.setex("key", 3600, value)  # 1์‹œ๊ฐ„ ํ›„ ์ž๋™ ์‚ญ์ œ

๋‹จ์ˆœํ•˜์ง€๋งŒ TTL ๋งŒ๋ฃŒ ์ „๊นŒ์ง€๋Š” Stale Data๋ฅผ ์„œ๋น™ํ•˜๊ฒŒ ๋œ๋‹ค.

์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ๋ฌดํšจํ™”

DB ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ํ•ด๋‹น ์บ์‹œ ํ‚ค๋ฅผ ์ง์ ‘ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

def update_user(user_id: str, data: dict):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.delete(f"user:{user_id}")  # ์บ์‹œ ์‚ญ์ œ โ†’ ๋‹ค์Œ ์ฝ๊ธฐ ๋•Œ DB์—์„œ ์ฑ„์›Œ์ง

์‚ญ์ œ ํ›„ ๋‹ค์Œ ์ฝ๊ธฐ๊ฐ€ Cache-Aside๋กœ ์ฑ„์›Œ์ง€๋Š” ๊ตฌ์กฐ๋‹ค. ์—…๋ฐ์ดํŠธ์™€ ์‚ญ์ œ ์‚ฌ์ด์— ์งง์€ ์‹œ๊ฐ„ ๋™์•ˆ Stale Data๊ฐ€ ์„œ๋น™๋  ์ˆ˜ ์žˆ๋‹ค.

์บ์‹œ ์Šคํƒฌํ”ผ๋“œ (Cache Stampede) ๋ฌธ์ œ

TTL์ด ๋งŒ๋ฃŒ๋˜๋Š” ์ˆœ๊ฐ„ ๋™์‹œ์— ์ˆ˜๋ฐฑ ๊ฐœ์˜ ์š”์ฒญ์ด Cache Miss๋ฅผ ๋ฐ›์œผ๋ฉด, ๋ชจ๋‘ DB๋ฅผ ๋™์‹œ์— ์กฐํšŒํ•œ๋‹ค. DB์— ๊ฐ‘์ž๊ธฐ ๋ถ€ํ•˜๊ฐ€ ๋ชฐ๋ฆฌ๋Š” ํ˜„์ƒ์ด๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. ๋ฎคํ…์Šค ๋ฝ: ์ฒซ ๋ฒˆ์งธ ์š”์ฒญ๋งŒ DB๋ฅผ ์กฐํšŒํ•˜๊ฒŒ ํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ๋Œ€๊ธฐ
def get_with_lock(key: str):
    cached = redis.get(key)
    if cached:
        return cached

    lock_key = f"lock:{key}"
    if redis.set(lock_key, "1", nx=True, ex=5):  # ๋ฝ ํš๋“
        try:
            value = db.query(...)
            redis.setex(key, 3600, value)
            return value
        finally:
            redis.delete(lock_key)
    else:
        # ๋ฝ ํš๋“ ์‹คํŒจ โ†’ ์ž ์‹œ ๋Œ€๊ธฐ ํ›„ ์žฌ์‹œ๋„
        time.sleep(0.1)
        return get_with_lock(key)
  1. ํ™•๋ฅ ์  ์กฐ๊ธฐ ๊ฐฑ์‹  (Probabilistic Early Expiration): TTL์ด ์™„์ „ํžˆ ๋งŒ๋ฃŒ๋˜๊ธฐ ์ „์— ์ผ๋ถ€ ์š”์ฒญ์—์„œ ๋ฏธ๋ฆฌ ๊ฐฑ์‹ 

Redis ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์„ ํƒ

์บ์‹ฑ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋”ฐ๋ผ Redis ์ž๋ฃŒ๊ตฌ์กฐ๋ฅผ ์ ์ ˆํžˆ ์„ ํƒํ•˜๋ฉด ์„ฑ๋Šฅ์„ ๋” ๋Œ์–ด์˜ฌ๋ฆด ์ˆ˜ ์žˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜คRedis ์ž๋ฃŒ๊ตฌ์กฐ๋ช…๋ น์–ด ์˜ˆ์‹œ
๋‹จ์ˆœ ํ‚ค-๊ฐ’ ์บ์‹ฑStringGET, SETEX
์œ ์ € ์„ธ์…˜, ๊ฐ์ฒด ์บ์‹ฑHashHGETALL, HSET
์ตœ๊ทผ ์กฐํšŒ ๋ชฉ๋กListLPUSH, LRANGE
๋žญํ‚น, ์ •๋ ฌ๋œ ๋ชฉ๋กSorted SetZADD, ZRANGE
์ค‘๋ณต ์ œ๊ฑฐ (๋ฐฉ๋ฌธ์ž ์ˆ˜ ๋“ฑ)Set / HyperLogLogSADD, PFADD

์„ธ์…˜ ์บ์‹ฑ์— Hash๋ฅผ ์“ฐ๋ฉด ํ•„๋“œ ๋‹จ์œ„๋กœ ์—…๋ฐ์ดํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•ด์„œ String๋ณด๋‹ค ํšจ์œจ์ ์ด๋‹ค.

# String: ์ „์ฒด ๊ฐ์ฒด๋ฅผ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”
redis.set("session:abc", json.dumps({"user_id": 1, "role": "admin"}))

# Hash: ํ•„๋“œ๋งŒ ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ
redis.hset("session:abc", "role", "user")  # ๋‹ค๋ฅธ ํ•„๋“œ๋Š” ๊ทธ๋Œ€๋กœ

์บ์‹œ ํžˆํŠธ์œจ ๋ชจ๋‹ˆํ„ฐ๋ง

์บ์‹œ๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜๋Š”์ง€ ํžˆํŠธ์œจ(Hit Rate)์„ ์ง€์†์ ์œผ๋กœ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.

# Redis ํ†ต๊ณ„ ํ™•์ธ
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"

# keyspace_hits: ์บ์‹œ ํžˆํŠธ ํšŸ์ˆ˜
# keyspace_misses: ์บ์‹œ ๋ฏธ์Šค ํšŸ์ˆ˜
# Hit Rate = hits / (hits + misses)

์ผ๋ฐ˜์ ์œผ๋กœ ํžˆํŠธ์œจ์ด 80% ๋ฏธ๋งŒ์ด๋ฉด ์บ์‹ฑ ์ „๋žต์„ ์žฌ๊ฒ€ํ† ํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค.


์ •๋ฆฌ

  • ์ฝ๊ธฐ ์œ„์ฃผ ์„œ๋น„์Šค: Cache-Aside + TTL ๊ธฐ๋ฐ˜ ๋ฌดํšจํ™”๋กœ ์‹œ์ž‘ํ•ด์„œ ํžˆํŠธ์œจ ํ™•์ธ
  • ์“ฐ๊ธฐ ๋นˆ๋„๊ฐ€ ๋‚ฎ๊ณ  ์ผ๊ด€์„ฑ์ด ์ค‘์š”: Write-Through
  • ์“ฐ๊ธฐ๊ฐ€ ๋งŽ๊ณ  ์†๋„๊ฐ€ ์šฐ์„ : Write-Behind (๋‹จ, ๋ฐ์ดํ„ฐ ์œ ์‹ค ์œ„ํ—˜ ๊ฐ์ˆ˜)
  • ์บ์‹œ ์Šคํƒฌํ”ผ๋“œ ๋ฐฉ์ง€: ๋ฎคํ…์Šค ๋ฝ ๋˜๋Š” ํ™•๋ฅ ์  ์กฐ๊ธฐ ๊ฐฑ์‹ 
  • ๋ชจ๋‹ˆํ„ฐ๋ง: ํžˆํŠธ์œจ ์ถ”์  ํ•„์ˆ˜, 70~80% ๋ฏธ๋งŒ์ด๋ฉด ์ „๋žต ์žฌ๊ฒ€ํ†