1. 글로벌 거래소를 여러 지역 서버가 공유할 때의 정합성

난이도 최상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 글로벌 거래소를 여러 지역 서버가 공유할 때의 정합성
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 / 분산):
//   - 하나의 "글로벌 거래소"를 여러 지역(region) 게임서버가 공유한다. 같은 매물(listing)을
//     서로 다른 지역의 플레이어가 거의 동시에 구매할 수 있다.
//   - 각 지역 서버는 지연을 줄이려고 매물 재고(remainingQty)를 로컬에 캐시해 두고, 구매
//     가능 여부를 로컬 캐시로 먼저 판단한 뒤 중앙 거래소(CentralExchange)에 차감을 요청한다.
//   - 중앙 거래소는 모든 지역의 차감을 받는 단일 권위(authority)다.
//
// 요구사항:
//   - 한 매물의 재고는 여러 지역에서 동시에 구매가 들어와도 절대 초과 판매(oversell)되면 안 된다.
//   - "구매 가능 검사 → 재고 차감 → 대금/아이템 지급"은 매물 단위로 원자적이어야 한다.
//   - 네트워크 지연/지역 캐시 불일치가 있어도 총 판매량은 재고를 넘지 않아야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 oversell·구매 유실·이중 차감이 분산 환경에서
//   발생하는지 설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

// 중앙 거래소(단일 권위) — 모든 지역이 RPC 로 호출한다고 가정
public class CentralExchange
{
    private readonly Dictionary<long, int> _remaining = new();   // listingId -> 남은 수량

    public int GetRemaining(long listingId)
        => _remaining.TryGetValue(listingId, out var q) ? q : 0;

    // 재고 차감 요청 (여러 지역에서 동시 호출)
    public bool Decrement(long listingId, int qty)
    {
        int cur = _remaining.TryGetValue(listingId, out var q) ? q : 0;
        // (A) 검사와 차감이 분리된 비원자 연산 (네트워크 RPC 경계 너머 동시 호출)
        if (cur < qty) return false;
        _remaining[listingId] = cur - qty;
        return true;
    }
}

// 지역 서버 — 로컬 재고 캐시로 먼저 판단
public class RegionExchangeProxy
{
    private readonly CentralExchange _central;
    private readonly Dictionary<long, int> _localCache = new();   // listingId -> 캐시된 남은 수량

    public RegionExchangeProxy(CentralExchange central) { _central = central; }

    public bool Buy(long buyerId, long listingId, int qty)
    {
        // (B) 로컬 캐시 재고로 구매 가능 판단
        int cached = _localCache.TryGetValue(listingId, out var q) ? q : RefreshCache(listingId);
        if (cached < qty) return false;

        // (C) 캐시를 먼저 줄이고 대금/아이템 지급까지 진행한 뒤, 중앙엔 사후 차감 요청
        _localCache[listingId] = cached - qty;
        GrantItemAndCharge(buyerId, listingId, qty);

        _central.Decrement(listingId, qty);   // (C) 반환값 무시 — 실패해도 이미 지급됨
        return true;
    }

    private int RefreshCache(long listingId)
    {
        int r = _central.GetRemaining(listingId);
        _localCache[listingId] = r;
        return r;
    }

    private void GrantItemAndCharge(long buyerId, long listingId, int qty) { /* 지급/과금 */ }
}
내 리뷰 · C#
내 답안 · 자동 저장

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