17. 결제 콜백 → 재화 지급의 멱등성 (C#)

난이도 중 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 결제 콜백 → 재화 지급의 멱등성 (서버-서버)
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 / 결제):
//   - 외부 결제 서버(PG/스토어)가 "구매 완료" 콜백(웹훅)을 게임 서버로 보낸다.
//   - 콜백에는 주문 식별자(transactionId)와 결제 금액이 담긴다.
//   - 게임 서버는 이를 받아 해당 계정에 인게임 재화(예: 캐시/보석)를 지급한다.
//   - 결제 콜백은 at-least-once 다: 네트워크 타임아웃/재시도로 같은 콜백이
//     여러 번, 때로는 거의 동시에 도착할 수 있다.
//   - PG 는 콜백을 받으면 200 OK 를 기대하며, 실패로 보이면 재전송한다.
//
// 요구사항:
//   - 같은 transactionId 의 지급은 정확히 한 번만 일어나야 한다(중복 지급 금지).
//   - 동시에 도착한 중복 콜백에도 한 번만 지급돼야 한다.
//   - 서버 재시작 후 같은 콜백이 다시 와도 두 번 지급되면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 중복 지급/유실이 발생하는지
//   설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class Account
{
    public long Id;
    public long Balance;     // 인게임 재화 잔액
}

public class PaymentCallback
{
    public string TransactionId;
    public long   AccountId;
    public long   Amount;    // 콜백 페이로드에 담긴 지급 금액
}

public class PaymentService
{
    // accountId -> Account
    private readonly Dictionary<long, Account> _accounts;
    // 이미 처리한 transactionId 들
    private readonly HashSet<string> _processed = new HashSet<string>();

    public PaymentService(Dictionary<long, Account> accounts) { _accounts = accounts; }

    // 결제 서버가 콜백할 때마다 호출(중복/동시 호출 가능). 처리 성공 시 true 반환.
    public bool OnPaymentCallback(PaymentCallback cb)
    {
        // (A) 이미 처리한 거래면 스킵
        if (_processed.Contains(cb.TransactionId))
            return true;

        var acc = _accounts[cb.AccountId];

        // (B) 재화 지급
        acc.Balance += cb.Amount;

        // (C) 처리 표시
        _processed.Add(cb.TransactionId);

        return true;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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