12. 분산 락(Redis)의 펜싱/만료 (C#)

난이도 상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// 시나리오 (서버-서버 / 분산)
// ----------------------------------------------------------------------------
// 여러 게임 서버 인스턴스가 공유하는 자원(예: 글로벌 거래소 정산, 길드 창고
// 일괄 처리)을 보호하기 위해 Redis 분산 락을 쓴다. 한 번에 한 인스턴스만
// 그 자원을 처리해야 한다.
//  - Acquire 로 락을 잡고, 작업을 한 뒤, Release 로 푼다.
//  - 락에는 TTL 을 걸어, 락을 잡은 인스턴스가 죽어도 언젠가 자동 해제되게 한다.
//  - 작업(정산)은 외부 호출/대량 DB 라 시간이 들쭉날쭉하다. 가끔 GC/스톨로
//    프로세스가 수 초간 멈출 수도 있다.
//  - 여러 인스턴스가 같은 자원에 거의 동시에 Acquire 를 시도한다.
//
// 요구사항:
//   - 같은 자원을 두 인스턴스가 동시에 "처리 중" 이 되면 안 된다(상호 배제).
//   - 락을 잡은 인스턴스가 죽어도 자원이 영영 잠기면 안 된다(데드락 방지).
//   - 한 인스턴스가 푼 락을 다른 인스턴스가 실수로 풀면 안 된다.
//   - TTL 이 지나 락이 만료된 뒤에 "예전 락 보유자" 가 자원에 늦게 기록해
//     자원을 오염시키면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 발생하는지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;

// Redis 클라이언트 추상화(명령 1:1 대응)
public interface IRedis
{
    bool   SetNx(string key, string value);   // SET key value NX (성공 시 true)
    void   Expire(string key, int seconds);    // EXPIRE key seconds
    string Get(string key);                    // GET key
    void   Del(string key);                    // DEL key
}

public class DistributedLock
{
    private readonly IRedis _redis;
    public DistributedLock(IRedis redis) { _redis = redis; }

    private static string K(string resource) => "lock:" + resource;

    // 락 획득. 성공 시 true.
    public bool Acquire(string resource, int ttlSeconds)
    {
        if (!_redis.SetNx(K(resource), "1"))   // (A) 고정값으로 락 표시
            return false;

        _redis.Expire(K(resource), ttlSeconds); // (B) TTL 은 별도 명령으로
        return true;
    }

    // 락 해제
    public void Release(string resource)
    {
        _redis.Del(K(resource));               // (C) 바로 DEL
    }
}

// 보호 자원을 처리하는 잡(여러 인스턴스에서 동시에 호출될 수 있음)
public class SettlementJob
{
    private readonly DistributedLock _lock;
    private readonly IRedis _redis;

    public SettlementJob(DistributedLock l, IRedis r) { _lock = l; _redis = r; }

    public void Run(string resource)
    {
        if (!_lock.Acquire(resource, ttlSeconds: 30))
            return;                            // 다른 인스턴스가 처리 중

        try
        {
            // ... 오래 걸리는 정산 작업 (외부 호출/대량 DB). (D) TTL 보다 길 수 있음.
            //     도중 GC/스톨로 수 초 멈출 수도 있음.
            DoSettlement(resource);            // (E) 락만 믿고 자원에 직접 기록
        }
        finally
        {
            _lock.Release(resource);
        }
    }

    private void DoSettlement(string resource)
    {
        // 거래소 정산 결과를 공유 저장소에 기록 (구현 생략)
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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