← 문제로

9. 스캐터-개더 부분 응답을 완전 응답으로 오인 + 무경계 팬아웃·집계 경합 (C#)

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

해설 — 스캐터-개더 부분 응답을 완전 응답으로 오인 + 무경계 팬아웃·집계 경합 (C#)

난이도: 상

요약

전 샤드에 팬아웃해 재화 총량을 합산하는데, (1) catch { } 로 실패 샤드를 조용히 건너뛰고 총합을 "완전한 값"처럼 반환하며, (2) 각 호출에 타임아웃이 없어 느린 샤드 하나가 Task.WhenAll 을 무한정 매달고, (3) total += part 를 여러 Task.Run동기화 없이 수행해 경합으로 갱신 유실이 난다.

문제점

  • [부분 결과를 완전으로 오인] (A) return total; + catch { }
    • 증상: 샤드가 예외를 던지면 catch {} 가 삼켜 그 기여가 0인 채 합산 → 언더카운트된 총량을 글로벌 총량으로 신뢰. 인플레 감시·환수 정책이 잘못 판단된다.
    • 재현: 샤드 3개 중 1개 다운 → 그만큼 적은 총량이 정상값처럼 반환.
    • 근본원인: 응답/전체 샤드 수와 완전성 표시 부재. 결과 타입이 부분 실패를 표현하지 못함.
  • [무경계 대기 / 타임아웃 부재] await Task.WhenAll(tasks)
    • 증상: 한 샤드 호출이 hang → WhenAll 이 영원히 완료되지 않아 요청·스레드풀 자원 점유.
    • 근본원인: 퍼-샤드 CancellationToken/데드라인 부재.
  • [집계 경합] (B) total += part;
    • 증상: 여러 Task.Run 이 같은 total 을 비원자 증가 → 읽고-더하고-쓰는 사이 갱신 유실. Interlocked/락 없음.
    • 근본원인: 공유 누산기 동기화 부재.

수정안

퍼-샤드 타임아웃 + 성공/실패를 값으로 수집 + 완전성 플래그 명시. 누산은 결과 배열로 모은 뒤 합산.

public readonly record struct AggResult(ulong Total, int Responded, int TotalShards)
{
    public bool Complete => Responded == TotalShards;
}

public async Task<AggResult> GlobalCurrencyTotalAsync(TimeSpan perShardTimeout)
{
    async Task<(ulong v, bool ok)> QueryOne(IShardClient sc)
    {
        using var cts = new CancellationTokenSource(perShardTimeout);
        try
        {
            // 클라이언트가 토큰을 받는다면 전달; 아니면 Task.WhenAny 로 타임아웃
            var call = sc.QueryCurrencyTotalAsync();
            var done = await Task.WhenAny(call, Task.Delay(perShardTimeout, cts.Token));
            if (done != call) return (0, false);          // 타임아웃 → 미응답
            cts.Cancel();
            return (await call, true);
        }
        catch { return (0, false); }                       // 실패를 값으로 표현
    }

    var results = await Task.WhenAll(_shards.Select(QueryOne)); // 예외 없이 전부 완료
    ulong total = 0; int responded = 0;
    foreach (var (v, ok) in results)                       // 단일 스레드 합산(경합 없음)
        if (ok) { total += v; responded++; }
    return new AggResult(total, responded, _shards.Count);
}
// 호출부: if (!r.Complete) { /* 권위 결정 보류 또는 '부분/저하'로 표시 */ }
  • 샤드가 매우 많으면 SemaphoreSlim 으로 유계 동시성 팬아웃.
  • 공유 누산이 꼭 필요하면 Interlocked.Add 로, 단 결과 배열 합산이 더 단순·안전.

더 나은 설계

  • 결과 타입에 완전성을 담아라: (Total, Responded, TotalShards, AsOf). 호출자가 "권위 결정"인지 "대시보드 추정"인지에 따라 다르게 쓰게. 부분값을 완전값처럼 흘리는 게 핵심 결함.
  • 권위 불변식은 단일 출처: 전역 총량을 정책에 쓰려면 분산 합산보다 중앙 원장/이벤트 집계가 안전. 스캐터-개더는 부분 실패에 취약.
  • 타임아웃·서킷브레이커·격벽: 느린 샤드 격리(distributed_systems/problem7) — 단 여기 강조는 "결과 신뢰성".
  • catch {} 금지: 실패를 삼키지 말고 값(ok=false)으로 표현해 집계가 알게 하라.

면접 포인트

  • 부분 실패를 0으로 삼키는 위험과, 결과에 완전성/응답 비율을 담는 설계.
  • Task.WhenAll 의 무경계 대기 문제와 퍼-샤드 타임아웃(WhenAny+Delay/토큰) 패턴.
  • 전역 합산 불변식의 분산 집계 한계와 중앙 원장/이벤트 집계 대안, Interlocked vs 결과 배열 합산.