21. 일일/주간 초기화 시점과 진행도 갱신이 겹치는 상황 (C#)

난이도 상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 일일/주간 초기화 시점과 진행도 갱신이 겹치는 상황
// ----------------------------------------------------------------------------
// 시나리오 (영속화/동시성 · 콘텐츠):
//   - 플레이어는 "일일 미션" 진행도(처치 수 등)를 가진다. 매일 자정(서버 기준)에
//     스케줄러가 모든 플레이어의 일일 진행도를 0 으로 초기화(reset)한다.
//   - 동시에 게임 로직 스레드들은 플레이어 활동마다 진행도를 증가시킨다(AddProgress).
//     자정 근처에도 플레이어는 계속 플레이 중일 수 있다.
//   - 초기화는 단일 스케줄러 스레드가 전체 플레이어를 순회하며 수행하고, AddProgress 는
//     여러 게임 워커 스레드에서 동시에 호출된다.
//
// 요구사항:
//   - 초기화 경계 직전/직후의 진행도 증가가 "유실" 되거나 "엉뚱한 날짜에 귀속" 되면 안 된다.
//   - 일일 진행도는 자정에 정확히 한 번만 0 으로 리셋돼야 한다(이중 리셋/리셋 누락 금지).
//   - 초기화 순회 중 자료구조가 손상되거나 예외가 나면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 진행도 유실·오귀속·손상이 발생하는지
//   (동시 인터리빙 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System.Collections.Generic;

public class DailyMission
{
    public int  Day          = 0;    // 이 진행도가 속한 "날짜"(예: epoch day)
    public int  Progress     = 0;    // 오늘 누적 진행도
    public int  Target       = 10;   // 완료 목표
    public bool ClaimedToday = false;
}

public class DailyMissionManager
{
    private readonly Dictionary<long, DailyMission> _players = new();

    // 게임 워커 스레드들이 동시에 호출
    public void AddProgress(long playerId, int amount, int currentDay)
    {
        // (A) 없으면 기본 생성(인덱서 set)
        if (!_players.TryGetValue(playerId, out var dm))
        { dm = new DailyMission(); _players[playerId] = dm; }

        // (B) 날짜가 바뀌었으면 먼저 리셋하고 누적
        if (dm.Day != currentDay)
        {
            dm.Day = currentDay;
            dm.Progress = 0;
            dm.ClaimedToday = false;
        }
        dm.Progress += amount;          // (C) 진행도 누적
    }

    // 스케줄러 스레드가 자정에 1회 호출: 전체 플레이어 일일 진행도 초기화
    public void ResetAll(int newDay)
    {
        foreach (var kv in _players)    // (D) 전체 순회하며 리셋
        {
            var dm = kv.Value;
            dm.Day = newDay;
            dm.Progress = 0;
            dm.ClaimedToday = false;
        }
    }

    public bool IsComplete(long playerId)
        => _players.TryGetValue(playerId, out var dm) && dm.Progress >= dm.Target;
}
내 리뷰 · C#
내 답안 · 자동 저장

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