27. 세션 타이머/지연 콜백의 수명 관리 결함 (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ─────────────────────────────────────────────────────────────
// 시나리오: 각 접속 세션에 주기 타이머를 건다.
// - 유휴 점검 타이머: 일정 주기로 마지막 활동 시각을 보고 핑을 보내거나
// 유휴 종료를 결정한다(콜백이 세션의 소켓/상태를 참조한다).
// - 세션은 정상 종료(로그아웃)·강제 종료(중복 로그인/제재)·연결 끊김 등
// 여러 경로로 종료되며, 종료되면 SessionManager에서 제거된다.
// - 타이머 콜백은 ThreadPool 스레드에서 발화하므로 종료 처리와 병행될 수 있다.
//
// 요구사항:
// - 세션이 종료되면 그 세션에 걸린 타이머도 더 이상 발화하지 않아야 한다.
// - 종료된 세션의 소켓/상태에 접근하거나, 종료된 세션이 회수되지 못하고
// 계속 쌓이는 일이 없어야 한다.
//
// 과제: (A)(B) 지점을 검토하라. 세션이 Close된 뒤 타이머가 어떻게 동작하는지,
// 그로 인해 무엇이 잘못되는지(두 가지 이상) 설명하고 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
using System;
using System.Net.Sockets;
using System.Threading;
namespace Net
{
public sealed class Session
{
private readonly Socket _socket;
private readonly Timer _idleTimer;
public long PlayerId { get; }
public DateTime LastActivity;
private volatile bool _closed;
public Session(Socket socket, long playerId)
{
_socket = socket;
PlayerId = playerId;
LastActivity = DateTime.UtcNow;
// (A) 유휴 점검 타이머 — 5초마다 OnIdleCheck 발화(콜백이 this를 참조)
_idleTimer = new Timer(OnIdleCheck, null, 5000, 5000);
}
public void Touch() => LastActivity = DateTime.UtcNow;
private void OnIdleCheck(object? state)
{
if ((DateTime.UtcNow - LastActivity).TotalSeconds > 30)
{
// 유휴로 판단 → 핑 전송(응답 없으면 다음 주기에 종료)
var ping = new byte[] { 0x00 };
_socket.Send(ping);
}
}
public void Close()
{
_closed = true;
_socket.Close();
// (B) 세션 종료 처리: 매니저에서 제거
SessionManager.Remove(PlayerId);
}
}
public static class SessionManager
{
private static readonly System.Collections.Concurrent.ConcurrentDictionary<long, Session> _map = new();
public static void Add(Session s) => _map[s.PlayerId] = s;
public static void Remove(long id) => _map.TryRemove(id, out _);
public static int Count => _map.Count;
}
} 결함 코드 · C++
// ─────────────────────────────────────────────────────────────
// 시나리오: 각 접속 세션에 유휴 점검 타이머를 건다. 타이머 콜백은 세션을
// 참조해 마지막 활동 시각을 보고 핑을 보내거나 유휴 종료를 결정한다.
// 세션은 로그아웃/강제 종료/연결 끊김 등으로 종료되며, 종료 시 매니저에서
// 제거되고 객체가 해제된다. 타이머는 wheel.tick()에서 만기분이 발화한다.
//
// 요구사항:
// - 세션이 종료되면 그 세션에 걸린 타이머도 더 이상 발화하지 않아야 한다.
// - 종료된 세션을 가리키는 콜백이 실행되거나, 종료된 세션이 회수되지 못하고
// 계속 쌓이는 일이 없어야 한다.
//
// 과제: (A)(B) 지점을 검토하라. 세션이 closeSession된 뒤 wheel.tick()이
// 그 세션의 타이머를 발화하면 어떤 일이 일어나는지 설명하고 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
#include <cstdint>
#include <functional>
#include <unordered_map>
#include <utility>
namespace net {
using Clock = uint64_t;
inline Clock now() { return 0; } // 스텁
inline void closeSocket(int fd) { (void)fd; } // 스텁
// 간단한 타이머 휠: id -> (만기시각, 콜백). tick()이 만기 콜백을 발화한다.
class TimerWheel {
public:
uint64_t schedule(Clock when, std::function<void()> cb) {
uint64_t id = ++seq_;
timers_[id] = Timer{when, std::move(cb)};
return id;
}
void cancel(uint64_t id) { timers_.erase(id); }
void tick(Clock t) {
for (auto& kv : timers_)
if (kv.second.when <= t) kv.second.cb(); // 만기 콜백 발화
}
private:
struct Timer { Clock when; std::function<void()> cb; };
std::unordered_map<uint64_t, Timer> timers_;
uint64_t seq_ = 0;
};
struct Session {
int fd = -1;
uint64_t playerId = 0;
Clock lastActivity = 0;
bool closed = false;
};
class Server {
public:
Session* openSession(int fd, uint64_t pid) {
Session* s = new Session{fd, pid, now(), false};
sessions_[pid] = s;
// (A) 유휴 점검 타이머: 콜백이 이 세션 포인터를 캡처한다
wheel_.schedule(now() + 5000, [this, s]() { onIdleCheck(s); });
return s;
}
void onIdleCheck(Session* s) {
if (now() - s->lastActivity > 30000) sendPing(s);
}
void closeSession(uint64_t pid) {
auto it = sessions_.find(pid);
if (it == sessions_.end()) return;
Session* s = it->second;
closeSocket(s->fd);
sessions_.erase(it);
// (B) 세션 객체 해제
delete s;
}
TimerWheel& wheel() { return wheel_; }
private:
void sendPing(Session* s) { (void)s; /* s->fd 로 핑 전송 */ }
TimerWheel wheel_;
std::unordered_map<uint64_t, Session*> sessions_;
};
} // namespace net 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.