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++
// ============================================================================
// [코드리뷰 문제] C++ - 결제 콜백 → 재화 지급의 멱등성 (서버-서버)
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 / 결제):
// - 외부 결제 서버(PG/스토어)가 "구매 완료" 콜백(웹훅)을 게임 서버로 보낸다.
// - 콜백에는 주문 식별자(transactionId)와 결제 금액이 담긴다.
// - 게임 서버는 이를 받아 해당 계정에 인게임 재화를 지급한다.
// - 결제 콜백은 at-least-once 다: 재시도로 같은 콜백이 여러 번, 때로는 거의
// 동시에 도착할 수 있다.
//
// 요구사항:
// - 같은 transactionId 의 지급은 정확히 한 번만(중복 지급 금지).
// - 동시 중복 콜백에도 한 번만 지급.
// - 서버 재시작 후 같은 콜백이 다시 와도 두 번 지급되면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 중복 지급/유실이 발생하는지
// 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <string>
#include <unordered_map>
#include <unordered_set>
struct Account {
int64_t id = 0;
int64_t balance = 0; // 인게임 재화 잔액
};
struct PaymentCallback {
std::string transactionId;
int64_t accountId = 0;
int64_t amount = 0; // 콜백 페이로드에 담긴 지급 금액
};
class PaymentService {
public:
explicit PaymentService(std::unordered_map<int64_t, Account>& accounts)
: accounts_(accounts) {}
// 결제 서버가 콜백할 때마다 호출(중복/동시 호출 가능). 성공 시 true.
bool OnPaymentCallback(const PaymentCallback& cb) {
// (A) 이미 처리한 거래면 스킵
if (processed_.count(cb.transactionId) != 0)
return true;
Account& acc = accounts_[cb.accountId];
// (B) 재화 지급
acc.balance += cb.amount;
// (C) 처리 표시
processed_.insert(cb.transactionId);
return true;
}
private:
std::unordered_map<int64_t, Account>& accounts_;
std::unordered_set<std::string> processed_; // 처리한 transactionId
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.