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++
// ============================================================================
// 시나리오 (서버-서버 / 분산)
// ----------------------------------------------------------------------------
// 여러 게임 서버 인스턴스가 공유하는 자원(예: 글로벌 거래소 정산, 길드 창고
// 일괄 처리)을 보호하기 위해 Redis 분산 락을 쓴다. 한 번에 한 인스턴스만
// 그 자원을 처리해야 한다.
// - Acquire 로 락을 잡고, 작업을 한 뒤, Release 로 푼다.
// - 락에는 TTL 을 걸어, 락을 잡은 인스턴스가 죽어도 언젠가 자동 해제되게 한다.
// - 작업(정산)은 외부 호출/대량 DB 라 시간이 들쭉날쭉하다. 가끔 스톨로
// 프로세스가 수 초간 멈출 수도 있다.
// - 여러 인스턴스가 같은 자원에 거의 동시에 Acquire 를 시도한다.
//
// 요구사항:
// - 같은 자원을 두 인스턴스가 동시에 "처리 중" 이 되면 안 된다(상호 배제).
// - 락을 잡은 인스턴스가 죽어도 자원이 영영 잠기면 안 된다(데드락 방지).
// - 한 인스턴스가 푼 락을 다른 인스턴스가 실수로 풀면 안 된다.
// - TTL 이 지나 락이 만료된 뒤에 "예전 락 보유자" 가 자원에 늦게 기록해
// 자원을 오염시키면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 발생하는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <string>
// Redis 클라이언트 추상화(명령 1:1 대응)
class IRedis
{
public:
virtual bool SetNx(const std::string& key, const std::string& val) = 0; // SET NX
virtual void Expire(const std::string& key, int seconds) = 0; // EXPIRE
virtual std::string Get(const std::string& key) = 0; // GET
virtual void Del(const std::string& key) = 0; // DEL
virtual ~IRedis() = default;
};
class DistributedLock
{
public:
explicit DistributedLock(IRedis* redis) : redis_(redis) {}
// 락 획득. 성공 시 true.
bool Acquire(const std::string& resource, int ttlSeconds)
{
if (!redis_->SetNx(K(resource), "1")) // (A) 고정값으로 락 표시
return false;
redis_->Expire(K(resource), ttlSeconds); // (B) TTL 은 별도 명령으로
return true;
}
// 락 해제
void Release(const std::string& resource)
{
redis_->Del(K(resource)); // (C) 바로 DEL
}
private:
static std::string K(const std::string& r) { return "lock:" + r; }
IRedis* redis_;
};
// 보호 자원을 처리하는 잡(여러 인스턴스에서 동시에 호출될 수 있음)
class SettlementJob
{
public:
SettlementJob(DistributedLock* l, IRedis* r) : lock_(l), redis_(r) {}
void Run(const std::string& resource)
{
if (!lock_->Acquire(resource, /*ttlSeconds=*/30))
return; // 다른 인스턴스가 처리 중
// ... 오래 걸리는 정산 작업 (외부 호출/대량 DB). (D) TTL 보다 길 수 있음.
// 도중 스톨로 수 초 멈출 수도 있음.
DoSettlement(resource); // (E) 락만 믿고 자원에 직접 기록
lock_->Release(resource);
}
private:
void DoSettlement(const std::string& /*resource*/)
{
// 거래소 정산 결과를 공유 저장소에 기록 (구현 생략)
}
DistributedLock* lock_;
IRedis* redis_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.