2. 보상 지급을 여러 독립 커밋으로 쪼갠 부분 커밋/이중 지급
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 보상 지급을 여러 독립 커밋으로 쪼갠 부분 커밋/이중 지급
// ----------------------------------------------------------------------------
// 시나리오 (영속화/DB · 서버-클라):
// - 퀘스트 보상 지급은 DB 에 세 가지를 남긴다: ① 인벤토리에 아이템 추가,
// ② 지갑에 골드 가산, ③ 수령 로그 기록(중복 수령 방지용).
// - 아래 IDb.Execute 는 호출 한 번이 곧바로 커밋되는 autocommit 모델이다(각 문장이
// 독립 트랜잭션). 서버는 처리 중 예외/크래시/배포로 중간에 멈출 수 있다.
//
// 요구사항:
// - 한 번의 보상 지급은 "전부 반영" 또는 "전혀 반영 안 됨" 이어야 한다(원자성).
// - 같은 (playerId, questId) 보상은 두 번 지급되면 안 된다(멱등성). 재시도/재기동에도.
//
// 과제:
// 이 지급 로직의 잠재적 문제를 모두 찾고, 중간 단계에서 실패하거나 재시도될 때
// 어떤 부분 상태/이중 지급이 생기는지 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
public interface IDb
{
void Execute(string sql, params object[] args); // 각 호출이 즉시 커밋(autocommit)
int QueryInt(string sql, params object[] args);
}
public class RewardService
{
private readonly IDb _db;
public RewardService(IDb db) { _db = db; }
public void GrantQuestReward(long playerId, long questId, long itemId, int gold)
{
int already = _db.QueryInt(
"SELECT COUNT(*) FROM quest_reward_log WHERE player_id=? AND quest_id=?",
playerId, questId); // (A)
if (already > 0) return;
_db.Execute("INSERT INTO inventory(player_id,item_id,count) VALUES(?,?,1)",
playerId, itemId); // (B)
_db.Execute("UPDATE wallet SET gold = gold + ? WHERE player_id=?",
gold, playerId); // (C)
_db.Execute("INSERT INTO quest_reward_log(player_id,quest_id) VALUES(?,?)",
playerId, questId); // (D)
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 보상 지급을 여러 독립 커밋으로 쪼갠 부분 커밋/이중 지급
// ----------------------------------------------------------------------------
// 시나리오 (영속화/DB · 서버-클라):
// - 퀘스트 보상 지급은 DB 에 세 가지를 남긴다: ① 인벤토리 아이템 추가,
// ② 지갑 골드 가산, ③ 수령 로그(중복 수령 방지용).
// - 아래 IDb::Execute 는 호출 한 번이 곧바로 커밋되는 autocommit 모델이다.
// 각 호출은 예외를 던질 수 있고(연결 끊김/제약 위반 등), 프로세스는 중간에 죽을 수 있다.
//
// 요구사항:
// - 한 번의 보상 지급은 "전부 반영" 또는 "전혀 반영 안 됨" 이어야 한다(원자성).
// - 같은 (playerId, questId) 보상은 두 번 지급되면 안 된다(멱등성). 재시도/재기동에도.
//
// 과제:
// 이 지급 로직의 잠재적 문제를 모두 찾고, 중간 단계가 예외/크래시로 멈출 때 어떤 부분
// 상태/이중 지급이 생기는지 설명하고, 수정안(예외 안전성 포함)과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <string>
struct IDb
{
virtual void Execute(const std::string& sql) = 0; // 각 호출 즉시 커밋(autocommit)
virtual int QueryInt(const std::string& sql) = 0;
virtual ~IDb() = default;
};
class RewardService
{
IDb& db_;
public:
explicit RewardService(IDb& db) : db_(db) {}
void GrantQuestReward(long playerId, long questId, long itemId, int gold)
{
int already = db_.QueryInt(
"SELECT COUNT(*) FROM quest_reward_log WHERE player_id=" +
std::to_string(playerId) + " AND quest_id=" + std::to_string(questId)); // (A)
if (already > 0) return;
db_.Execute("INSERT INTO inventory(player_id,item_id,count) VALUES(" +
std::to_string(playerId) + "," + std::to_string(itemId) + ",1)"); // (B)
db_.Execute("UPDATE wallet SET gold = gold + " + std::to_string(gold) +
" WHERE player_id=" + std::to_string(playerId)); // (C)
db_.Execute("INSERT INTO quest_reward_log(player_id,quest_id) VALUES(" +
std::to_string(playerId) + "," + std::to_string(questId) + ")"); // (D)
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.