11. DB 트랜잭션 데드락: 일관된 락 순서 부재와 재시도 누락 (C#)

난이도 상 해설 보기 →

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

결함 코드 · C#
// [시나리오] 두 계정 간 골드 이체를 DB 트랜잭션으로 처리 (서버 내부)
// 거래/우편/선물 등으로 A 계정에서 B 계정으로 골드를 옮길 때, 한 트랜잭션
// 안에서 두 계정 행을 갱신한다(양쪽 행에 쓰기 잠금이 걸린다).
// 이 이체는 초당 수천 건, 임의의 계정 쌍에 대해 동시에 일어난다.
//
// [요구사항]
//   - 이체는 원자적이어야 하고 총합이 보존돼야 한다.
//   - 순간적인 경합 때문에 정상적인 이체가 사용자에게 실패로 튀어서는 안 된다.
//
// [과제] TransferService.Transfer 를 검토하고, 서로 반대 방향(A→B 와 B→A)의
//        이체가 동시에 몰릴 때 어떤 일이 생기며 사용자에게 어떻게 드러나는지
//        원인과 수정안을 제시하라. 표시된 (A)(B) 지점에 주목하라.

using System;
using System.Data;
using System.Data.Common;

class TransferService
{
    private readonly DbProviderFactory _factory;
    private readonly string _connString;

    private void Exec(DbConnection c, DbTransaction tx, string sql, long amount, int id)
    {
        using DbCommand cmd = c.CreateCommand();
        cmd.Transaction = tx;
        cmd.CommandText = sql;
        // @a=amount, @id=id 파라미터 바인딩 (생략)
        cmd.ExecuteNonQuery();
    }

    public void Transfer(int fromAccount, int toAccount, long amount)
    {
        using DbConnection conn = _factory.CreateConnection();
        conn.ConnectionString = _connString;
        conn.Open();
        using DbTransaction tx = conn.BeginTransaction();
        try
        {
            // (A) 두 계정을 인자 순서대로 갱신(각 행에 쓰기 잠금)
            Exec(conn, tx, "UPDATE accounts SET gold = gold - @a WHERE id = @id", amount, fromAccount);
            Exec(conn, tx, "UPDATE accounts SET gold = gold + @a WHERE id = @id", amount, toAccount);
            tx.Commit();
        }
        catch (DbException)
        {
            // (B)
            tx.Rollback();
            throw;
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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