25. 업적 달성 판정과 동시 다발 이벤트 집계

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 업적 달성 판정과 동시 다발 이벤트 집계
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
//   - 업적은 누적 카운터에 기반한다. 예: "몬스터 100마리 처치", "골드 1,000,000 누적 획득".
//   - 전투/획득 이벤트는 여러 워커 스레드에서 동시에 발생한다(몹 처치, 골드 드롭 등).
//     각 이벤트가 해당 업적의 진행도(progress)를 증가시킨다.
//   - 진행도가 목표치(threshold)에 도달하면 그 업적은 "달성" 으로 1회만 처리되고,
//     보상(타이틀/재화/아이템)을 정확히 한 번만 지급한다.
//
// 요구사항:
//   - 동시 이벤트에서도 진행도 증가가 유실되면 안 된다(정확한 누적).
//   - 업적 달성/보상 지급은 정확히 한 번만 일어나야 한다(이중 지급 금지).
//   - 이미 달성한 업적은 추가 이벤트가 들어와도 다시 보상하지 않는다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 진행도 유실·이중 달성·보상 중복이
//   동시 인터리빙으로 발생하는지 설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class Achievement
{
    public int  AchievementId;
    public long Threshold;       // 목표치
    public long Progress;        // 누적 진행도
    public bool Unlocked;        // 달성 여부
}

public class AchievementService
{
    // playerId -> (achievementId -> Achievement)
    private readonly Dictionary<long, Dictionary<int, Achievement>> _byPlayer = new();
    private readonly Action<long, int> _grantReward;   // (playerId, achievementId)

    public AchievementService(Action<long, int> grantReward)
    {
        _grantReward = grantReward;
    }

    // 여러 워커 스레드에서 동시 호출: 이벤트가 진행도를 amount 만큼 증가
    public void OnProgress(long playerId, int achievementId, long amount)
    {
        if (!_byPlayer.TryGetValue(playerId, out var map)) return;
        if (!map.TryGetValue(achievementId, out var ach)) return;
        if (ach.Unlocked) return;                  // 이미 달성했으면 무시

        // (A) 진행도 누적
        ach.Progress += amount;

        // (B) 목표 도달 시 달성 처리 + 보상
        if (ach.Progress >= ach.Threshold)
        {
            ach.Unlocked = true;
            _grantReward(playerId, achievementId);
        }
    }

    // 신규 업적 등록(플레이어 로그인 시 등)
    public void Register(long playerId, Achievement ach)
    {
        if (!_byPlayer.TryGetValue(playerId, out var map))
        {
            map = new Dictionary<int, Achievement>();
            _byPlayer[playerId] = map;
        }
        // (C) 동시 등록/조회와의 경합 고려 없음
        map[ach.AchievementId] = ach;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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