23. 요청 데드라인 전파 부재와 재시도 증폭 (C#)

난이도 상 해설 보기 →

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

결함 코드 · C#
// [시나리오]
// 게임 백엔드는 게이트웨이 → 랭킹 서비스 → (통계 서비스, 프로필 서비스, DB) 로 이어지는
// 다단계 호출 체인을 갖는다. 클라이언트는 랭킹 화면을 열 때 게이트웨이에 요청하고,
// 게이트웨이는 5초 안에 응답하지 못하면 클라에 타임아웃을 반환한다.
//
// 각 서비스는 자기 나름의 타임아웃과 재시도 정책을 갖고 있다.
//   게이트웨이   : 전체 5초, 재시도 2회
//   랭킹 서비스  : 하위 호출당 3초, 재시도 3회
//   통계 서비스  : DB 쿼리 10초
//
// 운영 중 보고된 현상:
//   - 통계 서비스가 느려지면 게이트웨이는 이미 타임아웃으로 클라에 실패를 반환했는데,
//     하위 서비스들의 CPU/DB 부하는 오히려 계속 올라간다.
//   - 부하가 걷히지 않고 오래 지속되며, 회복이 느리다.
//   - 랭킹 서비스 로그에는 "완료"된 요청이 게이트웨이 로그의 요청 수보다 훨씬 많다.
//
// [요구사항]
// - 클라이언트가 더 이상 기다리지 않는 작업은 하위 계층에서도 중단되어야 한다.
// - 한 요청의 총 소요 시간은 체인 전체에 걸쳐 상한이 있어야 한다.
//
// [과제]
// (A), (B) 지점을 중심으로 위 현상이 발생하는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace GameServer.Distributed
{
    // ---- 게이트웨이 ----
    public sealed class GatewayHandler
    {
        private readonly RankingClient _ranking = new();

        public async Task<string> GetRankingAsync(long accountId)
        {
            // (A)
            for (int attempt = 0; attempt < 3; attempt++)
            {
                using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                try
                {
                    return await _ranking.FetchAsync(accountId, cts.Token);
                }
                catch (OperationCanceledException)
                {
                    // 재시도
                }
            }
            throw new TimeoutException("ranking unavailable");
        }
    }

    public sealed class RankingClient
    {
        private static readonly HttpClient Http = new();

        public async Task<string> FetchAsync(long accountId, CancellationToken ct)
        {
            var req = new HttpRequestMessage(HttpMethod.Get, $"http://ranking/v1/{accountId}");
            var res = await Http.SendAsync(req, ct);
            return await res.Content.ReadAsStringAsync(ct);
        }
    }

    // ---- 랭킹 서비스 ----
    public sealed class RankingService
    {
        private readonly StatsClient _stats = new();
        private readonly ProfileClient _profile = new();

        public async Task<string> HandleAsync(long accountId)
        {
            // (B)
            var statsTask = CallWithRetryAsync(
                ct => _stats.GetAsync(accountId, ct), retries: 3, timeout: TimeSpan.FromSeconds(3));
            var profileTask = CallWithRetryAsync(
                ct => _profile.GetAsync(accountId, ct), retries: 3, timeout: TimeSpan.FromSeconds(3));

            var stats = await statsTask;
            var profile = await profileTask;
            return Combine(stats, profile);
        }

        private static async Task<string> CallWithRetryAsync(
            Func<CancellationToken, Task<string>> call, int retries, TimeSpan timeout)
        {
            for (int i = 0; i < retries; i++)
            {
                using var cts = new CancellationTokenSource(timeout);
                try { return await call(cts.Token); }
                catch (OperationCanceledException) { }
                catch (HttpRequestException) { }
            }
            throw new TimeoutException();
        }

        private static string Combine(string a, string b) => a + b;
    }

    public sealed class StatsClient
    {
        public Task<string> GetAsync(long id, CancellationToken ct) => Task.FromResult("");
    }
    public sealed class ProfileClient
    {
        public Task<string> GetAsync(long id, CancellationToken ct) => Task.FromResult("");
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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