15. 트랜잭션을 연 채 하는 결제 게이트웨이 동기 호출 (커넥션 풀 고갈 + 행 잠금 장기 보유)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 아이템 구매 확정 (결제 게이트웨이 확인 포함)
// ----------------------------------------------------------------------------
// 시나리오 (서버 내부 / 서버-서버):
// - 유료 아이템 구매는 (1) 주문 행을 잠그고 (2) 외부 결제 게이트웨이에
// "결제가 실제로 승인됐는지" 동기 HTTP 로 재확인한 뒤 (3) 재화 지급과
// 주문 상태 갱신을 DB 트랜잭션으로 커밋한다.
// - 결제 게이트웨이 확인은 네트워크 왕복이 있는 동기 호출이며, 게이트웨이가
// 혼잡하면 수백 ms~수 초까지 걸릴 수 있다.
// - DB 커넥션 풀은 크기가 한정돼 있고, 동시 구매 확정 요청이 많다(이벤트/할인 시).
//
// 상황:
// - 결제 게이트웨이가 평소보다 느려지는 시간대에 구매 확정 API 전체가
// 느려지고, 급기야 "커넥션을 얻지 못했습니다" 오류가 다른 무관한
// 요청(인벤토리 조회 등)에서도 터지기 시작한다.
//
// 과제:
// (A) 트랜잭션 시작 시점과 (B) 결제 게이트웨이 확인 호출의 위치를 검토하라.
// 게이트웨이가 느려질 때 DB 커넥션 풀과 주문 행 잠금에 각각 무슨 일이
// 생기는지 설명하고 수정안을 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
namespace GameServer.Store
{
public interface IDbConnection
{
IDbTransaction BeginTransaction();
void Execute(IDbTransaction tx, string sql, object args);
object QuerySingle(IDbTransaction tx, string sql, object args);
}
public interface IDbTransaction { void Commit(); void Rollback(); }
public interface IConnectionPool
{
IDbConnection Rent();
void Return(IDbConnection c);
}
public interface IPaymentGateway
{
// 동기 HTTP 호출. 게이트웨이 혼잡 시 수백 ms ~ 수 초 소요.
bool ConfirmPayment(string orderId);
}
public sealed class PurchaseService
{
private readonly IConnectionPool _pool;
private readonly IPaymentGateway _gateway;
public PurchaseService(IConnectionPool pool, IPaymentGateway gateway)
{
_pool = pool;
_gateway = gateway;
}
public bool ConfirmPurchase(string orderId, long characterId, int itemId)
{
IDbConnection conn = _pool.Rent();
try
{
IDbTransaction tx = conn.BeginTransaction(); // (A) 트랜잭션 시작
try
{
// 주문 행을 잠그고 상태 확인 (SELECT ... FOR UPDATE 상당)
conn.QuerySingle(tx, "SELECT status FROM orders WHERE id=@id FOR UPDATE", new { id = orderId });
bool approved = _gateway.ConfirmPayment(orderId); // (B) 트랜잭션을 연 채 외부 동기 호출
if (!approved)
{
tx.Rollback();
return false;
}
conn.Execute(tx, "UPDATE orders SET status='CONFIRMED' WHERE id=@id", new { id = orderId });
conn.Execute(tx, "UPDATE inventory SET count = count + 1 WHERE char_id=@cid AND item_id=@iid",
new { cid = characterId, iid = itemId });
tx.Commit();
return true;
}
catch
{
tx.Rollback();
throw;
}
}
finally
{
_pool.Return(conn);
}
}
}
} 결함 코드 · C++
// ─────────────────────────────────────────────────────────────
// 시나리오 (서버 내부 / 서버-서버): 아이템 구매 확정 (결제 게이트웨이 확인 포함)
// - 유료 아이템 구매는 (1) 주문 행을 잠그고 (2) 외부 결제 게이트웨이에
// "결제가 실제로 승인됐는지" 동기 HTTP 로 재확인한 뒤 (3) 재화 지급과
// 주문 상태 갱신을 DB 트랜잭션으로 커밋한다.
// - 결제 게이트웨이 확인은 네트워크 왕복이 있는 블로킹 호출이며, 게이트웨이가
// 혼잡하면 수백 ms~수 초까지 걸릴 수 있다.
// - DB 커넥션 풀은 크기가 한정돼 있고, 동시 구매 확정 요청이 많다(이벤트/할인 시).
//
// 상황: 결제 게이트웨이가 평소보다 느려지는 시간대에 구매 확정 요청 처리가
// 전반적으로 느려지고, 커넥션을 빌리지 못해 대기하는 스레드(인벤토리 조회 등
// 무관한 요청 포함)가 급증한다.
//
// 과제: (A) 트랜잭션을 시작해 얻은 가드(TxGuard)의 생존 범위와 (B) 결제
// 게이트웨이 확인 호출의 위치를 검토하라. 게이트웨이가 느려질 때 DB 커넥션
// 풀과 주문 행 잠금에 각각 무슨 일이 생기는지 설명하고 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
#include <memory>
#include <stdexcept>
#include <string>
namespace store {
class IDbConnection {
public:
virtual ~IDbConnection() = default;
virtual void begin() = 0;
virtual void commit() = 0;
virtual void rollback() = 0;
virtual void execute(const std::string& sql) = 0;
virtual bool queryLockOrderRow(const std::string& orderId) = 0;
};
class IConnectionPool {
public:
virtual ~IConnectionPool() = default;
virtual std::shared_ptr<IDbConnection> rent() = 0; // 풀에서 대여(없으면 대기/타임아웃)
virtual void giveBack(const std::shared_ptr<IDbConnection>& c) = 0;
};
class IPaymentGateway {
public:
virtual ~IPaymentGateway() = default;
// 블로킹 네트워크 호출. 게이트웨이 혼잡 시 수백 ms ~ 수 초 소요.
virtual bool confirmPayment(const std::string& orderId) = 0;
};
// 트랜잭션 시작~소멸까지 자동으로 commit/rollback 을 보장하는 RAII 가드.
class TxGuard {
public:
explicit TxGuard(std::shared_ptr<IDbConnection> conn)
: conn_(std::move(conn)) {
conn_->begin();
}
~TxGuard() {
if (!done_) conn_->rollback();
}
void commit() {
conn_->commit();
done_ = true;
}
private:
std::shared_ptr<IDbConnection> conn_;
bool done_ = false;
};
class PurchaseService {
public:
PurchaseService(std::shared_ptr<IConnectionPool> pool, std::shared_ptr<IPaymentGateway> gateway)
: pool_(std::move(pool)), gateway_(std::move(gateway)) {}
bool confirmPurchase(const std::string& orderId, int64_t characterId, int itemId) {
auto conn = pool_->rent();
TxGuard tx(conn); // (A) 트랜잭션 시작, conn 이 살아있는 한 지속
if (!conn->queryLockOrderRow(orderId)) {
pool_->giveBack(conn);
return false; // 주문 없음
}
bool approved = gateway_->confirmPayment(orderId); // (B) 트랜잭션을 연 채 블로킹 호출
if (!approved) {
pool_->giveBack(conn);
return false; // tx 소멸 시 자동 rollback
}
conn->execute("UPDATE orders SET status='CONFIRMED' WHERE id='" + orderId + "'");
conn->execute("UPDATE inventory SET count = count + 1 WHERE char_id=" +
std::to_string(characterId) + " AND item_id=" + std::to_string(itemId));
tx.commit();
pool_->giveBack(conn);
return true;
}
private:
std::shared_ptr<IConnectionPool> pool_;
std::shared_ptr<IPaymentGateway> gateway_;
};
} // namespace store 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.