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#
내 답안 · 자동 저장

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