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++
// ─────────────────────────────────────────────────────────────
// 시나리오: 플레이어 재화(gold)는 DB의 players 테이블에 영속된다. 상점 구매·
// 스킬 비용 등으로 골드를 차감할 때 spendGold가 호출된다. 같은 플레이어에
// 대해 여러 요청(여러 워커 스레드/연결)이 거의 동시에 들어올 수 있다.
// 트랜잭션은 기본 격리수준(대개 READ COMMITTED)으로 연다.
//
// 요구사항:
// - 골드는 음수가 될 수 없고, 차감 총액과 잔액 감소가 정확히 일치해야 한다.
//
// 과제: (A)(B) 지점을 검토하라. 동일 플레이어에 대해 spendGold가 동시에
// 두 번 실행될 때 잔액이 어떻게 되는지 구체적 수치로 설명하고,
// 기본 격리수준에서 막히는지, 어떻게 고칠지 제시하라.
// ─────────────────────────────────────────────────────────────
#include <cstdint>
#include <string>
namespace persistence {
// DB 클라이언트 추상화(본문은 드라이버 구현 — 여기선 선언만)
struct DbConn {
void begin();
void commit();
void rollback();
int64_t querySingleInt(const std::string& sql, int64_t param); // 단일 정수 결과
int execute(const std::string& sql, int64_t a, int64_t b); // 영향 행 수 반환
};
bool spendGold(DbConn& db, int64_t playerId, int64_t cost) {
db.begin(); // 기본 격리수준(대개 READ COMMITTED)
// (A)
int64_t balance = db.querySingleInt(
"SELECT gold FROM players WHERE id = ?", playerId);
if (balance < cost) {
db.rollback();
return false;
}
int64_t newBalance = balance - cost;
// (B)
db.execute("UPDATE players SET gold = ? WHERE id = ?", newBalance, playerId);
db.commit();
return true;
}
} // namespace persistence 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.