10. Cache-Aside 읽기 재채움 vs 쓰기 무효화 경합(stale 부활) (C#)

난이도 최상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// 시나리오: 캐시 어사이드(Cache-Aside / lazy loading) 패턴으로 플레이어 프로필을 읽는다.
// 읽기는 캐시(Redis 류)를 먼저 보고, 미스면 DB 에서 읽어 캐시에 채운다.
// 쓰기는 DB 를 갱신한 뒤 캐시를 무효화(삭제)한다.
//
// 요구사항:
//   - GetProfile: 캐시 히트면 그 값을, 미스면 DB 값을 읽어 캐시에 채우고 반환한다.
//   - UpdateProfile: DB 를 갱신하고 캐시를 무효화한다.
//   - 읽기와 쓰기는 서로 다른 요청 스레드에서 동시에 들어올 수 있다.
//   - 캐시는 결국 DB 와 수렴해야 한다(오래도록 옛 값을 주면 안 됨).
//
// 과제: (A)(B) 지점을 검토하라. 읽기와 쓰기가 특정 순서로 겹치면 캐시에 옛 값이
//       "부활"해 TTL 이 만료될 때까지 남을 수 있다. 어떤 인터리빙에서 그런지 밝히고 고쳐라.

using System;

public interface ICache
{
    bool TryGet(string key, out string val);
    void Set(string key, string val, TimeSpan ttl);
    void Delete(string key);
}

public interface IDb
{
    string Read(string key);
    void Write(string key, string val);
}

public sealed class ProfileService
{
    private readonly ICache _cache;
    private readonly IDb _db;
    private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(10);

    public ProfileService(ICache cache, IDb db) { _cache = cache; _db = db; }

    public string GetProfile(string playerId)
    {
        if (_cache.TryGet(playerId, out var cached))
            return cached;

        string v = _db.Read(playerId);          // 캐시 미스 → DB 조회
        _cache.Set(playerId, v, Ttl);           // (A) 읽은 값을 캐시에 채움
        return v;
    }

    public void UpdateProfile(string playerId, string val)
    {
        _db.Write(playerId, val);               // DB 먼저 갱신
        _cache.Delete(playerId);                // (B) 그 다음 캐시 무효화
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.