12. 분산 스케줄러의 조정 장치 부재로 인한 배치 중복 실행
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오] 일일 보상 지급 배치 스케줄러 (서버-서버)
// 게임서버 클러스터는 여러 노드(인스턴스)로 이중화되어 있고, 각 노드는 독립적으로
// 기동되며 "매일 00:00 KST 에 접속 보상을 모든 온라인 플레이어에게 지급"하는
// 배치 작업을 자체 스케줄러(타이머)로 실행한다. 노드들은 서로 다른 프로세스이며
// 클러스터 내 어떤 노드가 "이번 배치를 담당할지"를 정하는 조정 장치는 없다 —
// 각 노드가 각자의 로컬 시계로 자정을 감지하면 곧바로 배치를 실행한다.
//
// [요구사항]
// - 일일 보상 지급 배치는 클러스터 전체에서 하루에 정확히 한 번만 실행되어야 한다.
// - 노드가 여러 대 떠 있어도(스케일 아웃/이중화) 위 요구사항이 지켜져야 한다.
//
// [과제] DailyRewardScheduler.RunIfDue 를 검토하고, 클러스터에 노드가 2대 이상
// 떠 있을 때 위 요구사항이 어떻게 어긋날 수 있는지 원인과 수정안을 제시하라.
// 표시된 (A)(B) 지점에 주목하라.
using System;
using System.Threading;
using System.Threading.Tasks;
class DailyRewardScheduler
{
private DateTime _lastRunDate = DateTime.MinValue; // 이 노드가 마지막으로 실행한 날짜
private readonly Timer _timer;
public DailyRewardScheduler()
{
// 1분마다 자정 여부를 확인
_timer = new Timer(_ => RunIfDue(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
}
private void RunIfDue()
{
var today = DateTime.UtcNow.Date;
// (A)
if (_lastRunDate >= today)
return;
// (B)
RunDailyRewardBatch();
_lastRunDate = today;
}
private void RunDailyRewardBatch()
{
// 온라인 플레이어 전원에게 접속 보상 지급
foreach (var player in GetOnlinePlayers())
{
GrantLoginReward(player);
}
}
private System.Collections.Generic.IEnumerable<int> GetOnlinePlayers() => Array.Empty<int>();
private void GrantLoginReward(int playerId) { /* 보상 지급 */ }
} 결함 코드 · C++
// [시나리오] 일일 보상 지급 배치 스케줄러 (서버-서버)
// 게임서버 클러스터는 여러 노드로 이중화되어 있고, 각 노드는 "매일 00:00 KST 에
// 접속 보상을 모든 온라인 플레이어에게 지급"하는 배치를 자체 타이머로 실행하려 시도한다.
// 중복 실행을 막기 위해 Redis 같은 외부 저장소에 "오늘 날짜"를 키로 한 분산 락을
// 짧게 획득해 "내가 이번 배치의 담당자"임을 표시하는 방식을 도입했다. 배치 자체
// (모든 온라인 플레이어 순회+지급)는 수만 명 규모에서 수 분이 걸릴 수 있다.
//
// [요구사항]
// - 일일 보상 지급 배치는 클러스터 전체에서 하루에 정확히 한 번만 실행되어야 한다.
// - 배치 실행 시간이 길어지더라도(수 분 소요) 다른 노드가 같은 배치를 중복
// 실행해서는 안 된다.
//
// [과제] DailyRewardScheduler::runIfDue 를 검토하고, 클러스터에 노드가 2대 이상
// 떠 있을 때 위 요구사항이 어떻게 어긋날 수 있는지 원인과 수정안을 제시하라.
// 표시된 (A)(B) 지점에 주목하라.
#include <string>
#include <vector>
#include <chrono>
// Redis 등 외부 저장소를 흉내낸 간단한 분산 락 클라이언트 인터페이스.
class IDistributedLockClient {
public:
virtual ~IDistributedLockClient() = default;
// key 에 대해 락 획득 시도. ttlMs 동안만 유효. 성공하면 true.
virtual bool TryAcquire(const std::string& key, int ttlMs) = 0;
virtual void Release(const std::string& key) = 0;
};
class DailyRewardScheduler {
IDistributedLockClient& lockClient_;
std::string lastRunDate_; // 이 노드가 마지막으로 배치를 실행한 날짜(로컬 캐시, 참고용)
public:
explicit DailyRewardScheduler(IDistributedLockClient& lockClient)
: lockClient_(lockClient) {}
// 1분마다 스케줄러가 호출한다고 가정.
void runIfDue(const std::string& todayKey /* 예: "reward:2026-07-04" */) {
if (lastRunDate_ == todayKey)
return;
// (A)
if (!lockClient_.TryAcquire(todayKey, /*ttlMs=*/5000))
return; // 다른 노드가 이미 담당 중이거나, 오늘 이미 처리됨
lastRunDate_ = todayKey;
// (B)
runDailyRewardBatch();
lockClient_.Release(todayKey);
}
private:
void runDailyRewardBatch() {
// 온라인 플레이어 전원에게 접속 보상 지급 (수만 명, 수 분 소요 가능)
for (int playerId : getOnlinePlayers()) {
grantLoginReward(playerId);
}
}
std::vector<int> getOnlinePlayers() { return {}; }
void grantLoginReward(int /*playerId*/) { /* 보상 지급 */ }
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.