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++
// ============================================================================
// [코드리뷰 문제] C++ - 시즌 랭킹 마감과 종료 직전 점수 갱신 경합
// ----------------------------------------------------------------------------
// 시나리오:
// - 시즌제 랭킹 보드. 시즌 종료 시각(seasonEndMs)이 미리 정해져 있다.
// - 플레이어가 점수를 얻으면 여러 로직 워커 스레드에서
// AddScore(playerId, delta, eventTimeMs) 가 호출된다.
// eventTimeMs = 그 점수가 "실제로 발생한" 서버 시각.
// - 종료 시각이 지나면 타이머 스레드가 한 번 FinalizeSeason() 을 호출한다:
// (1) 현재 순위를 확정(스냅샷)하고 (2) 상위 N명에게 보상을 지급하고
// (3) 보드를 비워 다음 시즌을 시작한다.
//
// 요구사항:
// - 종료 시각 이전(eventTimeMs <= seasonEndMs)에 발생한 점수는 이번 시즌에 반영.
// 종료 이후 발생분은 다음 시즌으로. (지연 도착해도 발생 시각 기준으로 귀속)
// - 확정/보상 지급은 일관된 스냅샷 위에서 이루어져야 한다.
// - 한 점수가 두 시즌에 중복 반영되거나, 유실되면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 어떤 조건에서 점수가 유실/중복/오귀속되는지
// 설명하고, 수정안과 더 나은 설계를 제시하라. (먼저 직접 리뷰 후 answer.md 와 대조)
// ============================================================================
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <algorithm>
class RewardSink { public: void Grant(int64_t /*pid*/, int /*rank*/) {} };
class Leaderboard {
public:
Leaderboard(int64_t seasonEndMs, RewardSink& sink)
: seasonEndMs_(seasonEndMs), sink_(sink) {}
// 여러 워커 스레드에서 동시에 호출된다.
void AddScore(int64_t playerId, int64_t delta, int64_t eventTimeMs)
{
// (A) 종료 여부를 플래그 하나로만 판단
if (ended_)
return;
// (B) 발생 시각(eventTimeMs)은 보지 않고 무조건 현재 보드에 가산
scores_[playerId] += delta; // (C) 보호 없음
}
// 타이머 스레드에서 종료 시각 도달 시 한 번 호출.
void FinalizeSeason()
{
ended_ = true; // (A) 먼저 종료 플래그를 세운다
// (D) 살아있는 scores_ 를 그대로 복사/정렬하며 보상 지급
std::vector<std::pair<int64_t, int64_t>> ranking(scores_.begin(), scores_.end());
std::sort(ranking.begin(), ranking.end(),
[](auto& a, auto& b) { return a.second > b.second; });
int rank = 1;
for (auto& e : ranking) {
if (rank > kTopN) break;
sink_.Grant(e.first, rank);
++rank;
}
scores_.clear(); // 다음 시즌 리셋
ended_ = false;
}
private:
static constexpr int kTopN = 100;
int64_t seasonEndMs_;
RewardSink& sink_;
std::unordered_map<int64_t, int64_t> scores_;
bool ended_ = false;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.