22. 길드 임원 강등의 Write Skew와 재시도/캐시 결함 (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 길드 서버의 "임원(officer) 관리" 서비스다. 테이블은
// guild_members(guild_id BIGINT, member_id BIGINT, role VARCHAR(16), ...)
// role 은 'master' | 'officer' | 'member'
//
// 운영 규칙(불변식)
// I1. 임원은 항상 최소 1명 남아야 한다. 0명이 되면 길드 창고와 가입 승인 큐를
// 아무도 열 수 없어 길드가 사실상 잠긴다.
// I2. 임원은 최대 5명까지만 임명할 수 있다.
//
// 강등(자진 사임)과 임명은 클라이언트 요청으로 들어오며, 여러 임원이 같은 순간에
// 사임 버튼을 누르는 일이 잦다(길드 해체 직전, 대규모 길드 이동 시). DB 는
// SQL Server 이고 스냅샷 격리를 쓴다. 길드 정보는 조회 부하를 줄이려고 프로세스
// 메모리에 캐시되어 있고, 길드 목록 UI 와 권한 체크가 이 캐시를 읽는다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 리뷰하고, (A)/(B) 마커 지점을 중심으로 무엇이 잘못되었는지,
// 어떤 상황에서 I1/I2 가 깨지는지, 어떻게 고칠지 설명하라.
// ============================================================================
using System;
using System.Collections.Concurrent;
using System.Data;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
namespace GuildService.Persistence
{
public sealed class GuildSummary
{
public long GuildId; public int OfficerCount; public int MemberCount; public DateTime UpdatedUtc;
}
public sealed class GuildCache
{
private readonly ConcurrentDictionary<long, GuildSummary> _map = new();
public GuildSummary Get(long g) => _map.GetOrAdd(g, id => new GuildSummary { GuildId = id });
public void SetOfficerCount(long guildId, int count)
{
var s = Get(guildId);
s.OfficerCount = count; s.UpdatedUtc = DateTime.UtcNow;
}
}
public interface ILogger { void Warn(string m); void Error(string m, Exception e); }
public sealed class GuildOfficerService
{
private const string ConnStr = "Server=guild-db;Database=game;Integrated Security=true;";
private const int MaxOfficers = 5;
private readonly GuildCache _cache;
private readonly ILogger _log;
public GuildOfficerService(GuildCache cache, ILogger log) { _cache = cache; _log = log; }
public async Task<bool> DemoteSelfAsync(long guildId, long memberId)
{
var conn = new SqlConnection(ConnStr); // (B)
await conn.OpenAsync();
var tx = conn.BeginTransaction(IsolationLevel.Snapshot);
try
{
int officerCount;
using (var cmd = new SqlCommand(
"SELECT COUNT(*) FROM guild_members WHERE guild_id=@g AND role='officer'",
conn, tx))
{
cmd.Parameters.AddWithValue("@g", guildId);
officerCount = (int)await cmd.ExecuteScalarAsync();
}
if (officerCount < 2) // (A)
{
tx.Rollback();
conn.Close();
return false;
}
_cache.SetOfficerCount(guildId, officerCount - 1); // (B)
using (var upd = new SqlCommand(
"UPDATE guild_members SET role='member' " +
"WHERE guild_id=@g AND member_id=@m AND role='officer'",
conn, tx))
{
upd.Parameters.AddWithValue("@g", guildId);
upd.Parameters.AddWithValue("@m", memberId);
await upd.ExecuteNonQueryAsync(); // (A)
}
tx.Commit();
conn.Close();
return true;
}
catch (SqlException ex) // (B)
{
tx.Rollback();
_log.Error($"demote failed guild={guildId} member={memberId}", ex);
return false;
}
}
public async Task<bool> PromoteWithRetryAsync(long guildId, long memberId)
{
int cachedOfficerCount = _cache.Get(guildId).OfficerCount; // (B)
for (int attempt = 0; attempt < 3; attempt++)
{
if (cachedOfficerCount >= MaxOfficers) // (A)
return false;
var conn = new SqlConnection(ConnStr);
await conn.OpenAsync();
var tx = conn.BeginTransaction(IsolationLevel.Snapshot);
try
{
using (var upd = new SqlCommand(
"UPDATE guild_members SET role='officer' " +
"WHERE guild_id=@g AND member_id=@m AND role='member'",
conn, tx))
{
upd.Parameters.AddWithValue("@g", guildId);
upd.Parameters.AddWithValue("@m", memberId);
await upd.ExecuteNonQueryAsync();
}
tx.Commit();
_cache.SetOfficerCount(guildId, cachedOfficerCount + 1);
conn.Close();
return true;
}
catch (Exception ex)
{
tx.Rollback();
_log.Warn($"promote retry {attempt}: {ex.Message}"); // (B)
}
}
return false;
}
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 길드 서버의 "임원(officer) 관리" 서비스다. 테이블은
// guild_members(guild_id BIGINT, member_id BIGINT, role VARCHAR(16), ...)
// role 은 'master' | 'officer' | 'member'
//
// 불변식: I1 임원은 항상 최소 1명 남아야 한다(0명이면 길드 창고와 가입 승인 큐를
// 아무도 열 수 없어 길드가 잠긴다). I2 임원은 최대 5명.
//
// 강등(자진 사임)과 임명은 클라이언트 요청으로 들어오며, 여러 임원이 같은 순간에
// 사임 버튼을 누르는 일이 잦다. DB 는 스냅샷 격리를 쓴다. 길드 정보는 프로세스
// 메모리에 캐시되어 있고 길드 목록 UI 와 권한 체크가 이 캐시를 읽는다. 아래
// DbConn/ConnectionPool 은 실제 드라이버를 대신하는 얇은 인터페이스이며,
// DbError::code 는 40001(직렬화 실패), 1205(데드락), 그 외 일반 오류를 뜻한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 리뷰하고, (A)/(B) 마커 지점을 중심으로 무엇이 잘못되었는지,
// 어떤 상황에서 I1/I2 가 깨지는지, 어떻게 고칠지 설명하라.
// ============================================================================
#include <cstdint>
#include <initializer_list>
#include <mutex>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
namespace guild {
// ---------------------------- 얇은 DB 인터페이스 ----------------------------
struct Param {
enum class Kind { Int, Text };
Kind kind; int64_t i = 0; std::string s;
Param(int64_t v) : kind(Kind::Int), i(v) {}
Param(std::string v) : kind(Kind::Text), s(std::move(v)) {}
};
struct Row {
std::vector<std::string> cols;
int64_t AsInt(size_t i) const { return std::stoll(cols.at(i)); }
};
struct ResultSet { std::vector<Row> rows; };
enum class Isolation { ReadCommitted, RepeatableRead, Snapshot, Serializable };
struct DbError : std::runtime_error {
int code; // 40001 / 1205 / etc.
DbError(int c, const std::string& m) : std::runtime_error(m), code(c) {}
};
struct DbConn {
ResultSet Query(const std::string& sql, std::initializer_list<Param> args);
int Execute(const std::string& sql, std::initializer_list<Param> args);
void Begin(Isolation iso);
void Commit();
void Rollback();
bool InTransaction() const;
};
class ConnectionPool {
public:
DbConn* Acquire();
void Release(DbConn*);
};
// --------------------------------------------------------------------------
struct GuildSummary { int64_t guild_id = 0; int officer_count = 0; };
class GuildCache {
public:
GuildSummary& Get(int64_t g) { std::lock_guard<std::mutex> lk(m_); return map_[g]; }
void SetOfficerCount(int64_t g, int c) {
std::lock_guard<std::mutex> lk(m_);
map_[g].guild_id = g;
map_[g].officer_count = c;
}
private:
std::mutex m_;
std::unordered_map<int64_t, GuildSummary> map_;
};
class GuildOfficerService {
public:
GuildOfficerService(ConnectionPool& p, GuildCache& c) : pool_(p), cache_(c) {}
bool DemoteSelf(int64_t guild_id, int64_t member_id) {
DbConn* conn = pool_.Acquire(); // (B)
conn->Begin(Isolation::Snapshot);
ResultSet rs = conn->Query(
"SELECT COUNT(*) FROM guild_members WHERE guild_id=? AND role='officer'",
{Param(guild_id)});
const int officer_count = static_cast<int>(rs.rows.at(0).AsInt(0));
if (officer_count < 2) { // (A)
conn->Rollback();
pool_.Release(conn);
return false;
}
cache_.SetOfficerCount(guild_id, officer_count - 1); // (B)
const std::string sql =
"UPDATE guild_members SET role='member' WHERE guild_id=" +
std::to_string(guild_id) + " AND member_id=" +
std::to_string(member_id) + " AND role='officer'";
conn->Execute(sql, {}); // (A)
conn->Commit();
pool_.Release(conn);
return true;
}
bool PromoteWithRetry(int64_t guild_id, int64_t member_id, const std::string& note) {
int cached_officers = cache_.Get(guild_id).officer_count; // (B)
for (int attempt = 0; attempt < 3; ++attempt) {
if (cached_officers >= kMaxOfficers) // (A)
return false;
DbConn* conn = pool_.Acquire();
try {
conn->Begin(Isolation::Snapshot);
conn->Execute(
"UPDATE guild_members SET role='officer' "
"WHERE guild_id=? AND member_id=? AND role='member'",
{Param(guild_id), Param(member_id)});
conn->Execute(
"INSERT INTO guild_log(guild_id, note) VALUES(" +
std::to_string(guild_id) + ", '" + note + "')", // (B)
{});
conn->Commit();
cache_.SetOfficerCount(guild_id, cached_officers + 1);
pool_.Release(conn);
return true;
} catch (const std::exception&) { // (B)
conn->Rollback();
pool_.Release(conn);
}
}
return false;
}
private:
static constexpr int kMaxOfficers = 5;
ConnectionPool& pool_;
GuildCache& cache_;
};
} // namespace guild 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.