← 문제로

27. 파티장 위임 중에 파티장이 탈퇴/접속종료되는 상황

난이도 하
내 리뷰 · C#
해설 · C#

해설 — 파티장 위임 중에 파티장이 탈퇴/접속종료되는 상황

난이도: 하

답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계

요약

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 만 한다. targetIdparty.Members 에 없거나 방금 나갔거나 다른 파티 소속이어도 그대로 리더가 된다. 그 리더가 오프라인/부재면 파티는 사실상 leaderless(초대/추방/해산 명령이 먹히지 않음).
  • 재현조건: 클라가 잘못된/오래된 targetId 전송, 또는 대상이 위임 직전에 탈퇴.
  • 근본 원인: 위임을 서버가 검증하지 않고 클라가 보낸 대상을 신뢰. 멤버십·온라인 검사 누락.

(B) 위임 vs 자동 승계 경합 — 이중 리더/잘못된 리더 (동시성·정합) ★간판

  • 분류 태그: race / check-then-act.
  • 증상: Delegate(req=A→target=B) 와 옛 파티장 A 의 Leave 가 동시에 진행되면,
    • LeaveLeaderId==A 로 보고 Members[0](예: C)로 승계하는 사이 DelegateLeaderId=B 로 덮어써 A 의 의도(B)와 자동 승계(C)가 충돌 → 최종 리더가 비결정적.
    • 또는 Delegate 가 먼저 LeaderId=B 로 바꾼 뒤 Leave(A)LeaderId==A 검사가 실패해 승계 분기를 건너뛰는데, 정작 A 는 아직 Members 에 남아 제거 순서가 꼬인다.
  • 근본 원인: 리더 변경 경로가 여러 개인데 상호배제·단일 권위가 없다(락 없음).

(C) 접속종료 시 승계/해산 누락 — 영구 leaderless (정합) ★간판

  • 분류 태그: missing state transition.
  • 증상: OnDisconnectMembers.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. 파티 상태를 단일 라이터(액터)로: 한 파티의 모든 명령(초대/추방/위임/탈퇴/접속종료)을 파티별 메시지 큐로 순차 처리 → 락 없이 불변식(리더 정확히 1명) 유지. 트레이드오프: 라우팅.
  2. 승계 정책 명시화: Members[0] 대신 "가입이 오래된 순 / 부파티장 우선 / 온라인 우선" 규칙으로 결정적 승계. 트레이드오프: 정책 복잡도.
  3. 접속종료 유예(grace) + 재접속 승계 보류: 파티장이 잠깐 끊긴 경우 짧은 유예 동안 자리를 비워두고, 미복귀 시 승계. 트레이드오프: 그동안 명령 제약/타임아웃 설계.
  4. 불변식 검증 자산화: 변경 후 "리더는 멤버에 속하고 정확히 1명" 을 assert/단위테스트로 상시 검증해 회귀 방지.

면접 포인트 (예상 질문)

  1. 위임 대상을 서버가 검증하지 않으면 어떤 비정상 상태가 생기는가? 무엇을 검증해야 하나?
  2. 파티장이 빠지는 경로가 탈퇴·접속종료 둘인데 승계 로직이 한쪽에만 있으면 왜 위험한가? 어떻게 일원화하는가?
  3. 위임과 자동 승계가 동시에 일어날 때 "리더 정확히 1명" 불변식을 어떻게 보장하겠는가?