본문 바로가기

UE5 Delegate

@iamrain2025. 10. 31. 08:45

1. Delegate (델리게이트)

언리얼 엔진의 델리게이트(Delegate)는 C++의 함수 포인터를 일반화하고, 타입 안정성(type-safe)과 유연성을 극대화한 콜백(Callback) 메커니즘입니다. 특정 이벤트가 발생했을 때, 해당 이벤트에 등록된 여러 함수를 안전하게 호출할 수 있도록 해주는 강력한 시스템입니다.

기존의 C++ 함수 포인터는 특정 함수의 주소만 저장할 수 있어 객체의 멤버 함수를 직접 담지 못하는 한계가 있습니다. 델리게이트는 이 문제를 해결하여 전역 함수, 클래스의 정적(static) 함수, 멤버 함수, 그리고 람다(lambda) 함수까지 모든 종류의 호출 가능한(callable) 대상을 캡슐화하고 실행할 수 있습니다.

주요 특징은 다음과 같습니다.

  • 일반화된 함수 포인터: 어떤 종류의 C++ 함수든(멤버, 정적, 전역, 람다) 바인딩(binding)하여 저장할 수 있습니다.
  • 타입 안정성: 델리게이트는 특정 함수 시그니처(매개변수 타입, 반환 타입)를 가지도록 선언됩니다. 시그니처가 일치하지 않는 함수는 바인딩할 수 없어 컴파일 타임에 오류를 방지합니다.
  • 동적 바인딩: 런타임에 함수를 바인딩하거나 해제할 수 있습니다.
  • 안전한 객체 참조: UObject의 멤버 함수를 바인딩할 경우, 해당 UObject가 소멸되면 델리게이트가 자동으로 바인딩을 해제하여 크래시를 방지합니다. (Weak Pointer 방식)
  • 멀티캐스팅(Multicasting): 하나의 델리게이트에 여러 개의 함수를 바인딩하여, 한 번의 호출로 모든 함수를 실행할 수 있습니다. (멀티캐스트 델리게이트의 경우)
  • 직렬화 및 리플렉션: 다이나믹 델리게이트(Dynamic Delegate)는 UObject 시스템과 통합되어, 델리게이트 자체와 그 바인딩 정보를 직렬화(저장)하거나 블루프린트에서 접근할 수 있습니다.

2. Delegate의 종류

언리얼 델리게이트는 기능과 특성에 따라 여러 종류로 나뉩니다.

구분 종류 특징 주요 사용처
바인딩 수 Single-cast 하나의 함수만 바인딩 가능. Execute()로 실행. 반환 값 가질 수 있음. 단일 콜백이 필요할 때 (e.g., 비동기 작업 완료 콜백)
  Multi-cast 여러 함수를 바인딩 가능. Broadcast()로 실행. 반환 값 가질 수 없음. 이벤트 알림 (e.g., 플레이어 사망, UI 버튼 클릭)
데이터 전달 Payload 함수 호출 시 매개변수를 전달할 수 있음. 이벤트 발생 시 관련 데이터를 함께 전달해야 할 때
  No-Payload 매개변수 없이 함수 호출. 단순 알림용 이벤트
구현 방식 C++ (Static) C++ 코드 전용. 가장 빠르고 효율적. 직렬화 불가. 순수 C++ 시스템 간의 통신, 고성능이 요구되는 곳
  Dynamic UObject 시스템과 연동. 함수 이름(FName)으로 바인딩. 직렬화 가능. 블루프린트 노출 가능. C++ 델리게이트보다 느림. 블루프린트와의 상호작용, 레벨에 저장되어야 하는 이벤트

이들을 조합하여 DECLARE_DELEGATE, DECLARE_MULTICAST_DELEGATE, DECLARE_DYNAMIC_DELEGATE, DECLARE_DYNAMIC_MULTICAST_DELEGATE 등의 매크로를 사용하여 선언합니다.

3. 내부 구조 및 구현 방식 (Deep Dive)

델리게이트의 내부는 복잡한 템플릿과 상속 구조로 이루어져 있으며, 핵심은 호출 가능한 대상을 추상화하고 저장하는 방식에 있습니다.

3.1. C++ (Static) Delegate의 구조

정적 델리게이트의 핵심은 TDelegate (싱글캐스트)와 TMulticastDelegate (멀티캐스트) 클래스입니다. 이들은 내부적으로 IDelegateInstance 인터페이스를 구현한 객체에 대한 포인터를 가집니다.

  1. IDelegateInstance: 호출할 함수에 대한 정보를 추상화하는 인터페이스입니다. 이 인터페이스를 상속받는 구상 클래스들은 각각 다른 종류의 함수(UObject 멤버, 일반 C++ 객체 멤버, 정적 함수 등)를 저장하고 호출하는 방법을 구현합니다.
    • TStaticMethodDelegateInstance: 전역 또는 정적 함수를 저장.
    • TRawMethodDelegateInstance: 일반 C++ 객체의 멤버 함수를 저장. (객체 포인터 + 멤버 함수 포인터)
    • TFunctorDelegateInstance: 람다 또는 TFunction과 같은 함수 객체를 저장.
    • TUObjectMethodDelegateInstance: UObject의 멤버 함수를 저장. TWeakObjectPtr를 사용해 UObject를 참조하므로, 객체가 파괴되면 안전하게 IsStale()을 통해 확인할 수 있습니다.
  2. TDelegate (싱글캐스트): 내부에 IDelegateInstance* 포인터 하나를 가집니다. Bind()가 호출되면, 함수 종류에 맞는 ...DelegateInstance 객체를 생성하고 이 포인터에 저장합니다. Execute()는 이 인스턴스의 Execute()를 호출합니다.
  3. TMulticastDelegate (멀티캐스트): 내부에 TArray<FDelegateBase> 형태의 호출 목록(Invocation List)을 가집니다. Add()가 호출되면 새로운 ...DelegateInstance를 생성하여 이 배열에 추가합니다. Broadcast()는 배열을 순회하며 유효한(stale하지 않은) 모든 인스턴스의 Execute()를 호출합니다.
    • 컴팩트(Compact) 최적화: 멀티캐스트 델리게이트에 바인딩된 함수가 하나뿐일 경우, 배열을 할당하는 대신 싱글캐스트 델리게이트처럼 단일 인스턴스 포인터만 저장하여 메모리와 성능을 최적화합니다. 두 번째 함수가 바인딩되는 시점에 배열로 전환됩니다.
    • 실행 중 수정 방지: Broadcast() 실행 중에 Add()Remove()가 호출되어도 안전하도록, 실행 전에 호출 목록을 복사하거나 잠그는 메커니즘이 있습니다.

3.2. Dynamic Delegate의 구조

다이나믹 델리게이트는 UObject의 리플렉션 시스템을 기반으로 동작합니다.

  1. 바인딩 정보: 함수 포인터를 직접 저장하는 대신, UObject 포인터함수 이름(FName)을 저장합니다.
  2. FScriptDelegate / FMulticastScriptDelegate: 다이나믹 델리게이트의 실제 구현 클래스입니다. 내부에 TWeakObjectPtr<UObject>FName을 멤버로 가집니다.
  3. 실행 메커니즘: Execute() 또는 Broadcast()가 호출되면, 델리게이트는 저장된 UObject 포인터와 FName을 사용하여 UObject::ProcessEvent() 함수를 호출합니다. ProcessEvent()UFunction 캐시 시스템을 통해 이름에 해당하는 UFunction을 찾고, 파라미터를 설정한 뒤 가상 머신(VM)을 통해 함수를 실행합니다. 이 과정은 C++ 함수를 직접 호출하는 것보다 훨씬 느립니다.
  4. 직렬화: UObject 포인터와 FName은 언리얼의 직렬화 시스템을 통해 쉽게 디스크에 저장하거나 네트워크로 전송할 수 있습니다. 이것이 다이나믹 델리게이트가 블루프린트와 연동되고 레벨에 저장될 수 있는 이유입니다.

4. 계층 구조

델리게이트 시스템의 동작은 여러 계층으로 나눌 수 있습니다.

  1. 선언 계층 (Declaration Layer):
    • 개발자가 DECLARE_..._DELEGATE... 매크로를 사용하여 커스텀 델리게이트 타입을 선언합니다.
    • 이 매크로는 TDelegate, TMulticastDelegate 등을 상속받는 새로운 타입을 정의하는 복잡한 템플릿 코드를 생성합니다.
  2. 사용자 API 계층 (User API Layer):
    • MyDelegate.BindUObject(this, &UMyClass::MyFunction) 처럼 Bind, Add, Execute, Broadcast 등의 API를 사용하여 델리게이트를 조작합니다.
    • UPROPERTY(BlueprintAssignable) 같은 UProperty 지정자를 사용하여 리플렉션 시스템에 노출시킵니다.
  3. 델리게이트 인스턴스 계층 (Delegate Instance Layer):
    • 사용자 API 호출에 따라 적절한 IDelegateInstance 구현체(TUObjectMethodDelegateInstance 등)가 생성되고 관리됩니다.
    • 싱글캐스트는 단일 포인터로, 멀티캐스트는 호출 목록(배열)으로 이 인스턴스들을 관리합니다.
  4. 호출/실행 계층 (Invocation/Execution Layer):
    • Execute()/Broadcast()가 호출되면 이 계층이 동작합니다.
    • 정적 델리게이트: IDelegateInstance의 가상 함수 Execute()를 직접 호출하여 C++ 함수를 실행합니다. (가상 함수 호출 오버헤드 존재)
    • 다이나믹 델리게이트: UObject::ProcessEvent()를 통해 UObject 시스템에 함수 실행을 위임합니다. (리플렉션 시스템 오버헤드 존재)
  5. UObject 시스템 계층 (UObject System Layer):
    • UObject 바인딩 시 TWeakObjectPtr를 통해 객체의 유효성을 검사하고, 객체 소멸 시 자동으로 바인딩을 무효화합니다.
    • 다이나믹 델리게이트의 경우, UFunction 검색, 파라미터 전달, 직렬화, 블루프린트 VM과의 연동 등 모든 리플렉션 관련 작업을 처리합니다.

5. Delegate 종류별 비교 및 사용 사례

구분 Static Delegate Dynamic Delegate
성능 매우 빠름. 거의 C++ 직접 호출에 가까움. 느림. 함수 이름 검색 및 리플렉션 시스템 오버헤드.
타입 안정성 컴파일 타임에 함수 시그니처 체크. 런타임에 일부 체크. 시그니처가 달라도 바인딩은 성공하고 실행 시 오류 발생 가능.
유연성 모든 C++ 호출 가능 대상 바인딩 가능. UFUNCTION()으로 선언된 UObject 멤버 함수만 바인딩 가능.
블루프린트 노출 불가. 노출 가능 (BlueprintAssignable, BlueprintCallable).
직렬화 불가. 델리게이트 바인딩 정보 저장 안 됨. 가능. 레벨 저장, 네트워크 리플리케이션 시 바인딩 유지.

사용 사례:

  • TDelegate (싱글, 정적): 비동기 작업(파일 읽기, HTTP 요청)이 완료되었을 때 단 하나의 콜백 함수를 실행해야 할 때.
    // FHttpModule.h
    TDelegate<void (FHttpRequestPtr, FHttpResponsePtr, bool)> OnProcessRequestComplete;
  • TMulticastDelegate (멀티, 정적): 순수 C++ 시스템 내부에서 발생하는 이벤트 알림. 예를 들어, 게임의 상태(시작, 종료, 일시정지)가 변경되었음을 여러 서브시스템에 알릴 때.
    // AGameModeBase.h
    FOnActorSpawned::FDelegate OnActorSpawned;
  • FScriptDelegate (싱글, 다이나믹): FTimerManager에서 SetTimer의 콜백으로 사용. 타이머는 레벨에 저장될 수 있으므로 다이나믹 델리게이트가 필요.
    // TimerManager.h
    FTimerHandle SetTimer(FScriptDelegate const& InDelegate, float InRate, bool InbLoop, float InFirstDelay = -1.0f);
  • FMulticastScriptDelegate (멀티, 다이나믹): 가장 흔하게 사용되는 형태로, 블루프린트 이벤트의 기반. UI 버튼의 OnClicked 이벤트, 액터의 OnActorBeginOverlap 이벤트 등.
    // UPrimitiveComponent.h
    UPROPERTY(BlueprintAssignable)
    FComponentBeginOverlapSignature OnComponentBeginOverlap;

6. 요약

델리게이트는 C++ 함수 포인터를 훨씬 안전하고 유연하게 만든 '슈퍼 함수 포인터'라고 생각하시면 됩니다. 특정 이벤트가 일어났을 때 어떤 함수를 호출할지 미리 약속(등록)해두는 시스템입니다. 일반 함수 포인터와 달리, 클래스 멤버 함수나 람다 함수 등 거의 모든 종류의 함수를 담을 수 있고, 함수 시그니처가 다르면 컴파일 단계에서 막아주기 때문에 타입에 안전합니다. 특히 UObject가 파괴되면 자동으로 연결을 끊어줘서 크래시를 막아주는 기능도 있습니다.

 

 주로 두 클래스 간의 결합도(coupling)를 낮추기 위해 사용합니다. 예를 들어, A라는 객체에서 어떤 일이 일어났을 때 B, C, D 객체에게 알려줘야 할 때, 델리게이트가 없다면 A가 B, C, D 객체의 포인터를 모두 알고 있어야 해서 서로 복잡하게 얽히게 됩니다. 하지만 A에 'OnSomethingHappened'라는 델리게이트를 하나 만들어두면, B, C, D는 그저 A의 델리게이트에 자신의 함수를 등록('나한테도 알려줘!')만 해두면 됩니다. A는 누가 등록했는지 알 필요 없이 그냥 이벤트가 발생했을 때 델리게이트를 호출하기만 하면 됩니다. 이렇게 하면 코드 수정이나 확장이 매우 유연해집니다.

 

 Static(정적) 델리게이트는 순수 C++ 코드에서만 사용되고, 함수 포인터를 직접 저장해서 호출하기 때문에 매우 빠릅니다. 반면, Dynamic(다이나믹) 델리게이트는 블루프린트와 연동하기 위해 만들어졌습니다. 함수 포인터 대신 함수 이름을 저장하고, 실행될 때 리플렉션 시스템을 통해 이름으로 함수를 찾아 실행하기 때문에 속도는 느립니다. 하지만 이 방식 덕분에 델리게이트의 연결 상태를 파일에 저장하거나, 블루프린트의 이벤트 그래프에서 노드로 만들어 시각적으로 연결할 수 있다는 큰 장점이 있습니다. 그래서 UI 버튼의 OnClicked 이벤트처럼 블루프린트에서 다뤄야 하는 기능들은 대부분 다이나믹 델리게이트를 사용합니다.

'Unreal' 카테고리의 다른 글

콜리전 필터링 Collision Filtering  (0) 2025.11.03
UE5 Delegate와 Event의 차이점  (0) 2025.10.31
UE5 TSparseArray  (0) 2025.10.30
UE5 TSet  (0) 2025.10.30
std::map과 TMap  (0) 2025.10.29
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차