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++
// 시나리오: 인게임 상점 재화 구매의 서버 처리 (C++ 서버, DB 트랜잭션).
// - 단일 트랜잭션 안에서 잔액을 1차로 읽어 구매 가능 여부를 검사하고,
// 최종 반영 직전에 2차로 다시 읽어 "새 잔액"을 절대값으로 UPDATE 한다.
// - 격리수준은 READ COMMITTED. 같은 계정에 동시 구매가 들어올 수 있다.
//
// 요구사항:
// 1) 잔액은 음수가 되지 않는다.
// 2) 동시 구매가 커밋돼도 금액 합만큼만 줄어든다(갱신 유실 없음).
//
// 과제: purchase 를 검토하여, 같은 계정에 구매가 동시에 들어올 때 요구사항 1~2가
// 깨지는 지점을 (A)(B) 마커로 진단하고, 정확한 수정안과 더 나은 설계를 제시하라.
//
// 컴파일: g++ -std=c++17 -fsyntax-only problem.cpp
#include <cstdint>
#include <string>
// 가상의 DB 커넥션 추상(구현 생략).
class DbConn {
public:
void begin(const char* isolation); // "READ COMMITTED"
void commit();
void rollback();
std::int64_t scalarInt(const std::string& sql, std::int64_t accountId);
void exec(const std::string& sql, std::int64_t v, std::int64_t accountId);
void grantItem(std::int64_t accountId, int itemId);
};
class ShopService {
public:
explicit ShopService(DbConn* db) : db_(db) {}
bool purchase(std::int64_t accountId, int itemId, std::int64_t price)
{
db_->begin("READ COMMITTED");
// 1차 읽기: 구매 가능 여부 검사
std::int64_t balance =
db_->scalarInt("SELECT balance FROM wallet WHERE account_id=?", accountId); // (A)
if (balance < price) { db_->rollback(); return false; }
db_->grantItem(accountId, itemId);
// 2차 읽기: 최종 반영 직전 재조회 후 새 잔액을 절대값으로 계산
std::int64_t balanceNow =
db_->scalarInt("SELECT balance FROM wallet WHERE account_id=?", accountId);
std::int64_t newBalance = balanceNow - price;
db_->exec("UPDATE wallet SET balance=? WHERE account_id=?", newBalance, accountId); // (B)
db_->commit();
return true;
}
private:
DbConn* db_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.