8. DB read-modify-write의 갱신 유실(lost update) (C#)

난이도 상 해설 보기 →

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

결함 코드 · C#
// ─────────────────────────────────────────────────────────────
// 시나리오: 플레이어 재화(gold)는 DB의 players 테이블에 영속된다.
//   상점 구매·스킬 비용 등으로 골드를 차감할 때 SpendGold가 호출된다.
//   같은 플레이어에 대해 여러 요청(다른 기기/매크로/연속 클릭, 여러 워커
//   스레드)이 거의 동시에 들어올 수 있다. DB 연결은 기본 격리수준
//   (대개 READ COMMITTED)으로 트랜잭션을 연다.
//
// 요구사항:
//   - 골드는 음수가 될 수 없고, "차감한 총액"과 "잔액 감소"가 정확히 일치해야
//     한다(차감이 유실되거나 이중 적용되면 안 됨).
//
// 과제: (A)(B) 지점을 검토하라. 동일 플레이어에 대해 SpendGold가 동시에
//        두 번 실행될 때 잔액이 어떻게 되는지 구체적 수치로 설명하고,
//        기본 격리수준에서 이 문제가 막히는지, 어떻게 고칠지 제시하라.
// ─────────────────────────────────────────────────────────────
using System.Data;
using System.Threading.Tasks;

namespace Persistence
{
    public sealed class WalletRepository
    {
        private readonly IDbConnectionFactory _factory;
        public WalletRepository(IDbConnectionFactory f) => _factory = f;

        public async Task<bool> SpendGold(long playerId, long cost)
        {
            using IDbConnection conn = _factory.Open();
            using IDbTransaction tx = conn.BeginTransaction();   // 기본 격리수준

            // (A)
            long balance = await Db.QuerySingleAsync<long>(conn, tx,
                "SELECT gold FROM players WHERE id = @id",
                new { id = playerId });

            if (balance < cost)
            {
                tx.Rollback();
                return false;
            }

            long newBalance = balance - cost;

            // (B)
            await Db.ExecuteAsync(conn, tx,
                "UPDATE players SET gold = @g WHERE id = @id",
                new { g = newBalance, id = playerId });

            tx.Commit();
            return true;
        }
    }

    // (인프라 인터페이스 — 본문 생략)
    public interface IDbConnectionFactory { IDbConnection Open(); }
    public static class Db
    {
        public static Task<T> QuerySingleAsync<T>(IDbConnection c, IDbTransaction t, string sql, object p) => Task.FromResult(default(T)!);
        public static Task<int> ExecuteAsync(IDbConnection c, IDbTransaction t, string sql, object p) => Task.FromResult(0);
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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