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

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