26. 기간제 아이템 TTL 만료 정리 잡과 연장의 경합 (C#)

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# — 기간제 아이템 TTL 만료 정리 잡과 연장(renew)의 경합
// ----------------------------------------------------------------------------
// 시나리오:
//   기간제(임대) 아이템은 DB 테이블 rented_items(item_id PK, owner_id, expire_at)에
//   저장된다. 백그라운드 정리 잡(ExpireSweeper)이 주기적으로 만료된 행을 삭제한다.
//   한편 플레이어는 만료 전에 아이템을 "연장"할 수 있고, 이때 expire_at 이 미래로
//   갱신된다(UPDATE). 정리 잡과 연장 요청은 서로 다른 커넥션/트랜잭션에서 동시에 일어난다.
//
// 요구사항:
//   - 만료된 아이템만 삭제한다. 연장으로 유효해진 아이템은 절대 삭제하면 안 된다.
//
// 과제: (A) (B) 지점의 동작을 설명하고 수정안을 제시하라.
// ============================================================================

using System;
using System.Collections.Generic;

public interface IDb
{
    List<long> Query(string sql, object args);      // id 목록 반환
    int Execute(string sql, object args);           // 영향받은 행 수
}

public sealed class ExpireSweeper
{
    private readonly IDb _db;
    public ExpireSweeper(IDb db) => _db = db;

    public void SweepOnce()
    {
        var now = DateTime.UtcNow;

        // 1) 만료 후보를 먼저 조회한다.
        var expiredIds = _db.Query(
            "SELECT item_id FROM rented_items WHERE expire_at < @now",
            new { now });                            // (A)

        if (expiredIds.Count == 0) return;

        // 2) 조회된 id 를 삭제한다.
        _db.Execute(
            "DELETE FROM rented_items WHERE item_id IN @ids",
            new { ids = expiredIds });               // (B)
    }
}

// 별도 경로: 플레이어의 연장 요청
public sealed class RentalService
{
    private readonly IDb _db;
    public RentalService(IDb db) => _db = db;

    public void Renew(long itemId, TimeSpan extend)
    {
        var newExpire = DateTime.UtcNow + extend;
        _db.Execute(
            "UPDATE rented_items SET expire_at = @newExpire WHERE item_id = @itemId",
            new { newExpire, itemId });
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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