1. 글로벌 거래소를 여러 지역 서버가 공유할 때의 정합성
난이도 최상해설 — 글로벌 거래소를 여러 지역 서버가 공유할 때의 정합성
난이도: 최상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
세 곳에서 깨진다. (A) 중앙 거래소 Decrement 가 "검사(cur<qty) → 차감" 을 비원자로 하고
공유 Dictionary 를 락 없이 다룬다 → 여러 지역이 동시에 호출하면 둘 다 cur>=qty 를 통과해
oversell. (B) 각 지역이 로컬 캐시 재고로 구매 가능 판단 → 캐시는 다른 지역의 차감을
모르므로, 동시 구매 시 합산이 실제 재고를 초과한다. (C) 지역이 캐시를 먼저 줄이고 아이템을
지급한 뒤 중앙 차감을 요청하며 반환값을 무시 → 중앙이 거부(재고 0)해도 이미 지급해
무한정 oversell + 이중 지급. 정답 한 줄: 재고의 단일 권위는 중앙이며, 구매는 중앙의
원자적 compare-and-decrement(또는 직렬화 트랜잭션) 성공 후에만 지급한다. 로컬 캐시는 판단의
권위가 아니라 힌트일 뿐이다.
변별: concurrency_memory/problem16(존 간 이전 분산 트랜잭션), problem12(분산 락 펜싱/ 만료)와 달리, 본 문제는 다중 지역이 공유하는 단일 재고의 권위·원자 차감과 로컬 캐시 신뢰 오류(oversell) 가 핵심이다.
문제점
(A) 중앙 Decrement 의 비원자 검사-차감 — oversell (분산/동시성) ★간판
- 분류 태그: check-then-act / lost update (across RPC).
- 증상:
if (cur < qty) return false; _remaining = cur - qty;가 원자가 아니다. 재고 1, 두 지역이 동시에Decrement(1)→ 둘 다cur=1>=1통과 → 둘 다 성공, 재고-1. 단일 권위인데도 동시 요청에 oversell.Dictionary자체도 동시 쓰기로 손상. - 재현조건: 서로 다른 지역의 구매 RPC 가 거의 동시에 중앙에 도달.
- 근본 원인: 권위 노드의 차감이 원자적(락/CAS/직렬화)이지 않다.
(B) 로컬 캐시 재고로 구매 판단 — 분산 oversell (분산/일관성) ★간판
- 분류 태그: stale-distributed-cache / authority-bypass.
- 증상: 지역 A·B 가 각자 캐시(예: 둘 다 5)를 보고 각각 5개를 판다 → 합계 10인데 실제 재고는 5. 캐시는 다른 지역의 차감을 실시간 반영하지 못한다.
- 근본 원인: "구매 가능" 의 권위를 로컬 캐시에 둔다(캐시는 힌트여야 함).
(C) 선지급 후 사후 차감 + 반환 무시 — 이중 지급/무한 oversell (정합) ★간판
- 분류 태그: act-before-commit / ignored-failure.
- 증상:
_localCache를 줄이고GrantItemAndCharge로 먼저 지급한 뒤_central .Decrement(...)를 호출하고 반환값을 무시한다. 중앙이 재고 부족으로 거부해도 아이템은 이미 나갔다. 보상/롤백도 없다. - 근본 원인: "권위 커밋(중앙 차감 성공)" 전에 부작용(지급)을 실행한다.
수정안
핵심: ① 중앙 차감을 원자 compare-and-decrement, ② 지급은 중앙 성공 이후에만, ③ 로컬 캐시는 판단 권위가 아닌 힌트.
public class CentralExchange
{
private readonly Dictionary<long, int> _remaining = new();
private readonly object _gate = new();
// 원자 차감: 성공 시 true. 단일 임계구역에서 검사+차감.
public bool TryDecrement(long listingId, int qty)
{
if (qty <= 0) return false;
lock (_gate)
{
int cur = _remaining.TryGetValue(listingId, out var q) ? q : 0;
if (cur < qty) return false; // 검사
_remaining[listingId] = cur - qty; // 차감 — 같은 락 안이라 원자
return true;
}
}
}
public bool Buy(long buyerId, long listingId, int qty)
{
// (B) 로컬 캐시는 '빠른 사전 거절' 힌트로만 사용(정확성 근거 아님)
if (_localCache.TryGetValue(listingId, out var hint) && hint < qty)
{
// 힌트가 부족이라 말해도, 갱신 후 한 번 더 시도해볼 수 있음(여기선 빠른 거절)
}
// (A)(C) 권위(중앙)의 원자 차감이 성공한 뒤에만 지급
if (!_central.TryDecrement(listingId, qty))
{
RefreshCache(listingId); // 캐시 보정
return false; // 재고 없음 → 지급 안 함
}
GrantItemAndCharge(buyerId, listingId, qty); // 커밋 이후 지급
_localCache[listingId] = _central.GetRemaining(listingId); // 힌트 갱신
return true;
}
포인트
- 단일 권위(중앙)의 원자 차감이 oversell 을 막는다(락/CAS/
SELECT ... FOR UPDATE/Redis Lua/DECRBY+검사). - 지급은 차감 성공 후에만 → 거부 시 부작용 없음(이중 지급 차단).
- 로컬 캐시는 힌트(빠른 거절·UX)일 뿐, 구매 성공 판단의 권위가 아니다.
- 지급 자체도 멱등하게(주문ID 기반) 해 재시도 안전(아래 설계).
더 나은 설계 (+트레이드오프)
- 중앙 원자 연산 + 멱등 주문: 구매를 주문ID로 멱등화하고 중앙에서
qty를 원자 차감. 재시도/네트워크 중복에도 한 번만 반영. 트레이드오프: 주문 저장소·중복 제거 비용. - 예약(reserve)–확정(commit) 2단계: 중앙이 재고를 잠깐 예약(TTL) → 지급 성공 시 확정, 실패/타임아웃 시 자동 복원. 분산 트랜잭션의 사가(saga). 트레이드오프: 예약 만료/보상 설계.
- 지역 샤딩 또는 재고 리스풀(lease): 매물 재고를 지역에 일정량 리스로 나눠줘(예: 각 지역 N개) 핫 매물의 중앙 경합을 줄이고, 소진 시 재요청. 트레이드오프: 잔여 회수/재분배 복잡.
- 단일 파티션 직렬화: 한 매물의 모든 구매를 같은 파티션(액터/큐)이 순차 처리 → 락 없이 원자. 트레이드오프: 핫스팟 매물의 처리량 한계.
면접 포인트 (예상 질문)
- 단일 권위 노드인데도
Decrement가 oversell 을 내는 인터리빙을 설명하라. 원자 compare-and-decrement 를 DB/Redis 로 어떻게 구현하나? - 로컬 캐시 재고로 구매를 판단하면 왜 분산 oversell 이 나나? 캐시를 "힌트"로만 쓴다는 게 무슨 뜻인가?
- "지급 후 차감" 대신 "차감 성공 후 지급"이어야 하는 이유, 그리고 예약-확정(saga) 패턴이 네트워크 실패에서 어떻게 안전한가?
해설 — 글로벌 거래소를 여러 지역 서버가 공유할 때의 정합성 (C++)
난이도: 최상
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
(A) 중앙 decrement 가 "검사 → 차감" 을 비원자로(락 없이 공유 unordered_map) 처리 →
동시 호출 시 둘 다 통과해 oversell + 자료구조 손상. (B) 각 지역이 로컬 캐시 재고로 구매
판단 → 다른 지역의 차감을 모르므로 합산이 재고 초과(분산 oversell). (C) 지역이 캐시를 먼저
줄이고 지급한 뒤 중앙 차감을 요청하고 반환을 무시 → 중앙이 거부해도 이미 지급(이중 지급).
정답 한 줄: 재고의 단일 권위는 중앙이며, 구매는 중앙의 원자 compare-and-decrement 성공
후에만 지급한다. 로컬 캐시는 권위가 아니라 힌트다.
변별: concurrency_memory/problem16(존 간 이전 분산 트랜잭션), problem12(분산 락 펜싱/만료)와 달리, 본 문제는 다중 지역 공유 단일 재고의 권위·원자 차감·로컬 캐시 신뢰 오류(oversell) 가 핵심이다.
문제점
(A) 중앙 decrement 비원자 검사-차감 — oversell (분산/동시성) ★간판
- 분류 태그: check-then-act / lost update (across RPC).
- 증상:
if (cur < qty) return false; remaining_=cur-qty;가 원자가 아니다. 재고 1, 두 지역 동시decrement(1)→ 둘 다 통과 → 재고-1.unordered_map동시 쓰기는 데이터 레이스(UB). - 근본 원인: 권위 노드 차감이 원자(락/CAS/직렬화)가 아님.
(B) 로컬 캐시로 구매 판단 — 분산 oversell (분산/일관성) ★간판
- 분류 태그: stale-distributed-cache / authority-bypass.
- 증상: 지역 A·B 가 각자 캐시(둘 다 5)를 보고 각각 5개 판매 → 합 10, 실제 5. 캐시는 타 지역 차감을 실시간 반영 못 함.
- 근본 원인: "구매 가능" 권위를 로컬 캐시에 둠(캐시는 힌트여야).
(C) 선지급 후 사후 차감 + 반환 무시 — 이중 지급 (정합) ★간판
- 분류 태그: act-before-commit / ignored-failure.
- 증상:
grantItemAndCharge로 먼저 지급한 뒤central_.decrement(...)반환을 무시. 중앙 거부에도 아이템은 이미 나갔고 롤백 없음. - 근본 원인: 권위 커밋(중앙 차감 성공) 전에 부작용(지급) 실행.
수정안
핵심: ① 중앙 차감을 원자 compare-and-decrement, ② 지급은 중앙 성공 후에만, ③ 로컬 캐시는 힌트.
#include <mutex>
class CentralExchange {
public:
// 원자 차감: 같은 임계구역에서 검사+차감 (분산 실서버에선 DB CAS / Redis Lua / DECR 검사)
bool tryDecrement(int64_t listingId, int qty) {
if (qty <= 0) return false;
std::lock_guard<std::mutex> g(gate_);
int cur = 0; auto it = remaining_.find(listingId);
if (it != remaining_.end()) cur = it->second;
if (cur < qty) return false; // 검사
remaining_[listingId] = cur - qty; // 차감 — 같은 락 안이라 원자
return true;
}
int getRemaining(int64_t listingId) {
std::lock_guard<std::mutex> g(gate_);
auto it = remaining_.find(listingId);
return it == remaining_.end() ? 0 : it->second;
}
private:
std::unordered_map<int64_t,int> remaining_;
std::mutex gate_;
};
bool buy(int64_t buyerId, int64_t listingId, int qty) {
// (B) 로컬 캐시는 '빠른 사전 거절' 힌트로만. 정확성 근거 아님.
// (A)(C) 권위(중앙)의 원자 차감 성공 후에만 지급
if (!central_.tryDecrement(listingId, qty)) {
refreshCache(listingId);
return false; // 재고 없음 → 지급 안 함
}
grantItemAndCharge(buyerId, listingId, qty); // 커밋 이후 지급
localCache_[listingId] = central_.getRemaining(listingId); // 힌트 갱신
return true;
}
포인트
- 단일 권위의 원자 차감이 oversell 차단(락/CAS/
SELECT FOR UPDATE/Redis Lua). - 지급은 차감 성공 후에만 → 이중 지급 차단.
- 로컬 캐시는 힌트(빠른 거절/UX)일 뿐 권위가 아님.
- 지급도 주문ID로 멱등화하면 재시도/중복 RPC 안전.
더 나은 설계 (+트레이드오프)
- 중앙 원자 연산 + 멱등 주문(주문ID): 재시도/중복에도 한 번만 반영. 트레이드오프: 주문 저장/중복 제거 비용.
- 예약(reserve, TTL)–확정(commit) saga: 재고를 잠깐 예약 후 지급 성공 시 확정, 실패/ 만료 시 자동 복원. 트레이드오프: 예약 만료/보상 설계.
- 재고 리스(lease)/지역 샤딩: 핫 매물 재고를 지역에 일정량 리스로 분배해 중앙 경합 완화, 소진 시 재요청. 트레이드오프: 잔여 회수/재분배 복잡.
- 매물 단위 단일 파티션 직렬화(액터/큐): 한 매물의 모든 구매를 순차 처리 → 락 없이 원자. 트레이드오프: 핫 매물 처리량 한계.
면접 포인트 (예상 질문)
- 단일 권위인데도
decrement가 oversell 을 내는 인터리빙을 설명하고, 원자 compare-and-decrement 를 DB/Redis 로 구현해보라. - 로컬 캐시 재고로 판단하면 왜 분산 oversell 이 나나? "캐시는 힌트" 원칙이란?
- "차감 성공 후 지급"이어야 하는 이유와, 예약-확정(saga)이 네트워크 실패에서 안전한 이유는?