16. 단일 JSON 컬럼 부분 갱신의 read-modify-write 유실 (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: 캐릭터 설정/진행도를 단일 JSON 컬럼에 저장 (C#)
//
// characters 테이블은 progress 라는 JSON(text) 컬럼 하나에 여러 하위 값을 담는다.
// 예: {"gold":100,"quests":{...},"settings":{...},"lastZone":"a"}.
// 서로 다른 기능(상점, 퀘스트, 설정)이 각자 자기 하위 값만 갱신한다.
//
// 요구사항:
// - 각 기능은 자신이 담당하는 하위 필드만 바꾸고, 다른 필드는 보존해야 한다.
// - 여러 요청이 동시에 서로 다른 하위 필드를 갱신해도 서로의 변경을 잃지 않아야 한다.
//
// 아래는 골드 차감 처리다. 표시된 지점 (A)(B)(C) 를 검토하라.
using System.Data.Common;
using System.Text.Json;
using System.Threading.Tasks;
public sealed class ProgressStore
{
private readonly DbConnection _db;
public ProgressStore(DbConnection db) { _db = db; }
public async Task SpendGoldAsync(long charId, int amount)
{
// (A) 현재 JSON 전체를 읽는다
string json = (string)(await ExecScalarAsync(
"SELECT progress FROM characters WHERE char_id=@id", charId))!;
var doc = JsonSerializer.Deserialize<JsonElement>(json);
var dict = new System.Collections.Generic.Dictionary<string, JsonElement>();
foreach (var p in doc.EnumerateObject()) dict[p.Name] = p.Value;
int gold = dict["gold"].GetInt32();
gold -= amount; // (B) 애플리케이션에서 수정
// (C) 수정한 필드만 바꾼 뒤 JSON 전체를 다시 쓴다
using var ms = new System.IO.MemoryStream();
using (var w = new Utf8JsonWriter(ms))
{
w.WriteStartObject();
foreach (var kv in dict)
{
if (kv.Key == "gold") w.WriteNumber("gold", gold);
else { w.WritePropertyName(kv.Key); kv.Value.WriteTo(w); }
}
w.WriteEndObject();
}
string updated = System.Text.Encoding.UTF8.GetString(ms.ToArray());
await ExecAsync("UPDATE characters SET progress=@p WHERE char_id=@id", updated, charId);
}
private Task<object?> ExecScalarAsync(string sql, long id)
=> Task.FromResult<object?>("{\"gold\":100,\"lastZone\":\"a\"}");
private Task ExecAsync(string sql, string p, long id) => Task.CompletedTask;
} 결함 코드 · C++
// 시나리오: 캐릭터 설정/진행도를 단일 JSON(blob) 컬럼에 저장 (C++)
//
// characters 테이블은 progress 라는 text 컬럼 하나에 여러 하위 값을 담는다.
// 예: {"gold":100,"quests":...,"settings":...}. 기능별로 자기 하위 값만 갱신한다.
//
// 요구사항:
// - 각 기능은 자신의 하위 필드만 바꾸고 다른 필드는 보존한다.
// - 동시에 서로 다른 하위 필드를 갱신해도 서로의 변경을 잃지 않는다.
//
// db_->QueryScalar / db_->Exec 는 각각 별도의 DB 왕복이다.
// 아래 골드 차감 처리를 검토하라. 표시된 지점 (A)(B)(C).
#include <string>
#include <cstdint>
// 아주 단순화한 JSON 헬퍼(실무의 라이브러리라고 가정)
struct Json {
static Json Parse(const std::string& s);
int GetInt(const std::string& key) const;
void SetInt(const std::string& key, int v);
std::string Dump() const;
};
struct IDb {
virtual std::string QueryScalar(const std::string& sql, std::int64_t id) = 0;
virtual void Exec(const std::string& sql, const std::string& p, std::int64_t id) = 0;
virtual ~IDb() = default;
};
class ProgressStore {
public:
explicit ProgressStore(IDb* db) : db_(db) {}
void SpendGold(std::int64_t charId, int amount) {
// (A) 현재 JSON 전체를 읽는다
std::string raw = db_->QueryScalar(
"SELECT progress FROM characters WHERE char_id=?", charId);
Json doc = Json::Parse(raw);
int gold = doc.GetInt("gold");
gold -= amount; // (B) 애플리케이션에서 수정
doc.SetInt("gold", gold);
// (C) 수정한 문서 전체를 다시 쓴다
db_->Exec("UPDATE characters SET progress=? WHERE char_id=?", doc.Dump(), charId);
}
private:
IDb* db_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.