12. ValueTask vs Task (할당·풀링·"한 번만 소비" 규칙)

난이도 상 해설 보기 →

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

결함 코드 · C#
// 주제: ValueTask<T> vs Task<T> — 할당/풀링, 동기 완료 경로, "정확히 한 번만 소비" 규칙.
//
// 아래 코드의 동작과 위험을 예측·설명하라:
//  (1) 캐시 적중(동기 완료) 시 ValueTask<T> 가 Task<T> 대비 무엇을 아끼는가? 게임 핫패스에서 왜 의미가 있나?
//  (2) (A): 같은 ValueTask 를 두 번 await 하면 무엇이 잘못되는가? (특히 풀링되는 백킹일 때)
//  (3) (B): ValueTask 를 변수에 담아 두고 나중에 .Result 로 읽으면?
//  (4) ValueTask 를 여러 번/늦게 소비하고 싶을 땐 어떻게 해야 하나?
//  (5) 메서드 반환 타입으로 Task 와 ValueTask 중 무엇을 언제 골라야 하나?

using System.Collections.Generic;
using System.Threading.Tasks;

public sealed class PlayerData { public long Id; }
public interface IPlayerDb { Task<PlayerData> FetchAsync(long id); }

public sealed class PlayerCache
{
    private readonly Dictionary<long, PlayerData> _hot = new();
    private readonly IPlayerDb _db;
    public PlayerCache(IPlayerDb db) => _db = db;

    // 캐시에 있으면 동기적으로 즉시 반환, 없으면 DB 비동기 조회.
    public ValueTask<PlayerData> GetAsync(long id)
    {
        if (_hot.TryGetValue(id, out var p))
            return new ValueTask<PlayerData>(p);          // 동기 완료 경로
        return new ValueTask<PlayerData>(LoadAsync(id));  // 비동기 경로
    }

    private async Task<PlayerData> LoadAsync(long id)
    {
        var data = await _db.FetchAsync(id);
        _hot[id] = data;
        return data;
    }
}

public static class Consumer
{
    public static async Task UseTwiceAsync(PlayerCache cache, long id)
    {
        ValueTask<PlayerData> vt = cache.GetAsync(id);
        var first  = await vt;          // (A)
        var second = await vt;          // (A) 같은 vt 를 또 await
        System.GC.KeepAlive(first); System.GC.KeepAlive(second);
    }

    public static async Task UseLaterAsync(PlayerCache cache, long id)
    {
        ValueTask<PlayerData> vt = cache.GetAsync(id);
        await Task.Delay(100);
        var data = vt.Result;           // (B) 완료 전일 수 있는데 .Result 로 동기 접근
        System.GC.KeepAlive(data);
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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