4. 지역 간 캐시 무효화 전파의 순서·유실·재채움 경합

난이도 최상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 지역 서버 간 캐시 무효화 전파 (pub/sub)
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 · 분산/캐시 일관성):
//   - 플레이어 프로필을 여러 지역(region) 서버가 각자 로컬 캐시에 들고 읽는다.
//   - 한 지역에서 프로필이 바뀌면, 무효화 메시지를 pub/sub 으로 발행해 다른 지역들이
//     자기 로컬 캐시에서 그 항목을 지우게 한다(다음 읽기 때 DB 에서 다시 채움).
//   - DB 는 지역별 읽기 복제본을 두고 있어 복제에 지연이 있을 수 있다.
//   - pub/sub 은 best-effort(메시지 유실/순서 역전 가능).
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 한 지역에서 프로필을 바꾼 직후 다른 지역의 캐시가
//   어떤 값을 갖게 되는지(특히 무효화·재읽기·캐시 채움의 순서) 설명하라. 무효화 메시지가
//   유실되거나 순서가 뒤바뀌면 어떻게 되는지도 논하라. 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System.Threading.Tasks;

public class Profile { public string Name; /* ... */ }

public interface ILocalCache
{
    bool TryGet(long playerId, out Profile p);
    void Set(long playerId, Profile p);
    void Remove(long playerId);
}

public interface IProfileDb
{
    Task UpdateProfileAsync(long playerId, Profile p);   // 마스터에 쓰기(커밋)
    Task<Profile> GetProfileAsync(long playerId);        // 지역 복제본에서 읽기
}

public interface IPubSub
{
    void Publish(string channel, long playerId);
    void Subscribe(string channel, System.Action<long> handler);
}

public class RegionCache
{
    private readonly ILocalCache _local;
    private readonly IProfileDb  _db;
    private readonly IPubSub     _bus;

    public RegionCache(ILocalCache local, IProfileDb db, IPubSub bus)
    {
        _local = local; _db = db; _bus = bus;
        _bus.Subscribe("profile.invalidate", OnInvalidate);
    }

    public async Task UpdateProfileAsync(long playerId, Profile p)
    {
        _bus.Publish("profile.invalidate", playerId);    // (A) 다른 지역에 무효화 알림
        await _db.UpdateProfileAsync(playerId, p);       // (B) 마스터 커밋
        _local.Remove(playerId);                         // (C) 내 로컬 캐시 제거
    }

    public async Task<Profile> GetProfileAsync(long playerId)
    {
        if (_local.TryGet(playerId, out var hit))        // (D) 로컬 히트
            return hit;

        var fromDb = await _db.GetProfileAsync(playerId);// (E) 복제본에서 읽기
        _local.Set(playerId, fromDb);                    // (F) 캐시 채움
        return fromDb;
    }

    private void OnInvalidate(long playerId)
    {
        _local.Remove(playerId);                         // (G) 무효화 수신 → 제거
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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