13. 시즌 랭킹 마감과 종료 직전 점수 갱신 경합 · C#

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 시즌 랭킹 마감과 종료 직전 점수 갱신 경합
// ----------------------------------------------------------------------------
// 시나리오:
//   - 시즌제 랭킹 보드. 시즌 종료 시각(SeasonEndMs)이 미리 정해져 있다.
//   - 플레이어가 점수를 얻으면 여러 로직 워커 스레드에서
//       AddScore(playerId, delta, eventTimeMs) 가 호출된다.
//     eventTimeMs = 그 점수가 "실제로 발생한" 서버 시각.
//   - 종료 시각이 지나면 타이머 스레드가 한 번 FinalizeSeason() 을 호출한다:
//       (1) 현재 순위를 확정(스냅샷)하고 (2) 상위 N명에게 보상을 지급하고
//       (3) 보드를 비워 다음 시즌을 시작한다.
//
// 요구사항:
//   - 종료 시각 이전(eventTimeMs <= SeasonEndMs)에 발생한 점수는 이번 시즌에 반영.
//     종료 이후 발생분은 다음 시즌으로. (지연 도착해도 발생 시각 기준으로 귀속)
//   - 확정/보상 지급은 일관된 스냅샷 위에서 이루어져야 한다.
//   - 한 점수가 두 시즌에 중복 반영되거나, 유실되면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 어떤 조건에서 점수가 유실/중복/오귀속되는지
//   설명하고, 수정안과 더 나은 설계를 제시하라. (먼저 직접 리뷰 후 answer.md 와 대조)
// ============================================================================

using System;
using System.Collections.Generic;
using System.Linq;

public interface IRewardSink { void Grant(long playerId, int rank); }

public class Leaderboard
{
    private const int TopN = 100;

    private readonly long _seasonEndMs;
    private readonly IRewardSink _sink;
    private readonly Dictionary<long, long> _scores = new Dictionary<long, long>();
    private bool _ended = false;

    public Leaderboard(long seasonEndMs, IRewardSink sink)
    {
        _seasonEndMs = seasonEndMs;
        _sink = sink;
    }

    // 여러 워커 스레드에서 동시에 호출된다.
    public void AddScore(long playerId, long delta, long eventTimeMs)
    {
        // (A) 종료 여부를 플래그 하나로만 판단
        if (_ended)
            return;

        // (B) 발생 시각(eventTimeMs)은 보지 않고 무조건 현재 보드에 가산
        if (!_scores.TryGetValue(playerId, out long cur)) cur = 0;
        _scores[playerId] = cur + delta;   // (C) 보호 없음
    }

    // 타이머 스레드에서 종료 시각 도달 시 한 번 호출.
    public void FinalizeSeason()
    {
        _ended = true;                      // (A) 먼저 종료 플래그를 세운다

        // (D) 살아있는 _scores 를 그대로 정렬/순회하며 보상 지급
        var ranking = _scores.OrderByDescending(kv => kv.Value).ToList();

        int rank = 1;
        foreach (var kv in ranking)
        {
            if (rank > TopN) break;
            _sink.Grant(kv.Key, rank);
            rank++;
        }

        _scores.Clear();                    // 다음 시즌 리셋
        _ended = false;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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