10. C# 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)
// ----------------------------------------------------------------------------
// 시나리오 (배포/오토스케일 다운 시 무중단 마이그레이션):
// - 게이트웨이 노드 풀. 배포·스케일인 시 특정 노드를 "드레이닝(draining)"으로
// 전환하고, 그 노드의 활성 세션을 다른 노드로 핸드오프한 뒤 종료한다.
// - 핸드오프: (1) 대상 노드에 세션 상태(스냅샷)를 전송 → (2) 클라에 "새 노드로
// 재접속하라" 지시(migrate ticket 발급) → (3) 클라가 새 노드에 재접속 →
// (4) 옛 노드에서 세션 제거.
// - Drain 시작 후에도 새 접속 시도, in-flight 요청, 진행 중 핸드오프가 섞인다.
// - 여러 스레드(수신/타이머/드레인 트리거)가 동시에 세션 테이블을 만진다.
//
// 요구사항:
// - 드레이닝 중 진행 중 작업은 끝까지 처리(graceful), 새 접속은 막거나 리다이렉트.
// - 핸드오프 중 메시지 유실/중복/순서 뒤바뀜 없이 한 노드에서만 활성이어야 한다
// (split-brain 금지: 두 노드가 동시에 같은 세션을 활성으로 보지 않기).
// - 옛 노드는 모든 세션이 비워진 뒤에만 종료한다(강제 종료로 유저 끊김 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public sealed class Session
{
public long Id;
public bool Active = true; // (A)
public byte[] Snapshot() => Array.Empty<byte>(); // 상태 직렬화(생략)
public void RedirectTo(string node) { /* 클라에 migrate ticket 전송 */ }
public void Close() { /* 소켓 종료 */ }
}
public sealed class GatewayNode
{
private readonly Dictionary<long, Session> _sessions = new Dictionary<long, Session>();
private readonly object _lock = new object();
public volatile bool Draining = false; // (B)
// 신규 접속 처리 (수신 스레드)
public Session OnNewConnection(long sessionId)
{
if (Draining) return null; // (C) 드레이닝이면 거절
var s = new Session { Id = sessionId };
lock (_lock) { _sessions[sessionId] = s; }
return s;
}
// 드레인 시작: 모든 세션을 targetNode 로 핸드오프하고 종료 준비
public async Task DrainAndHandoffAsync(string targetNode, IPeer peer)
{
Draining = true; // (B)
List<Session> snapshot;
lock (_lock) { snapshot = new List<Session>(_sessions.Values); } // (D)
foreach (var s in snapshot)
{
// 1) 대상 노드로 상태 전송
await peer.SendSnapshotAsync(targetNode, s.Id, s.Snapshot()); // (E)
// 2) 클라에 새 노드로 재접속 지시
s.RedirectTo(targetNode); // (F)
// 3) 옛 노드에서 제거
lock (_lock) { _sessions.Remove(s.Id); }
s.Close(); // (G) 즉시 닫음
}
// 모든 세션 처리 끝 → 프로세스 종료
Environment.Exit(0); // (H)
}
// in-flight 게임 요청 처리 (수신 스레드)
public void HandleRequest(long sessionId, byte[] req)
{
Session s;
lock (_lock) { _sessions.TryGetValue(sessionId, out s); }
if (s == null) return;
// ... 요청 처리 (드레이닝/핸드오프 진행 여부와 무관하게 그냥 처리) (I)
}
}
public interface IPeer
{
Task SendSnapshotAsync(string node, long sessionId, byte[] snapshot);
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)
// ----------------------------------------------------------------------------
// 시나리오 (배포/오토스케일 다운 시 무중단 마이그레이션):
// - 게이트웨이 노드 풀. 배포·스케일인 시 특정 노드를 "드레이닝(draining)"으로
// 전환하고, 그 노드의 활성 세션을 다른 노드로 핸드오프한 뒤 종료한다.
// - 핸드오프: (1) 대상 노드에 세션 상태(스냅샷)를 전송 → (2) 클라에 "새 노드로
// 재접속하라" 지시(migrate ticket 발급) → (3) 클라가 새 노드에 재접속 →
// (4) 옛 노드에서 세션 제거.
// - Drain 시작 후에도 새 접속 시도, in-flight 요청, 진행 중 핸드오프가 섞인다.
// - 여러 스레드(수신/타이머/드레인 트리거)가 동시에 세션 테이블을 만진다.
//
// 요구사항:
// - 드레이닝 중 진행 중 작업은 끝까지 처리(graceful), 새 접속은 막거나 리다이렉트.
// - 핸드오프 중 메시지 유실/중복/순서 뒤바뀜 없이 한 노드에서만 활성이어야 한다
// (split-brain 금지: 두 노드가 동시에 같은 세션을 활성으로 보지 않기).
// - 옛 노드는 모든 세션이 비워진 뒤에만 종료한다(강제 종료로 유저 끊김 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <unordered_map>
#include <vector>
#include <mutex>
#include <future>
#include <string>
#include <cstdint>
#include <cstdlib>
class Session {
public:
int64_t id;
bool active = true; // (A)
std::vector<char> Snapshot() { return {}; } // 상태 직렬화(생략)
void RedirectTo(const std::string& node) { /* 클라에 migrate ticket 전송 */ }
void Close() { /* 소켓 종료 */ }
};
class IPeer {
public:
virtual ~IPeer() = default;
virtual std::future<void> SendSnapshotAsync(const std::string& node,
int64_t sessionId,
const std::vector<char>& snapshot) = 0;
};
class GatewayNode {
std::unordered_map<int64_t, Session*> sessions_; // raw 포인터
std::mutex mtx_;
public:
bool draining = false; // (B)
// 신규 접속 처리 (수신 스레드)
Session* OnNewConnection(int64_t sessionId) {
if (draining) return nullptr; // (C) 드레이닝이면 거절
auto* s = new Session();
s->id = sessionId;
{ std::lock_guard<std::mutex> lk(mtx_); sessions_[sessionId] = s; }
return s;
}
// 드레인 시작: 모든 세션을 targetNode 로 핸드오프하고 종료 준비
void DrainAndHandoff(const std::string& targetNode, IPeer& peer) {
draining = true; // (B)
std::vector<Session*> snapshot;
{ std::lock_guard<std::mutex> lk(mtx_);
for (auto& kv : sessions_) snapshot.push_back(kv.second); } // (D)
for (auto* s : snapshot) {
// 1) 대상 노드로 상태 전송 (future 블로킹 대기)
peer.SendSnapshotAsync(targetNode, s->id, s->Snapshot()).get(); // (E)
// 2) 클라에 새 노드로 재접속 지시
s->RedirectTo(targetNode); // (F)
// 3) 옛 노드에서 제거
{ std::lock_guard<std::mutex> lk(mtx_); sessions_.erase(s->id); }
s->Close(); // (G) 즉시 닫고
delete s; // 즉시 해제
}
// 모든 세션 처리 끝 → 프로세스 종료
std::exit(0); // (H)
}
// in-flight 게임 요청 처리 (수신 스레드)
void HandleRequest(int64_t sessionId, const std::vector<char>& req) {
Session* s = nullptr;
{ std::lock_guard<std::mutex> lk(mtx_);
auto it = sessions_.find(sessionId);
if (it != sessions_.end()) s = it->second; }
if (s == nullptr) return;
// ... 요청 처리 (드레이닝/핸드오프 진행 여부와 무관하게 그냥 처리) (I)
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.