c++

c++의 function과 EventBus로 활용법

psb08 2025. 12. 12. 20:50
728x90
반응형

드디어 공부 글을 씁니다.

이번에는 c++ win32 api 프로젝트를 진행하면서 EventBus를 만들기 위해 공부한 function을 정리할 예정입니다.

그러기 전에 먼저 Callback(콜백)과 Callable(콜에이블, 콜러블)을 정리할 예정입니다.


콜백(callback)이란?

콜백은 나중에 호출될 함수를 미리 넘겨두는 방식입니다.

즉, 어떤 객체나 함수에게 이 상황이 되면 이 함수를 불러줘 하고 전달하는 함수입니다.

 

예)

  • 버튼이 눌리면 실행될 함수
  • 네트워크 요청이 끝나면 실행될 함수
  • 애니메이션이 끝나면 실행될 함수

콜백을 쓰는 이유는?

  1. 코드를 분리하고 깔끔하게 유지
    버튼 클래스는 클릭되면 뭘 할까?를 몰라도 됩니다.
    그냥 클릭되면 콜백만 호출하면 됩니다.
  2. 유연성 증가
    버튼을 사용하는 쪽에서 원하는 기능을 넣을 수 있습니다.
  3. 재사용성 증가
    버튼 코드는 그대로 두고, 다양한 상황에 다른 동작을 부여 가능합니다.

이를 활용하여 유니티에서 사용하는 EventBus 시스템을 c++에서도 구현 가능합니다. 


Callable이란?

callable은 함수처럼 호출할 수 있는 모든 것을 의미하는 더 큰 개념입니다.

즉, 괄호 ()를 붙여서 실행할 수 있으면 전부 callable입니다.

 

아래는 예시입니다.

1. 일반 함수

void TestFunc() {}

2. 함수 포인터

void (*tf)() = TestFunc;

3. 람다(lambda)

auto lam = [](){};

4. Functor (함수 객체)

struct F 
{
    void operator()() {}
};

5. bind로 만든 함수 객체

auto f = std::bind(&MyClass::Func, this);

6. std::function으로 감싼 것

std::function<void()> f = lam;

 

등등이 전부 Callable입니다.

그럼 functor와 bind, std::function을 간단히 알아보겠습니다.


1. Functor

operator()를 가진 객체입니다. 객체를 함수처럼 쓸 수 있습니다. 즉 함수처럼 작동하는 객체입니다.

2. Bind

함수 + 인자 + 객체를 미리 묶어서(call) 나중에 호출할 수 있는 callable을 만드는 도구입니다.

3. std::function

callable(함수처럼 호출 가능한 것)을 저장하는 타입입니다. 함수, 람다, functor, bind 결과 -> 전부 저장 가능합니다.

그리고 이 std::function은 밑에서 자세히 설명하도록 하겠습니다. 


Function?

function은 호출할 수 있는 객체(callable)를 저장 가능하고 호출 가능한 타입입니다.

예시로

function<void()> m_onClick = nullptr;

이렇게 선언하여 버튼이 클릭되면 실행할 함수를 저장해 두는 콜백 역할을 만들 수 있습니다. 

function은 람다랑 함수 포인터를 쓸 수 있어 매우 편리합니다.

UIButton(const wstring& text, function<void()> cb)
    : m_onClick(cb)
{
    m_text = text;
}

이렇게 버튼을 생성할 때 버튼이 눌렸을 때 실행될 함수를 전달할 수 있습니다.

 

Button에서 활용 부분

이 코드는 버튼에 function을 두어 람다로 실행할 함수를 넣습니다.

 

Function와 Callable의 관계

앞에서 function은 callable을 저장할 수 있는, 호출 가능한 타입이라 했습니다.

그래서 std::function<void()> 이라는 타입은
아무 인자도 받지 않고, 반환도 없는 callable 객체를 저장하는 컨테이너가 되는 것입니다.

 

그렇다면 이를 어디서 활용할 수 있을까요??

앞에서 간단히 보여드린 Button도 있지만, 저는 function을 EventBus 시스템을 구현하기 위해 활용했습니다.


EventBus에서 활용하기

EventBus : 특정 이름의 이벤트에 콜백을 등록하고, 나중에 호출할 수 있게 해주는 시스템입니다.

 

EventBus.h의 AddListener부분입니다. 

#pragma once
#include <functional>
#include <unordered_map>
#include <vector>
#include <string>

class EventBus
{
public:
	using EventCallback = std::function<void()>;
    	using ListenerID = size_t;
	
    	static ListenerID AddListener(const std::wstring& _evtName, EventCallback callback);
    
private:
	static std::unordered_map<std::wstring, std::map<ListenerID, EventCallback>> m_listeners;
    	static void Invoke(const wstring& _evtName);
    	static ListenerID m_nextID;
}

 

 

using EventCallback = std::function<void()>;

이벤트가 발생하면 실행할 함수 타입입니다. 
()로 호출 가능합니다. 
람다 / 함수 포인터 / functor 전부 저장 가능합니다. 

이벤트 리스너 함수의 타입.

 

using ListenerID = size_t;

등록한 리스너에게 부여할 고유 ID 타입입니다.
나중에 이 리스너만 삭제하는 작업도 하기 좋게 하려고 ID를 두었습니다.

 

static ListenerID AddListener(const std::wstring& _evtName, EventCallback callback);

이벤트 이름(_evtName)에 대해 실행할 콜백(callback)을 저장하고 고유 ID를 발급해서 반환합니다. 

 

static void Invoke(const wstring& _evtName);

static을 사용하여 객체 없이 호출 가능하도록 했습니다.

전역 시스템처럼 동작합니다.

const wstring&으로 이벤트 이름으로 등록된 콜백들 모두 실행하고, 문자열을 복사하지 않고 안전하게 전달합니다.

 

static std::unordered_map<std::wstring, std::map<ListenerID, EventCallback>> m_listeners;

키 1: 이벤트 이름 (wstring) / 키 2: 리스너 ID / 값: 등록된 콜백이 구조를 가지고 있습니다. 

 

m_listeners를 unordered_map으로 구현한 이유??

unordered_map은 해시 기반이라 탐색, 삽입, 삭제가 빠르기에 이벤트에 적합하다 생각하여 구현할 때 활용했습니다.

 

그렇다면 map은 어떨까요??

map은 레드블랙 트리 기반이라 매번 비교하며 트리를 타기에 점점 느려집니다. 이벤트 구현에는 정렬도 필요 없어서 정렬 기반인 map을 사용하지 않았습니다.  

 

static ListenerID m_nextID;​

새로운 리스너가 추가될 때마다 ID를 증가시키는 ID 자동 생성기입니다.


EventBus.cpp 

#include "pch.h"
#include "EventBus.h"

std::unordered_map<std::wstring, std::map<EventBus::ListenerID, EventBus::EventCallback>> EventBus::m_listeners;
EventBus::ListenerID EventBus::m_nextID = 1;

EventBus::ListenerID EventBus::AddListener(const std::wstring& _evtName, EventCallback callback)
{
    ListenerID id = m_nextID++;  //Id 생성, 저장하기 
    m_listeners[_evtName][id] = std::move(callback);
    return id;  //반환하기
}

void EventBus::Invoke(const wstring& _evtName)
{
    auto it = m_listeners.find(_evtName);  //찾기
    if (it == m_listeners.end()) return;

    for (auto& [id, callback] : it->second)
        callback();  //다 실행하기
}

 

 

std::unordered_map<std::wstring, std::map<EventBus::ListenerID, EventBus::EventCallback>> EventBus::m_listeners;
EventBus::ListenerID EventBus::m_nextID = 1;

정적 멤버 초기화 부분입니다.

m_listeners : 모든 이벤트 이름 -> (ID -> 콜백)을 저장하는 전역적 저장소입니다.
m_nextID : 새로운 리스너에 부여할 현재 ID 번호로, 시작값은 1입니다.

 

EventBus::ListenerID EventBus::AddListener(const std::wstring& _evtName, EventCallback callback)
{
    ListenerID id = m_nextID++;  //Id 생성, 저장하기 
    m_listeners[_evtName][id] = std::move(callback);
    return id;  //반환하기
}

AddListener입니다.

지금 m_nextID 값을 사용하고, 이후 ID가 증가합니다. -> 고유 ID 발급기 역할을 합니다.

이벤트 이름인 _evtName에 접근합니다. 그 안에서 ID에 해당하는 콜백을 저장하며,

std::move를 사용해서 콜백 객체 복사 대신 이동합니다.

등록한 리스너의 ID를 반환하며, 이 리스너만 제거에 사용이 가능합니다.

 

*std::move는 이 객체를 이동(move) 시켜도 좋다는 의도를 컴파일러에게 알려주는 함수입니다.

 

AddListener 예시

실제 프로젝트에서 사용한 부분입니다.

이 세 줄은 각 보스가 죽었을 때 실행될 콜백을 EventBus에 등록하고 있습니다.

"Boss1Killed", "Boss2Killed", "Boss3Killed" 이벤트에 대응하는 콜백을 EventBus에 등록하는 코드입니다.

보스가 죽었을 때 Invoke를 해주면 여기 람다 안에 들어가 있는 콜백을 실행합니다.

 

void EventBus::Invoke(const wstring& _evtName)
{
    auto it = m_listeners.find(_evtName);  //찾기
    if (it == m_listeners.end()) return;

    for (auto& [id, callback] : it->second)
        callback();  //다 실행하기
}

Invoke입니다.

m_listeners에서 _evtName으로 등록된 콜백 목록을 찾습니다.

아예 없으면 바로 종료하고, 등록된 모든 콜백을 순서대로 실행합니다.

 

Invoke 예시

를 하면 Boss1Killed에 등록된 콜백들이 하나씩 실행됩니다. 

 


이렇게 Callback과 Callable, function을 정리하고, EventBus에 어떻게 활용하였는지 작성했습니다. 

이렇게 구현을 해보면서 느낀 점은 적당한 자료구조를 잘 선택해서 개발하면 편하다는 것을 느끼게 되었습니다.

물론 구조 공부도 중요하겠지만, 이런 자료구조를 잘 선택해서 개발하는 능력도 공부하는 과정 중 한 단계라고 생각합니다. 

이를 통해 프로젝트에서 한 단계 더 성장할 수 있었던 것 같습니다.

다음에는 프로젝트 소개와 배운 점, 느낀 점 등을 정리하여 오겠습니다.

'c++' 카테고리의 다른 글

2025년도 2학기 게임 프로그래밍 팀 프로젝트 설명글  (0) 2025.12.30
백준 c++ 28278 문제 풀이  (0) 2025.05.21
백준 c++ 1158 문제 풀이2  (0) 2025.05.20
백준 c++ 1158 문제 풀이  (0) 2025.05.19
백준 c++ 1966 문제 풀이  (0) 2025.05.18