2. Room/Session 이벤트 핸들러 누수와 상호 참조
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 캐주얼 게임의 매치 룸(Room) 시스템이다.
// - Room 은 자기 안의 Session(플레이어 접속) 들을 관리한다.
// - 각 Session 은 "룸 이벤트(브로드캐스트 등)"를 받아야 해서, 룸의 이벤트에
// 구독(이벤트 핸들러 등록)한다.
// - 매치가 끝나면 RoomManager 가 Room 을 제거하고, Room 객체와 그 안의
// 모든 Session 이 깔끔하게 GC 회수되어야 한다.
// - 접속 종료 콜백은 네트워크 스레드에서 비동기로 들어올 수 있다.
//
// 운영 중 증상:
// - 룸을 반복 생성/제거하는 워크로드에서 시간이 갈수록 메모리가 단조 증가한다.
// - 매니저 맵에서 룸을 지워도 Room/Session 이 GC 되지 않는다(파이널라이저 미실행).
// - 가끔 이미 종료된 룸으로 메시지를 보내려다 예외/잘못된 동작이 난다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 룸이 RoomManager 에서 제거되면 Room/Session 이 GC 가능해야 한다.
// - 세션은 자기 룸으로 메시지를 보낼 수 있어야 한다(Broadcast).
// - 비동기 콜백에서 룸을 다뤄도 안전해야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 룸을 반복 생성/제거하면 메모리가 어떻게 되는가? 왜?
// 2) (A)(B)(C) 지점의 결함을 각각 설명하라.
// 3) 수명(lifetime)이 안전하도록 이벤트 구독/참조를 수정하라.
// ============================================================================
using System;
using System.Collections.Generic;
namespace MatchRoom
{
public sealed class Session
{
private readonly int _id;
// (A) 세션이 자기 룸을 강한 참조로 보관
private Room _room;
public Session(int id) { _id = id; }
public int Id => _id;
public void JoinRoom(Room room)
{
_room = room;
// (A) 룸의 이벤트에 구독 → Room 이 Session 핸들러를 강하게 붙잡는다
room.OnBroadcast += HandleBroadcast;
}
private void HandleBroadcast(string msg)
{
Console.WriteLine($"[session {_id}] {msg}");
}
public void SendToRoom(string msg)
{
// (B) room 이 살아있다고 가정하고 그냥 사용
_room.Broadcast(msg);
}
}
public sealed class Room
{
private readonly int _id;
// (B) 룸도 세션들을 강하게 보관
private readonly List<Session> _sessions = new();
// Session 들이 여기에 구독한다(델리게이트가 Session 인스턴스를 강하게 캡처)
public event Action<string> OnBroadcast;
public Room(int id) { _id = id; }
public int Id => _id;
public void AddSession(Session s)
{
_sessions.Add(s);
s.JoinRoom(this); // (A)
}
public void Broadcast(string msg)
{
OnBroadcast?.Invoke($"room {_id}: {msg}");
}
}
public sealed class RoomManager
{
private readonly Dictionary<int, Room> _rooms = new();
public Room CreateRoom(int id)
{
var room = new Room(id);
_rooms[id] = room;
return room;
}
// 매치 종료 시 호출
public void RemoveRoom(int id)
{
// (C) 맵에서만 지운다. 세션↔룸 상호 참조/구독은 그대로 남는다.
_rooms.Remove(id);
}
public Room Get(int id) => _rooms.TryGetValue(id, out var r) ? r : null;
}
// 데모용 구동 코드
public static class Demo
{
public static void Run()
{
var mgr = new RoomManager();
for (int r = 0; r < 3; r++)
{
var room = mgr.CreateRoom(r);
for (int s = 0; s < 2; s++)
{
var sess = new Session(r * 10 + s);
room.AddSession(sess);
}
room.Broadcast("welcome");
mgr.RemoveRoom(r); // 매치 종료
// 여기서 room/session 이 GC 가능해져야 정상
}
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("done");
}
}
} 결함 코드 · C++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 캐주얼 게임의 매치 룸(Room) 시스템이다.
// - Room 은 자기 안의 Session(플레이어 접속) 들을 소유한다.
// - 각 Session 은 "내가 속한 룸"을 알아야 한다(룸 채팅 브로드캐스트 등).
// - 매치가 끝나면 RoomManager 가 Room 을 제거하고, Room 객체와 그 안의
// 모든 Session 이 깔끔하게 해제되어야 한다.
// - 접속 종료 콜백은 네트워크 스레드에서 비동기로 들어올 수 있다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 룸이 RoomManager 에서 제거되면 Room/Session 메모리가 즉시 회수돼야 한다.
// - 세션은 자기 룸으로 메시지를 보낼 수 있어야 한다(Broadcast).
// - 비동기 콜백에서 세션 포인터를 다뤄도 안전해야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 룸을 반복 생성/제거하면 메모리가 어떻게 되는가? 왜?
// 2) (A)(B)(C) 지점의 결함을 각각 설명하라.
// 3) 수명(lifetime)이 안전하도록 스마트포인터 사용을 수정하라.
// ============================================================================
#include <cstdio>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
class Room; // fwd
class Session : public std::enable_shared_from_this<Session>
{
public:
explicit Session(int id) : id_(id) {}
~Session() { std::printf(" ~Session(%d)\n", id_); }
// (A) 세션이 자기 룸을 소유 포인터로 보관
void SetRoom(std::shared_ptr<Room> room) { room_ = room; }
void SendToRoom(const std::string& msg);
int Id() const { return id_; }
private:
int id_;
std::shared_ptr<Room> room_;
};
class Room : public std::enable_shared_from_this<Room>
{
public:
explicit Room(int id) : id_(id) {}
~Room() { std::printf("~Room(%d)\n", id_); }
void AddSession(const std::shared_ptr<Session>& s)
{
sessions_.push_back(s);
s->SetRoom(shared_from_this()); // (A)
}
void Broadcast(const std::string& msg)
{
for (auto& s : sessions_)
std::printf("[room %d -> session %d] %s\n", id_, s->Id(), msg.c_str());
}
int Id() const { return id_; }
private:
int id_;
std::vector<std::shared_ptr<Session>> sessions_; // (B)
};
void Session::SendToRoom(const std::string& msg)
{
// (B) room_ 가 살아있다고 가정하고 그냥 역참조
room_->Broadcast(msg);
}
class RoomManager
{
public:
std::shared_ptr<Room> CreateRoom(int id)
{
auto room = std::make_shared<Room>(id);
rooms_[id] = room;
return room;
}
// 매치 종료 시 호출
void RemoveRoom(int id)
{
// (C) 맵에서만 지운다. 세션들이 룸을 붙잡고 있다.
rooms_.erase(id);
}
Room* GetRaw(int id)
{
auto it = rooms_.find(id);
return it == rooms_.end() ? nullptr : it->second.get();
}
private:
std::unordered_map<int, std::shared_ptr<Room>> rooms_;
};
int main()
{
RoomManager mgr;
for (int r = 0; r < 3; ++r)
{
auto room = mgr.CreateRoom(r);
for (int s = 0; s < 2; ++s)
{
auto sess = std::make_shared<Session>(r * 10 + s);
room->AddSession(sess);
}
room->Broadcast("welcome");
mgr.RemoveRoom(r); // 매치 종료
// 여기서 ~Room / ~Session 이 찍혀야 정상
}
std::printf("done\n");
return 0;
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.