25. READ COMMITTED 하 비반복 읽기로 인한 판정-갱신 불일치 (C#)

난이도 하 해설 보기 →

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

결함 코드 · C#
// 시나리오: 인게임 상점에서 재화(캐시)로 아이템을 구매하는 서버 처리.
//   - 결제는 단일 DB 트랜잭션 안에서 이루어진다.
//   - 개발자는 "안전하게" 하려고, 먼저 잔액을 읽어 구매 가능 여부를 검사하고(1차 읽기),
//     이후 최종 반영 직전에 잔액을 한 번 더 읽어(2차 읽기) 그 값에서 가격을 뺀
//     "새 잔액"을 절대값으로 UPDATE 한다.
//   - 트랜잭션 격리수준은 기본값(READ COMMITTED)이다.
//   - 같은 계정에 대해 구매 요청이 동시에 여러 건(다른 기기/중복 클릭) 들어올 수 있다.
//
// 요구사항:
//   1) 잔액은 절대 음수가 되지 않는다(가진 것보다 많이 쓸 수 없다).
//   2) 동시에 여러 구매가 커밋돼도 각 구매 금액의 합만큼만 잔액이 줄어야 한다(유실 없음).
//
// 과제: 아래 Purchase 를 검토하여, 같은 계정에 구매가 동시에 들어올 때
//       요구사항 1~2가 깨지는 지점을 (A)(B) 마커로 진단하고,
//       정확한 수정안과 더 나은 설계를 제시하라.

using System.Data;

public sealed class ShopService
{
    private readonly IDbConnectionFactory _dbf;
    public ShopService(IDbConnectionFactory dbf) { _dbf = dbf; }

    public bool Purchase(long accountId, int itemId, long price)
    {
        using IDbConnection db = _dbf.Open();
        using IDbTransaction tx = db.BeginTransaction(IsolationLevel.ReadCommitted);

        // 1차 읽기: 구매 가능 여부 검사
        long balance = ScalarLong(db, tx,
            "SELECT balance FROM wallet WHERE account_id=@a", accountId);   // (A)
        if (balance < price)
        {
            tx.Rollback();
            return false;
        }

        GrantItem(db, tx, accountId, itemId);   // 아이템 지급(같은 트랜잭션)

        // 2차 읽기: 최종 반영 직전에 잔액을 다시 읽어 새 잔액을 계산
        long balanceNow = ScalarLong(db, tx,
            "SELECT balance FROM wallet WHERE account_id=@a", accountId);
        long newBalance = balanceNow - price;

        Exec(db, tx,
            "UPDATE wallet SET balance=@b WHERE account_id=@a", newBalance, accountId); // (B)

        tx.Commit();
        return true;
    }

    // --- 이하 헬퍼(구현 생략) ---
    private static long ScalarLong(IDbConnection db, IDbTransaction tx, string sql, long a) => 0;
    private static void Exec(IDbConnection db, IDbTransaction tx, string sql, long b, long a) { }
    private static void GrantItem(IDbConnection db, IDbTransaction tx, long a, int item) { }
}

public interface IDbConnectionFactory { IDbConnection Open(); }
내 리뷰 · C#
내 답안 · 자동 저장

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