20. 트랜잭션 재시도 범위 안의 비트랜잭션 부수효과 반복 실행 (이중 알림·이중 집계) (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: 캐시 상점 구매 처리. DB 트랜잭션으로 재화를 차감하고 아이템을 지급한다.
// 동시성 충돌(직렬화 실패/데드락)에 대비해 트랜잭션을 재시도하는 표준 패턴을 쓴다.
// 구매가 성공하면 (1) 인메모리 일일 매출 카운터를 올리고, (2) 유저에게 우편으로
// "구매 완료" 알림을 보낸다.
//
// 요구사항:
// 1) 재화 차감/아이템 지급은 원자적이어야 한다.
// 2) 동시성 충돌 시 재시도로 최종 일관성을 확보한다.
// 3) 구매 1건당 매출 카운트는 정확히 +1, 알림 우편은 정확히 1통이어야 한다
// (재시도가 몇 번 일어나든).
//
// 아래 코드를 동시성 충돌로 재시도가 발생하는 상황에서 검토하고,
// (A)(B) 지점을 중심으로 문제를 진단·수정하라.
using System;
public interface IDbConnection
{
IDbTransaction BeginTransaction();
void Execute(IDbTransaction tx, string sql, params object[] args);
}
public interface IDbTransaction : IDisposable { void Commit(); void Rollback(); }
public class DbSerializationException : Exception { }
public interface IMailService { void Send(int userId, string subject); }
public class SalesMetrics { public long PurchasesToday; }
public class PurchaseService
{
private readonly IDbConnection _db;
private readonly IMailService _mail;
private readonly SalesMetrics _metrics;
public PurchaseService(IDbConnection db, IMailService mail, SalesMetrics metrics)
{ _db = db; _mail = mail; _metrics = metrics; }
public void Purchase(int userId, int itemId, long price)
{
const int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
using var tx = _db.BeginTransaction();
_db.Execute(tx, "UPDATE wallet SET gold = gold - @p WHERE user_id=@u AND gold>=@p", price, userId);
_db.Execute(tx, "INSERT INTO inventory(user_id, item_id, qty) VALUES(@u,@i,1)", userId, itemId);
// (A) 구매 성공 처리: 매출 집계 + 알림 발송
_metrics.PurchasesToday++;
_mail.Send(userId, "구매 완료");
tx.Commit();
return;
}
catch (DbSerializationException)
{
// (B) 동시성 충돌 → 재시도
if (attempt == maxRetries) throw;
}
}
}
} 결함 코드 · C++
// 시나리오: 캐시 상점 구매 처리 (C++ 서버). DB 트랜잭션으로 재화를 차감하고
// 아이템을 지급한다. 동시성 충돌(직렬화 실패/데드락)에 대비해 트랜잭션을 재시도한다.
// 구매가 성공하면 (1) 인메모리 일일 매출 카운터를 올리고, (2) 유저에게 우편으로
// "구매 완료" 알림을 보낸다.
//
// 요구사항:
// 1) 재화 차감/아이템 지급은 원자적이어야 한다.
// 2) 동시성 충돌 시 재시도로 최종 일관성을 확보한다.
// 3) 구매 1건당 매출 카운트는 정확히 +1, 알림 우편은 정확히 1통이어야 한다
// (재시도가 몇 번 일어나든).
//
// 아래 코드를 동시성 충돌로 재시도가 발생하는 상황에서 검토하고,
// (A)(B) 지점을 중심으로 문제를 진단·수정하라.
#include <atomic>
#include <cstdint>
#include <stdexcept>
#include <string>
struct DbSerializationError : std::runtime_error {
DbSerializationError() : std::runtime_error("serialization failure") {}
};
class DbTransaction {
public:
void exec(const std::string& sql); // 실패 시 DbSerializationError 던질 수 있음
void commit(); // 실패 시 DbSerializationError 던질 수 있음
void rollback() noexcept;
};
class Db {
public:
DbTransaction begin();
};
class MailService {
public:
void send(int userId, const std::string& subject);
};
class PurchaseService {
public:
PurchaseService(Db& db, MailService& mail, std::atomic<int64_t>& purchasesToday)
: db_(db), mail_(mail), purchasesToday_(purchasesToday) {}
void purchase(int userId, int itemId, int64_t price) {
const int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; ++attempt) {
try {
DbTransaction tx = db_.begin();
tx.exec("UPDATE wallet SET gold = gold - " + std::to_string(price) +
" WHERE user_id=" + std::to_string(userId));
tx.exec("INSERT INTO inventory(user_id,item_id,qty) VALUES(" +
std::to_string(userId) + "," + std::to_string(itemId) + ",1)");
// (A) 구매 성공 처리: 매출 집계 + 알림 발송
purchasesToday_.fetch_add(1, std::memory_order_relaxed);
mail_.send(userId, "구매 완료");
tx.commit();
return;
} catch (const DbSerializationError&) {
// (B) 동시성 충돌 → 재시도
if (attempt == maxRetries) throw;
}
}
}
private:
Db& db_;
MailService& mail_;
std::atomic<int64_t>& purchasesToday_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.