📚 델리게이트 시리즈
1편: 함수 포인터
2편: std::function
3편: Delegate 구현
4편: 엔진 스타일 Delegate
🚀 들어가며
이전 글에서는 std::function을 기반으로
👉 멀티캐스트 델리게이트 구조를 직접 구현해봤습니다.
class Delegate
{
public:
void Add(std::function<void()> func)
{
functions.push_back(func);
}
void Broadcast()
{
for (auto& func : functions)
{
func();
}
}
private:
std::vector<std::function<void()>> functions;
};
이 구조만으로도 충분히 이벤트 시스템을 만들 수 있습니다.
하지만 실제 게임 엔진에서는 여기서 한 단계 더 나아갑니다.
❓ “등록된 함수 중 일부만 제거하려면?”
❓ “객체가 파괴되었을 때 자동으로 정리하려면?”
❓ “더 빠르게 호출하려면?”
👉 이 문제들을 해결하기 위해 엔진 스타일 델리게이트 구조가 등장합니다.
🎯 기존 구조의 한계
기존에 만든 구조는 간단하지만 다음 문제가 있습니다.
❌ 개별 제거 불가능
functions.push_back(func);
👉 특정 함수만 제거하기 어렵습니다
❌ 객체 생명주기 관리 없음
onClick.Add([&player]()
{
player.Attack();
});
👉 player가 사라지면?
👉 댕글링 참조 위험
❌ 성능 오버헤드
std::function → 유연하지만 비용 있음
🎯 엔진이 선택한 해결 방식
게임 엔진에서는 다음 구조를 사용합니다.
함수 리스트 + 등록 ID + 객체 추적 + 안전한 제거
🧠 핵심 설계 아이디어
1️⃣ 함수마다 ID 부여
함수 등록 ↓ 고유 ID 반환 ↓ ID로 제거
2️⃣ 객체 기반 바인딩
객체 + 함수 묶기
3️⃣ 안전한 실행
유효한 객체만 실행
🎮 개선된 Delegate 구현
✔ 등록 ID 구조
(아래 코드는 C++17 이상 필요합니다.)
#include <vector>
#include <functional>
#include <algorithm>
class Delegate
{
public:
using DelegateID = size_t;
DelegateID Add(std::function<void()> func)
{
functions.emplace_back(nextId, func);
return nextId++;
}
void Remove(DelegateID id)
{
functions.erase(
std::remove_if(functions.begin(), functions.end(),
[id](const auto& pair)
{
return pair.first == id;
}),
functions.end());
}
void Broadcast()
{
// 이 코드는 C++17 이상 필요합니다.
for (auto& [id, func] : functions)
{
func();
}
}
private:
std::vector<std::pair<DelegateID, std::function<void()>>> functions;
DelegateID nextId = 0;
};
🎯 사용 예제
Delegate onClick;
auto id1 = onClick.Add([]()
{
std::cout << "Sound\n";
});
auto id2 = onClick.Add([]()
{
std::cout << "UI\n";
});
onClick.Broadcast();
👉 출력:
Sound UI
🔥 특정 함수 제거
onClick.Remove(id1); onClick.Broadcast();
👉 출력:
UI
🎯 이 구조의 의미
등록 → ID 반환 ↓ ID 기반 관리 ↓ 선택적 제거 가능
👉 이것이 엔진 구조의 핵심입니다
🧠 객체 기반 바인딩 개선
문제:
[&player]() { player.Attack(); }
👉 객체가 사라지면 위험
✔ 안전한 방식 (weak_ptr)
std::weak_ptr<Player> weakPlayer = player;
onClick.Add([weakPlayer]()
{
if (auto p = weakPlayer.lock())
{
p->Attack();
}
});
🎯 의미
객체 생존 확인 ↓ 살아있으면 실행 ↓ 아니면 무시
👉 안전한 이벤트 시스템
🎮 엔진에서의 실제 구조
게임 엔진에서는 이런 식으로 동작합니다.
객체 등록 ↓ 델리게이트 바인딩 ↓ 이벤트 발생 ↓ 유효한 객체만 실행
🔥 왜 이렇게 복잡한가?
단순히 콜백을 넘어서:
✔ 안정성 ✔ 성능 ✔ 확장성
👉 이 세 가지를 동시에 만족해야 하기 때문입니다
🎯 구조 비교
| 단계 | 특징 |
|---|---|
| 함수 포인터 | 단순 |
| std::function | 유연 |
| Delegate | 확장 |
| 엔진 Delegate | 안전 + 관리 |
🧠 시리즈 전체 흐름
함수 포인터 ↓ std::function ↓ Delegate ↓ 엔진 Delegate
👉 이 흐름이 이해되면 이벤트 시스템이 보입니다.
🎯 핵심 정리
- 델리게이트는 이벤트 시스템의 핵심 구조이다
- ID 기반으로 등록/제거를 관리한다
- weak_ptr을 이용해 객체 생명주기를 안전하게 처리한다
- 엔진에서는 성능과 안정성을 위해 더 복잡한 구조를 사용한다
실제 언리얼의 델리게이트는 매크로, UObject 생명주기, 리플렉션 시스템과 연결되어 훨씬 복잡합니다.
이 글의 코드는 그 구조를 그대로 구현한 것이 아니라,
엔진 델리게이트가 왜 ID 관리와 객체 생명주기 처리를 필요로 하는지를 이해하기 위해 예제를 단순화했습니다.
🎮 마무리
이 시리즈를 통해 우리는 다음을 직접 구현해봤습니다.
- 함수 포인터
- std::function
- 델리게이트
- 엔진 스타일 델리게이트
👉 이 모든 구조는 결국 하나로 연결됩니다.
이벤트 시스템
이 구조를 이해하면 게임 엔진의 입력 시스템과 이벤트 흐름이 어떻게 연결되는지 보이기 시작합니다.
🚀 한 단계 더 나아가고 싶다면
델리게이트 구조는
입력 시스템, UI 이벤트, 게임 로직과 깊게 연결됩니다.
단순히 사용하는 것을 넘어서,
엔진이 어떻게 동작하는지 이해하는 데 초점을 맞춘 내용입니다.