← 문제로

12. 귓속말: 대상 로그아웃/채널이동 직후 전송 · C#

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

해설 — 귓속말: 대상 로그아웃/채널이동 직후 전송 · C#

난이도: 하

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

요약

핵심은 이름→세션 조회와 송신이 동시 로그아웃과 동기화되지 않는다는 것이다. (A) _byName[targetName] 는 키가 없으면 KeyNotFoundException 을 던지고, (B) 찾은 세션의 생사(Alive)를 확인하지 않고 송신하며 오프라인 시 발신자 통지도 없다. (C) _byName 이 일반 DictionaryWhisper(읽기)와 OnLogout(Remove)이 동시에 일어나면 "Collection was modified"/내부 손상 이 난다. C++ 판과 달리 GC 덕에 UAF(메모리 해제 후 접근)는 없지만, 오송신·예외·자료구조 손상은 그대로다. 정답의 한 줄: 레지스트리를 ConcurrentDictionary(또는 락)로 보호하고, TryGetValue + 생사 확인 후 송신, 오프라인이면 발신자에게 통지.


문제점

(A) 인덱서 조회 — 없는 키 예외 (정확성) ★간판

  • 증상: 오프라인 대상에게 귓속말하면 KeyNotFoundException 으로 핸들러가 죽는다.
  • 재현 조건: 존재하지 않거나 막 RemovetargetName. _byName[targetName] 예외.
  • 근본 원인: TryGetValue 로 조회하고 없으면 "오프라인" 으로 처리해야 한다.

(B) 생사 확인 없이 송신 — 검증 누락 (정확성)

  • target.Send(...) 전에 target.Alive 를 확인하지 않는다. 막 끊긴 세션으로 송신. 오프라인 시 발신자에게 "상대가 접속 중이 아닙니다" 통지도 없어 메시지가 조용히 사라진다.

(C) Dictionary 동시 접근 — 자료구조 손상/예외 (동시성) ★간판

  • 증상: 부하 시 간헐적 InvalidOperationException, 드물게 무한 루프/오독.
  • 재현 조건: T1 Whisper_byName 을 읽는 동안 T2 OnLogoutRemove. Dictionary 는 스레드 안전이 아니다.
  • 근본 원인: 공유 맵에 동기화가 없다. ConcurrentDictionary 또는 락 필요.

(보조) 채널이동 staleness — 정확성

  • 이동을 "로그아웃→로그인"으로 처리하면 그 사이 공백에 귓속말 유실. 이동 중 상태를 별도 표현하면 UX↑.

수정안

private readonly System.Collections.Concurrent.ConcurrentDictionary<string, Session> _byName = new();

public bool Whisper(Session from, string targetName, string text)
{
    // (A) TryGetValue: 예외 대신 안전 조회
    if (!_byName.TryGetValue(targetName, out var target) || !target.Alive)  // (B) 생사 확인
    {
        from.Send("상대가 접속 중이 아닙니다.");
        return false;
    }
    target.Send($"{from.Name} (귓속말): {text}");
    return true;
}

public void OnLogin(Session s)  => _byName[s.Name] = s;

public void OnLogout(Session s)
{
    s.Alive = false;
    // 내가 등록한 세션일 때만 제거(재로그인이 덮어쓴 경우 보호)
    _byName.TryRemove(new KeyValuePair<string, Session>(s.Name, s));
}
  • Alivevolatile/Interlocked 로 가시성 확보(또는 잠금). 송신 직후 끊길 수 있으니 Send 내부에서 닫힌 소켓을 안전 처리.
  • TryRemove(KeyValuePair) 로 "값까지 일치할 때만 제거" 해 재로그인 세션을 보호.

더 나은 설계

1) 안정 키 라우팅

  • 닉네임 대신 불변 playerId 로 라우팅하고 이름→id 인덱스를 분리. 닉변경/대소문자/이동에 강함.

2) 오프라인 귓속말 큐

  • 짧은 TTL 버퍼로 이동/재접속 중 메시지를 흡수, 도착 시 전달·만료 시 통지(트레이드오프: 메모리/복잡도).

3) 단일 소유 액터

  • 세션 레지스트리를 한 스레드가 소유하고 등록/해제/조회를 채널로 직렬화 → 락 제거.

4) 전송 결과 피드백

  • 송신 실패(큐 폐기/소켓 닫힘)를 발신자에게 일관되게 통지해 "보냈는데 안 갔다" 혼란 제거.

면접 포인트

  • 핵심: 공유 레지스트리 보호 + 조회-사용 사이 상태 변화(생사) 처리. C# 은 UAF는 없지만 Dictionary 비스레드안전·인덱서 예외가 함정.
  • 예상 질문:
    1. "dict[key]TryGetValue 차이가 왜 중요한가?" → 없는 키 예외 vs 안전 분기.
    2. "DictionaryConcurrentDictionary 중 무엇을, 왜?" → 다중 스레드 읽기/쓰기엔 후자(또는 락). 일관성 요구가 크면 액터.
    3. "재로그인이 막 들어온 사이 옛 세션이 로그아웃하면?" → 값 일치 조건부 제거로 새 세션 보호.