나만의 작은 도서관
[TIL][C++] 250826 MMO 서버 개발 87일차: [언리얼] 다시 복습하는 델리게이트(delegate), 이벤트(Event) 본문
Today I Learn
[TIL][C++] 250826 MMO 서버 개발 87일차: [언리얼] 다시 복습하는 델리게이트(delegate), 이벤트(Event)
pledge24 2025. 8. 26. 22:18주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
[언리얼] 다시 복습하는 델리게이트(delegate)
델리게이트(delegate)란?
- 델리게이트(delegate)란 임의의 함수를 저장해두었다가 필요한 시점에 호출할 수 있게 해주는 “콜백 시스템” 기능을 갖춘 방식들 중 하나이다. (콜백 시스템이라는 단어는 내가 부르기 편하기 위해 만든 단어이다.)
- 콜백 시스템을 갖춘 다른 방법들은 1) functor, 2) function객체 3) 함수 포인터 등이 있다.
언리얼 델리게이트(delegate)의 장점들
장점 1. 안전하게 함수를 호출할 수 있다.
- 함수 호출은 때때로 위험할 수 있다. 특정 객체의 멤버 함수를 호출할 경우, 객체가 파괴된 다음 멤버 함수를 호출하게 되면 댕글링 포인터 문제가 발생하기 때문이다.
- 하지만 언리얼 델리게이트를 사용할때 리플렉션 시스템에 의해 관리되는 UObject 객체의 멤버함수를 추가한다면, UObject 생명주기에 연동되어 객체가 파괴되면 자동으로 해제되므로 댕글링 포인터 발생을 막을 수 있다.
- 다른 말로 바꿔말하면 UObject로 관리되지 않는 클래스를 등록하면 안전하지 않다는 말이 되기도 한다.
장점 2. 동적으로 함수를 추가할 수 있다.
- 일반적은 C++ 함수 포인터는 컴파일 타임에 바인딩되므로 런타임에 동적으로 변경하기 어렵다. 하지만 언리얼의 델리게이트는 함수들을 런타임에 식별하여 추가할 수 있다.
- 다이나믹 델리게이트를 활용하면 블루프린트나 리플렉션을 통한 런타임 바인딩이 가능해지기도 한다.
장점 3. 여러 함수를 추가할 수 있다.
- 언리얼의 델리게이트는 여러 개의 함수를 하나의 델리게이트에 추가할 수 있다. 즉, 특정 이벤트가 발생했을때 여러 함수가 호출될 수 있다는 것이다.
- 어느 하나의 이벤트가 여러 시스템에 영향을 주어야하는 경우(Ex. 캐릭터 사망 시 UI 갱신, 점수 계산, 사운드 재생)에 유용하게 사용할 수 있다.
장점 4. 느슨한 결합
- 델리게이트 방식을 사용하면 송신자(이벤트를 발생시키는 객체)와 수신자(이벤트에 반응하는 객체)간의 직접적인 의존성이 없다. 송신자는 수신자가 어떤 객체인지, 어떤 함수를 호출하는지 몰라도 이벤트를 브로드캐스트 할 수 있다.
- 이러한 특성은 시스템 간의 결합도가 낮아져 코드를 관리하고 확장하기가 훨씬 쉬워진다.
델리게이트 고르는 법 요약
| 함수를 딱 하나만 바인드 | 함수를 여러 개 바인드 | |
| 네이티브에서만 바인드할거임 | 일반 델리게이트 | 멀티캐스트 일반 델리게이트 |
| 블루프린트에서도 바인드할거임 | 다이나믹 델리게이트 | 다이나믹 멀티캐스트 델리게이트 |
델리게이트 바인딩 종류
- 기존 델리게이트 객체 → Bind()
- 전역 C++ 함수 → BindStatic()
- Native 클래스 멤버 함수 → BindRaw()
- 람다 함수 → BindLambda()
- SharedPtr 클래스 멤버 함수(잘 안 씀) → BindSP()
- UObject 멤버 함수 → BindUObject()
델리게이트 실행시키기
- Excute(): 등록된 함수들을 호출한다.
- ExcuteIfBound(): 바인딩된 함수가 있을때만 호출한다.
- IsBound(): 바인딩된 함수가 있는지 bool값으로 반환
델리게이트 사용 예제
클래스가 하나 있다.
class FLogWriter
{
void WriteToLog(FString);
};
델리게이트를 선언한다.
- 함수 시그니처를 맞추기 위해 OneParam으로 만들었고, 델리게이트 이름은 FStringDelegate로 하기로 했다.
// 델리게이트 선언을 이따구로 하는 이유는 기반 언어인 C++에 델리게이트가 없기 때문
// 언리얼은 델리게이트 시스템을 구축하기 위해 이따구로 매크로를 만든것이다.
DECLARE_DELEGATE_OneParam(FStringDelegate, FString);
델리게이트를 배치한다.
- 보통 델리게이트는 이벤트의 주체가 되는 클래스안에 배치된다. 예를 들어 플레이어가 맞았을때 발동해야하는 이벤트는 플레이어 클래스에 들어간다.
- 헷갈릴 수 있는데, FStringDelegate는 델리게이트 클래스 이름, WriteToLogDelegate는 객체 이름이 되겠다.
class FMyClass
{
FStringDelegate WriteToLogDelegate;
};
만든 델리게이트에 함수를 바인딩한다.
- 여기서는 SharedPtr 타입을 통해 함수를 바인딩하려고 하므로, 그에 맞는 BindSP 함수로 바인딩하는 모습이다. 첫번째 인자로는 객체 포인터가, 두번째 인자로는 함수 포인터가 들어간다.
TSharedRef<FLogWriter> LogWriter(new FLogWriter());
WriteToLogDelegate.BindSP(LogWriter, &FLogWriter::WriteToLog);
원하는 시점에 델리게이트를 실행시킨다.
- 델리게이트를 실행시킬 시점이 되었다면 Execute를 통해 실행시킨다. WriteToLogDelegate는 OneParam 델리게이트 이므로 인자를 하나 넣어줘야한다.(당연히 같은 인자 타입을 넣어줘야한다.)
WriteToLogDelegate.Execute(TEXT("델리게이트 실행!"));
- Excute의 경우 호출시 assert가 발동되는데, assert를 통과하지 못하면 그대로 시스템이 멈추므로, 이런 경우를 피하기 위해 아래와 같이 안전하게 피해가도록 한다.
- assert는 비정상적인 실행이 감지되었을때 크래시를 내는 함수이다. 디버깅에 유용하지만 개발 단계가 아닌 서비스 중에 크래시가 나면 큰일이기 때문에 보통은 피해간다.
WriteToLogDelegate.ExecuteIfBound(TEXT("델리게이트 실행!"));
결과
- 위의 경우 SharedPtr로 바인드했기 때문에 객체가 먼저 죽어있는 경우는 없으므로, 무조건 실행될 것이다. 실행되는건 LogWrite객체의 WriteLog() 함수 하나이다.
BlueprintAssignable
- C++에서 선언한 다이나믹 델리게이트를 블루프린트에서 함수를 바인드하려고 할때 추가하는 UPROPERTY의 지정자
// MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
// 동적 멀티캐스트 델리게이트 선언
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSomethingHappened);
UCLASS()
class AMyActor : public AActor
{
GENERATED_BODY()
public:
// BlueprintAssignable → 블루프린트에서 바인딩 가능
UPROPERTY(BlueprintAssignable, Category="Event")
FOnSomethingHappened OnSomethingHappened;
// 어떤 이벤트가 발생했을 때 호출되는 함수
UFUNCTION(BlueprintCallable, Category="Event")
void TriggerEvent();
};
// MyActor.cpp
#include "MyActor.h"
void AMyActor::TriggerEvent()
{
// 델리게이트에 바인딩된 모든 함수 실행
OnSomethingHappened.Broadcast();
}
블루프린트에서 함수 바인드하기
- AMyActor를 상속한 블루프린트를 연다.
- 이벤트 그래프에서 MyActor 인스턴스를 선택 → 디테일 패널에서 델리게이트 변수이름으로 된 이벤트가 표시됨(OnSomethingHappened)
- C++에서 UPROPERTY(BlueprintAssignable)로 선언했기 때문에 표시되는 것!
- 버튼을 눌러 바인드된 이벤트 노드를 생성
- 원하는 로직을 추가한다.
이벤트(Event)
- 멀티캐스트 델리게이트와 유사한 기능.
- 외부에서 실행( Broadcast 또는 Excute 함수 호출) 불가. 이벤트를 선언한 클래스를 통해서만 실행 가능.
// 이벤트 선언
DECLARE_EVENT(AMyActor, FOnSomethingHappened)
class AMyActor : public AActor
{
public:
FOnSomethingHappened& OnSomethingHappened() { return SomethingHappened; }
void TriggerEvent()
{
SomethingHappened.Broadcast(); // 내부에서만 가능
}
private:
FOnSomethingHappened SomethingHappened;
};
// 구독하는 쪽
MyActor->OnSomethingHappened().AddUObject(this, &UMyClass::HandleSomething);
| 구분 | Delegate | Event(DECLARE_EVENT) |
| 바인딩 개수 | 싱글(기본), 멀티캐스트 가능 | 항상 멀티캐스트 |
| 접근성 | 누구나 Bind/Execute 가능 | 구독자는 Add~만 가능, Broadcast는 오직 선언한 클래스 내부에서만 가능 |
| 사용 의도 | 콜백 함수 저장/호출 | 이벤트 퍼블리셔-리스너 패턴 (발생 권한은 내부 전용, 외부는 구독 전용) |
| 안전성 | 실행/호출 모두 외부에서 가능 → 잘못 쓰면 위험 | 호출/발생은 캡슐화, 더 안전한 API 설계 |
정리
- Delegate → 범용적, 외부에서도 Broadcast 가능 (자유도 ↑, 실수 위험 ↑).
- Event → Observer 패턴 전용, 발생 권한은 내부 한정, 외부는 구독만 가능 (안전한 설계).
