15. 오래된 라우팅 캐시와 IP:포트 재사용으로 인한 인스턴스 오인

난이도 중 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 서비스 디스커버리 라우팅 캐시와 인스턴스 재사용
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버):
//   - 매치메이킹 서버는 각 게임 인스턴스 서버(파드/컨테이너)의 "논리 ID → 주소
//     (IP:포트)" 매핑을 서비스 디스커버리로부터 받아 로컬 캐시(RoutingCache)에
//     보관한다.
//   - 캐시는 TTL 동안 재조회를 생략해 디스커버리 조회 부하를 줄인다(주기적
//     갱신, 강제 무효화 없음).
//   - 인스턴스 서버는 배포/재시작 시 오케스트레이터에 의해 같은 IP:포트를
//     재할당받는 경우가 흔하다(파드 재스케줄링, 컨테이너 재생성).
//   - 재시작된 인스턴스는 이전 인스턴스와 논리적으로 무관하다(다른 인스턴스
//     식별자, 초기화된 상태 — 이전에 진행 중이던 매치는 없다).
//
// 상황:
//   - 인스턴스 서버 하나가 재시작됐다(크래시 복구 또는 배포). 재시작 직후
//     같은 IP:포트로 새 인스턴스가 떴는데, 매치메이킹 서버가 캐시된 라우팅
//     정보로 그 주소에 "예전 매치의 후속 요청"을 보내 새 인스턴스가 모르는
//     매치 ID에 대한 오류가 발생하거나, 최악의 경우 새 인스턴스가 우연히
//     같은 매치 ID를 다른 목적으로 재사용해 엉뚱한 요청을 처리해버린다.
//
// 과제:
//   RoutingCache.Resolve(A)와 인스턴스 등록/해제 흐름(B)을 검토하라. 인스턴스가
//   재시작되어 같은 주소를 재사용할 때 캐시가 왜 그것을 감지하지 못하는지
//   설명하고 수정안을 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

namespace GameServer.Discovery
{
    public sealed class InstanceEndpoint
    {
        public string InstanceId;   // 논리적 인스턴스 ID (예: "match-07")
        public string Address;      // "IP:포트"
        public DateTime CachedAt;
    }

    public interface IDiscoveryClient
    {
        // 디스커버리 서버(예: etcd/Consul)에서 최신 등록 정보를 가져온다.
        InstanceEndpoint Lookup(string instanceId);
    }

    public sealed class RoutingCache
    {
        private static readonly TimeSpan Ttl = TimeSpan.FromSeconds(30);
        private readonly Dictionary<string, InstanceEndpoint> _cache = new Dictionary<string, InstanceEndpoint>();
        private readonly IDiscoveryClient _discovery;

        public RoutingCache(IDiscoveryClient discovery)
        {
            _discovery = discovery;
        }

        // 매치메이킹 서버가 요청을 보낼 주소를 얻을 때 호출
        public string Resolve(string instanceId)
        {
            if (_cache.TryGetValue(instanceId, out var cached))
            {
                bool fresh = (DateTime.UtcNow - cached.CachedAt) < Ttl;
                if (fresh)
                {
                    return cached.Address;                          // (A) TTL 이내면 캐시 그대로 반환
                }
            }

            InstanceEndpoint fresh2 = _discovery.Lookup(instanceId);
            _cache[instanceId] = fresh2;                             // (B) 최신 정보로 교체
            return fresh2.Address;
        }

        // 이 서버가 특정 인스턴스로 요청을 보낼 때, 응답에 실려온 인스턴스
        // 식별자로 자신이 기대한 대상과 같은지 확인하지 않고 그대로 신뢰한다.
        public void SendMatchRequest(string instanceId, string matchId, byte[] payload)
        {
            string address = Resolve(instanceId);
            TransportSend(address, matchId, payload);   // 응답의 인스턴스 식별자 검증 없음
        }

        private void TransportSend(string address, string matchId, byte[] payload)
        {
            // 실제 네트워크 전송 (생략)
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.