27. 파티장 위임 중에 파티장이 탈퇴/접속종료되는 상황
난이도 하해설 — 파티장 위임 중에 파티장이 탈퇴/접속종료되는 상황
난이도: 하
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
Delegate/Leave/OnDisconnect 가 같은 Party 를 검증·동기화 없이 만진다.
(A) Delegate 는 위임 대상이 그 파티의 유효한 멤버인지 검증하지 않는다 — 이미 나간 사람,
다른 파티원, 외부인에게도 파티장을 넘겨 유효하지 않은 파티장/leaderless 를 만든다.
(B) 떠나는 사람이 파티장일 때 자동 승계가 위임과 경합한다: 위임이 LeaderId 를 막 바꾼 직후
옛 파티장의 Leave 가 "내가 리더네?" 검사를 통과(또는 못 해) 승계가 어긋나 떠난 사람이
리더로 남거나, 둘 다 리더가 되는 등 모순이 생긴다. (C) OnDisconnect 는 멤버만 제거하고
파티장 승계/해산을 전혀 하지 않아, 파티장이 접속종료되면 파티가 영구 leaderless(아무도
초대/추방/해산 불가)가 된다. 정답 한 줄: 파티 단위 락 아래에서 (1) 위임 대상이 현재 멤버인지
검증하고 (2) "파티장이 빠지는 모든 경로(탈퇴/접속종료)" 가 동일한 승계·해산 로직을 거치게
일원화하라.
변별: problem10(파티 전리품 분배), problem19(친구 추가/차단)와 달리, 본 문제는 리더십(소유권) 이양과 리더 이탈의 정합성 — leaderless/이중 리더/유효하지 않은 리더가 핵심.
문제점
(A) 위임 대상 미검증 — 유효하지 않은 파티장/leaderless (정합/검증) ★간판
- 분류 태그: missing validation / server-authority.
- 증상:
party.LeaderId = targetId만 한다.targetId가party.Members에 없거나 방금 나갔거나 다른 파티 소속이어도 그대로 리더가 된다. 그 리더가 오프라인/부재면 파티는 사실상 leaderless(초대/추방/해산 명령이 먹히지 않음). - 재현조건: 클라가 잘못된/오래된
targetId전송, 또는 대상이 위임 직전에 탈퇴. - 근본 원인: 위임을 서버가 검증하지 않고 클라가 보낸 대상을 신뢰. 멤버십·온라인 검사 누락.
(B) 위임 vs 자동 승계 경합 — 이중 리더/잘못된 리더 (동시성·정합) ★간판
- 분류 태그: race / check-then-act.
- 증상:
Delegate(req=A→target=B)와 옛 파티장 A 의Leave가 동시에 진행되면,Leave가LeaderId==A로 보고Members[0](예: C)로 승계하는 사이Delegate가LeaderId=B로 덮어써 A 의 의도(B)와 자동 승계(C)가 충돌 → 최종 리더가 비결정적.- 또는
Delegate가 먼저LeaderId=B로 바꾼 뒤Leave(A)의LeaderId==A검사가 실패해 승계 분기를 건너뛰는데, 정작 A 는 아직Members에 남아 제거 순서가 꼬인다.
- 근본 원인: 리더 변경 경로가 여러 개인데 상호배제·단일 권위가 없다(락 없음).
(C) 접속종료 시 승계/해산 누락 — 영구 leaderless (정합) ★간판
- 분류 태그: missing state transition.
- 증상:
OnDisconnect가Members.Remove만 한다. 파티장이 접속종료되면 리더 자리가 비고, 아무도 파티 명령을 못 써 파티가 영구 고착된다(남은 멤버 0명이어도 해산 안 됨). - 근본 원인: "파티장이 빠지는 경로" 가 탈퇴·접속종료 두 개인데, 승계·해산 로직이 한쪽
(
Leave)에만 있고 일원화돼 있지 않다.
수정안
핵심: 파티 단위 락 + 위임 대상 검증 + "리더 이탈 처리" 단일 함수로 일원화.
public class PartyService
{
private readonly Dictionary<long, Party> _parties = new();
private readonly object _gate = new(); // 데모용 글로벌 락(실전은 파티 단위 락 권장)
public bool Delegate(long partyId, long requesterId, long targetId)
{
lock (_gate)
{
if (!_parties.TryGetValue(partyId, out var party)) return false;
if (party.LeaderId != requesterId) return false; // 파티장만
if (targetId == requesterId) return false; // 자기 자신 무의미
if (!party.Members.Contains(targetId)) return false; // (A) 현재 멤버만 위임 가능
// (선택) 대상 온라인 여부 검사: if (!IsOnline(targetId)) return false;
party.LeaderId = targetId;
return true;
}
}
public void Leave(long partyId, long playerId)
{
lock (_gate)
{
if (!_parties.TryGetValue(partyId, out var party)) return;
party.Members.Remove(playerId);
if (party.LeaderId == playerId)
PromoteOrDisband(party); // 단일 경로
}
}
public void OnDisconnect(long partyId, long playerId)
{
lock (_gate)
{
if (!_parties.TryGetValue(partyId, out var party)) return;
party.Members.Remove(playerId);
if (party.LeaderId == playerId)
PromoteOrDisband(party); // (C) 동일 로직 재사용
}
}
// 리더가 빠졌을 때의 단일 승계/해산 로직 (반드시 락 보유 상태에서 호출)
private void PromoteOrDisband(Party party)
{
if (party.Members.Count > 0)
party.LeaderId = party.Members[0]; // 정책에 따라 가입 순/직책 순 등으로 선정
else
_parties.Remove(party.PartyId); // 남은 멤버 없으면 해산
}
}
포인트
- (A)
Members.Contains(targetId)검증으로 유효하지 않은 위임 차단(서버 권위). - (B) 모든 리더 변경이 같은 락 아래 → 위임과 승계가 직렬화되어 이중 리더/비결정 제거.
- (C) 탈퇴·접속종료 둘 다
PromoteOrDisband로 일원화 → 어떤 이탈 경로든 leaderless 불가. - 실전에서는
_gate대신 파티 단위 락으로 경합 범위를 좁힌다(여러 파티 동시 처리).
더 나은 설계 (+트레이드오프)
- 파티 상태를 단일 라이터(액터)로: 한 파티의 모든 명령(초대/추방/위임/탈퇴/접속종료)을 파티별 메시지 큐로 순차 처리 → 락 없이 불변식(리더 정확히 1명) 유지. 트레이드오프: 라우팅.
- 승계 정책 명시화:
Members[0]대신 "가입이 오래된 순 / 부파티장 우선 / 온라인 우선" 규칙으로 결정적 승계. 트레이드오프: 정책 복잡도. - 접속종료 유예(grace) + 재접속 승계 보류: 파티장이 잠깐 끊긴 경우 짧은 유예 동안 자리를 비워두고, 미복귀 시 승계. 트레이드오프: 그동안 명령 제약/타임아웃 설계.
- 불변식 검증 자산화: 변경 후 "리더는 멤버에 속하고 정확히 1명" 을 assert/단위테스트로 상시 검증해 회귀 방지.
면접 포인트 (예상 질문)
- 위임 대상을 서버가 검증하지 않으면 어떤 비정상 상태가 생기는가? 무엇을 검증해야 하나?
- 파티장이 빠지는 경로가 탈퇴·접속종료 둘인데 승계 로직이 한쪽에만 있으면 왜 위험한가? 어떻게 일원화하는가?
- 위임과 자동 승계가 동시에 일어날 때 "리더 정확히 1명" 불변식을 어떻게 보장하겠는가?
해설 — 파티장 위임 중에 파티장이 탈퇴/접속종료되는 상황 (C++)
난이도: 하
답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계
요약
delegate_/leave/onDisconnect 가 같은 Party 를 검증·동기화 없이 바꾼다.
(A) delegate_ 는 위임 대상이 그 파티의 유효 멤버인지 검증하지 않아 이미 나간 사람/외부인을
리더로 만든다(유효하지 않은 리더/leaderless). (B) 떠나는 사람이 리더일 때의 자동 승계가 위임과
경합해 이중 리더/비결정적 리더가 된다. (C) onDisconnect 는 멤버만 지우고 승계/해산을 하지
않아 파티장이 접속종료되면 영구 leaderless 가 된다. C++ 특유로, Party& p 참조를 들고 있을
때 다른 스레드가 parties_ 에 삽입(rehash)·erase(해산)하면 그 참조가 댕글링될 수 있다.
정답 한 줄: 파티 단위 mutex 아래에서 위임 대상 멤버십을 검증하고, 리더가 빠지는 모든 경로
(탈퇴/접속종료)를 같은 승계·해산 함수로 일원화하라.
변별: problem10·problem19 와 달리, 본 문제는 리더십(소유권) 이양과 리더 이탈의 정합성이 핵심.
문제점
(A) 위임 대상 미검증 — 유효하지 않은 리더/leaderless (정합/검증) ★간판
- 분류 태그: missing validation / server-authority.
- 증상:
p.leaderId = targetId만 한다.targetId가members에 없어도 리더가 되어, 그 리더가 부재면 파티 명령이 먹히지 않는다(사실상 leaderless). - 근본 원인: 클라가 보낸 대상을 서버가 검증 없이 신뢰.
std::find(members)누락.
(B) 위임 vs 자동 승계 경합 — 이중 리더/비결정 (동시성·정합) ★간판
- 분류 태그: race / check-then-act.
- 증상:
delegate_(A→B)와 옛 리더 A 의leave가 동시에 돌면, 승계(예:m[0]=C)와 위임(leaderId=B)이 서로 덮어써 최종 리더가 비결정적이거나,leaderId==A검사 타이밍이 엇갈려 승계가 통째로 누락된다. - 근본 원인: 리더 변경 경로가 여럿인데 상호배제가 없다.
(C) 접속종료 시 승계/해산 누락 — 영구 leaderless (정합) ★간판
- 분류 태그: missing state transition.
- 증상:
onDisconnect가members만 정리. 리더가 끊기면 자리만 비어 파티가 고착되고, 멤버 0명이어도 해산되지 않아 좀비 파티가 누적된다. - 근본 원인: 리더 이탈 처리(승계/해산)가
leave에만 있고 일원화돼 있지 않음.
(참고) 컨테이너 참조 수명 — 댕글링 (메모리/동시성)
Party& p = it->second;를 들고 있는 동안 다른 스레드의parties_.emplace(rehash) 또는 해산erase가 일어나면p가 무효화된다. 동시 접근 자체가 데이터 레이스(UB).
수정안
핵심: 파티 단위 mutex + 위임 대상 멤버십 검증 + "리더 이탈" 단일 함수.
#include <memory>
#include <mutex>
class PartyService {
public:
bool delegate_(int64_t partyId, int64_t requesterId, int64_t targetId) {
std::lock_guard<std::mutex> g(gate_);
auto it = parties_.find(partyId);
if (it == parties_.end()) return false;
Party& p = it->second;
if (p.leaderId != requesterId) return false;
if (targetId == requesterId) return false;
if (std::find(p.members.begin(), p.members.end(), targetId) == p.members.end())
return false; // (A) 현재 멤버만 위임 가능
p.leaderId = targetId;
return true;
}
void leave(int64_t partyId, int64_t playerId) { removeMember(partyId, playerId); }
void onDisconnect(int64_t partyId, int64_t playerId) { removeMember(partyId, playerId); } // (C)
private:
void removeMember(int64_t partyId, int64_t playerId) {
std::lock_guard<std::mutex> g(gate_);
auto it = parties_.find(partyId);
if (it == parties_.end()) return;
Party& p = it->second;
auto& m = p.members;
m.erase(std::remove(m.begin(), m.end(), playerId), m.end());
if (p.leaderId == playerId) { // (B) 리더 이탈은 단일 경로로
if (!m.empty()) p.leaderId = m[0]; // 승계 정책은 여기서 일원화
else parties_.erase(it); // 남은 멤버 없으면 해산
}
}
std::unordered_map<int64_t, Party> parties_;
std::mutex gate_; // 데모용 글로벌 락(실전은 파티 단위 락 권장)
};
포인트
std::find로 멤버십 검증 → 유효하지 않은 위임 차단.- 탈퇴·접속종료가 같은
removeMember로 일원화 → 어떤 이탈이든 승계/해산 보장. - 모든 리더 변경이 같은 락 → 이중 리더/비결정 제거.
- 실전에서는 파티별
mutex(예:unordered_map<int64_t, std::unique_ptr<std::mutex>>)로 경합 범위를 좁힌다.Party&를 락 밖으로 들고 나가지 말 것(rehash/해산 시 댕글링).
더 나은 설계 (+트레이드오프)
- 파티별 단일 라이터(액터/큐): 한 파티의 모든 명령을 순차 처리 → 락 없이 "리더 1명" 불변식 유지. 트레이드오프: 라우팅.
- 승계 정책 명시화:
m[0]대신 가입 순/부파티장/온라인 우선 등 결정적 규칙. 트레이드오프: 정책 복잡도. - 접속종료 grace 후 승계 보류: 잠깐 끊긴 리더는 짧은 유예 후 승계. 트레이드오프: 그동안 명령 제약/타임아웃.
- 불변식 assert + 테스트: 변경 후 "리더는 members 에 속하고 정확히 1명" 을 상시 검증.
면접 포인트 (예상 질문)
- 위임 대상을 서버가 검증하지 않으면 어떤 비정상 상태가 생기는가? 무엇을 검증해야 하나?
- 리더가 빠지는 경로가 탈퇴·접속종료 둘인데 한쪽에만 승계 로직이 있으면 왜 위험한가?
Party&참조를 락 밖으로 들고 나가면unordered_map에서 어떤 위험이 있는가?