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++
// [시나리오] 두 계정 간 골드 이체를 DB 트랜잭션으로 처리 (서버 내부)
// 거래/우편/선물 등으로 A 계정에서 B 계정으로 골드를 옮길 때, 한 트랜잭션
// 안에서 두 계정 행을 갱신한다(양쪽 행에 쓰기 잠금이 걸린다).
// 이 이체는 초당 수천 건, 임의의 계정 쌍에 대해 동시에 일어난다.
//
// [요구사항]
// - 이체는 원자적이어야 하고 총합이 보존돼야 한다.
// - 순간적인 경합 때문에 정상적인 이체가 사용자에게 실패로 튀어서는 안 된다.
//
// [과제] TransferService::transfer 를 검토하고, 서로 반대 방향(A->B 와 B->A)의
// 이체가 동시에 몰릴 때 어떤 일이 생기며 사용자에게 어떻게 드러나는지
// 원인과 수정안을 제시하라. 표시된 (A)(B) 지점에 주목하라.
#include <string>
#include <cstdint>
#include <stdexcept>
// DB 클라이언트 추상화 (외부 구현). exec 는 DB 오류 시 DbError 를 던진다.
struct DbError : std::runtime_error {
int code; // 드라이버 오류 코드
DbError(int c, const std::string& msg) : std::runtime_error(msg), code(c) {}
};
class Connection {
public:
void begin();
void commit();
void rollback();
void exec(const std::string& sql, int64_t amount, int id);
};
class ConnectionPool {
public:
Connection& acquire();
void release(Connection&);
};
class TransferService {
ConnectionPool& pool_;
public:
explicit TransferService(ConnectionPool& p) : pool_(p) {}
void transfer(int fromAccount, int toAccount, int64_t amount) {
Connection& conn = pool_.acquire();
conn.begin();
try {
// (A) 두 계정을 인자 순서대로 갱신(각 행에 쓰기 잠금)
conn.exec("UPDATE accounts SET gold = gold - ? WHERE id = ?", amount, fromAccount);
conn.exec("UPDATE accounts SET gold = gold + ? WHERE id = ?", amount, toAccount);
conn.commit();
} catch (const DbError&) {
// (B)
conn.rollback();
pool_.release(conn);
throw;
}
pool_.release(conn);
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.