2. 보상 지급을 여러 독립 커밋으로 쪼갠 부분 커밋/이중 지급
난이도 상해설 — 보상 지급을 여러 독립 커밋으로 쪼갠 부분 커밋/이중 지급
난이도: 상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
한 논리적 작업(아이템+골드+로그)을 세 개의 독립 커밋으로 쪼갰고, 멱등성 게이트를
맨 마지막에 둔다.
(B)(C)(D) 가 각각 autocommit 이라 사이에 예외/크래시가 나면 부분 커밋이 남는다: 아이템만
들어가고 골드는 안 들어가거나, 둘 다 들어갔는데 로그(D)가 없거나. (A) 멱등성 검사는
quest_reward_log 를 보는데 그 로그는 가장 마지막(D)에 쓰인다. 그래서 (B)/(C)까지만
하고 죽으면, 재시도 시 (A)가 "아직 안 받음"으로 통과 → 아이템·골드를 다시 지급(이중
지급). 정답 한 줄: 세 작업을 한 트랜잭션으로 묶어 all-or-nothing 으로 만들고, 멱등성 키
(quest_reward_log UNIQUE)를 같은 트랜잭션 안에서 — 가능하면 먼저 — 기록하라.
변별: persistence_data/problem1(read-your-writes/복제 지연)과 concurrency_memory/problem13 (캐시-DB 이중 쓰기)과 달리, 본 문제는 단일 DB 안에서의 트랜잭션 원자성/멱등성 경계 — 다중 커밋으로 인한 부분 반영과 dedup 마커 배치 순서가 핵심이다.
문제점
(B)(C)(D) 다중 autocommit — 부분 커밋 (영속화/원자성) ★간판
- 분류 태그: non-atomic multi-statement / partial commit.
- 증상: (B) 성공 후 (C)에서 예외/크래시 → 아이템은 영구히 남고 골드는 없음. (C)까지 성공하고 (D) 실패 → 보상은 다 줬는데 "받았다는 기록"이 없음.
- 재현 조건: 단계 사이의 예외/타임아웃/프로세스 종료.
- 근본 원인: 각
Execute가 즉시 커밋되어 롤백 단위가 없다. 한 논리 작업 = 한 트랜잭션 이어야 한다.
(A)/(D) 멱등성 마커를 마지막에 — 재시도 시 이중 지급 (멱등성/순서) ★간판
- 분류 태그: idempotency gate ordering / at-least-once double grant.
- 증상: dedup 기준인 로그(D)가 맨 끝이라, (B)/(C)만 하고 중단된 케이스를 (A)가 못 걸러 재지급한다.
- 근본 원인: "받았다" 표식이 부수효과 뒤에 따로 커밋되면, 부분 완료를 멱등성 검사로 잡을 수 없다. 표식은 같은 트랜잭션 안에서(이상적으로 UNIQUE 제약으로 먼저) 박혀야 한다.
(A) 검사-후-행동 경쟁(보강)
- 분류 태그: check-then-act / race on dedup.
- 증상: 더블클릭/동시 두 요청이 (A)를 동시에 통과해 둘 다 지급. UNIQUE 제약 + 충돌 처리로 막아야 한다.
수정안 (정확한 코드)
세 작업 + 멱등성 마커를 하나의 트랜잭션으로. quest_reward_log(player_id,quest_id) 에
UNIQUE 제약을 두고, 충돌 시 "이미 지급"으로 처리.
public interface IDbTx : System.IDisposable
{
void Execute(string sql, params object[] args);
void Commit();
// Dispose 시 미커밋이면 롤백
}
public interface IDb2 { IDbTx BeginTransaction(); }
public void GrantQuestReward(IDb2 db, long playerId, long questId, long itemId, int gold)
{
using var tx = db.BeginTransaction();
try
{
// 멱등성 게이트: UNIQUE(player_id, quest_id) 위반이면 이미 지급 → 중단
tx.Execute("INSERT INTO quest_reward_log(player_id,quest_id) VALUES(?,?)",
playerId, questId); // 중복이면 여기서 예외 → 롤백, 재지급 0
tx.Execute("INSERT INTO inventory(player_id,item_id,count) VALUES(?,?,1)",
playerId, itemId);
tx.Execute("UPDATE wallet SET gold = gold + ? WHERE player_id=?",
gold, playerId);
tx.Commit(); // 넷이 함께 커밋되거나, 예외 시 Dispose 에서 함께 롤백
}
catch (DuplicateKeyException)
{
// 이미 받은 보상 — 정상 종료(멱등). tx 는 롤백됨.
}
}
핵심: ① 한 트랜잭션 = all-or-nothing(부분 커밋 제거), ② 멱등성 마커를 트랜잭션 안에서 먼저 INSERT 해 재시도/동시요청을 UNIQUE 제약으로 차단. 부분 완료 자체가 사라지므로 "받았는지" 판단이 항상 정확하다.
더 나은 설계 (+ 트레이드오프)
- 멱등성 키를 클라/요청 단위로:
(player_id, quest_id)또는 요청 토큰(request id)을 키로. 외부 결제/지급은 멱등성 키가 사실상 필수. - 여러 저장소에 걸치면 단일 트랜잭션 불가: 인벤토리/지갑이 다른 DB·샤드면 2PC 는 비싸고 취약 → Saga + 보상 트랜잭션 또는 트랜잭셔널 아웃박스로 최종 일관성. 트레이드오프: 일시적 불일치 허용 vs 강한 일관성.
- 재시도 안전성: 호출자가 타임아웃 후 재시도해도 멱등 키로 안전. "지급은 됐는데 응답을 못 받은" 회색지대를 항상 가정.
- 관측성: 부분 상태/보상 트랜잭션을 모니터링·알림. 이중 지급/누락을 사후 탐지하는 정합성 검사 배치.
면접 포인트
- "한 논리 작업 = 한 트랜잭션." autocommit 다중 문장은 부분 커밋을 부른다. 롤백 단위를 의식하라.
- 멱등성 마커는 부수효과와 같은 트랜잭션 안에서, 가능하면 먼저 박아 부분 완료를 잡는다. UNIQUE 제약이 가장 견고한 dedup.
- 여러 저장소에 걸치면 단일 트랜잭션이 불가 → Saga/아웃박스/2PC 의 트레이드오프를 설명.
해설 — 보상 지급을 여러 독립 커밋으로 쪼갠 부분 커밋/이중 지급 (C++)
난이도: 상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
논리적 원자성/멱등성 문제는 C# 트윈과 같고, C++ 에서는 예외 안전성/RAII 관점이 더해진다.
(B)(C)(D) 가 각각 autocommit 이라, 어느 Execute 가 예외를 던지면 그 앞 단계는 이미
커밋돼 부분 상태가 남는다. C++ 에는 트랜잭션을 묶는 RAII 가드가 없어 스코프를 벗어나도
자동 롤백이 일어나지 않는다. (A) 멱등성 검사는 맨 마지막(D)에 쓰이는 로그를 보므로,
(B)/(C)까지 하고 죽으면 재시도 시 통과 → 이중 지급. 정답 한 줄: BEGIN/COMMIT 트랜잭션을
RAII 가드로 감싸(미커밋이면 소멸자에서 롤백) all-or-nothing 으로 만들고, 멱등성 마커를
같은 트랜잭션 안에서 먼저(UNIQUE) 박아라.
변별: persistence_data/problem1(복제 지연)·concurrency_memory/problem13(캐시-DB 이중 쓰기)과 달리, 단일 DB 의 트랜잭션 원자성 + 예외 안전성(RAII) + 멱등성 마커 순서 가 핵심.
문제점
(B)(C)(D) 다중 autocommit + 비RAII — 부분 커밋 (영속화/예외 안전성) ★간판
- 분류 태그: non-atomic multi-statement / no transactional RAII / partial commit.
- 증상:
Execute중 하나가 throw 하면 스택 되감기로 함수를 빠져나가지만, 앞서 커밋된 (B)/(C)는 그대로 남는다. 롤백을 호출하는 곳이 없다. - 근본 원인: 한 논리 작업이 한 트랜잭션이 아니고, 실패 시 되돌릴 RAII 가드도 없다.
(A)/(D) 멱등성 마커를 마지막에 — 재시도 시 이중 지급 (멱등성/순서) ★간판
- 분류 태그: idempotency gate ordering / double grant on retry.
- 증상: dedup 기준 로그(D)가 끝이라 부분 완료를 (A)가 못 걸러 재지급.
- 근본 원인: 표식이 부수효과 뒤 별도 커밋. 같은 트랜잭션에서 먼저 박아야 한다.
(A) check-then-act 경쟁(보강)
- 동시 두 요청이 (A)를 함께 통과 → 둘 다 지급. UNIQUE 제약으로 차단.
수정안 (정확한 코드)
트랜잭션을 RAII 가드로. commit() 을 호출하지 않고 스코프를 벗어나면 소멸자가 롤백.
#include <string>
struct ITx
{
virtual void Execute(const std::string& sql) = 0;
virtual void Commit() = 0;
virtual void Rollback() = 0;
virtual ~ITx() = default;
};
struct IDb2 { virtual ITx* Begin() = 0; virtual ~IDb2() = default; };
// 트랜잭션 RAII 가드: commit 안 하면 소멸 시 롤백
class TxGuard
{
ITx* tx_;
bool committed_ = false;
public:
explicit TxGuard(ITx* tx) : tx_(tx) {}
ITx* operator->() { return tx_; }
void commit() { tx_->Commit(); committed_ = true; }
~TxGuard() { if (!committed_) tx_->Rollback(); } // 예외 경로 포함 자동 롤백
TxGuard(const TxGuard&) = delete;
TxGuard& operator=(const TxGuard&) = delete;
};
void GrantQuestReward(IDb2& db, long playerId, long questId, long itemId, int gold)
{
TxGuard tx(db.Begin());
// 멱등성 게이트: UNIQUE(player_id,quest_id) 위반이면 throw → 소멸자에서 롤백
tx->Execute("INSERT INTO quest_reward_log(player_id,quest_id) VALUES(...)");
tx->Execute("INSERT INTO inventory(player_id,item_id,count) VALUES(...)");
tx->Execute("UPDATE wallet SET gold = gold + ... WHERE player_id=...");
tx.commit(); // 여기 도달해야 전체 커밋. 중간 예외면 전부 롤백.
}
핵심: RAII 가드로 예외/조기 반환 시 자동 롤백 → 부분 커밋 제거, 멱등성 마커를 먼저 INSERT → 재시도/동시요청을 UNIQUE 로 차단. (중복 INSERT 예외는 호출자에서 "이미 지급" 으로 흡수.)
더 나은 설계 (+ 트레이드오프)
- 여러 저장소/샤드면 단일 트랜잭션 불가 → Saga + 보상 트랜잭션 또는 트랜잭셔널 아웃박스 로 최종 일관성. 트레이드오프: 일시적 불일치 허용.
- 멱등성 키:
(player_id,quest_id)또는 요청 토큰. 외부 결제는 필수. - 강한 예외 안전성: 모든 다단계 영속화는 "전부/전무" 보장(스트롱 개런티)을 목표로 RAII.
- 정합성 검사 배치: 이중 지급/누락을 사후 탐지.
면접 포인트
- C++ 의 예외 안전성: 다단계 부수효과는 RAII 가드로 "commit 안 하면 롤백" 패턴. 소멸자는 throw 하지 않게(롤백 실패 로깅).
- "한 논리 작업 = 한 트랜잭션", 멱등성 마커는 같은 트랜잭션에서 먼저. UNIQUE 가 가장 견고.
- 여러 저장소면 단일 트랜잭션 불가 → Saga/아웃박스/2PC 트레이드오프.