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

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