- 느슨한 결합의 장접과 이를 편리하게 구현하도록 도와주는 델리게이트
- 발행 구독 디자인 패턴
- 언리얼 델리게이트를 활용한 느슨한 결합의 설계와 구현 학습
강한 결합과 느슨한 결합
- 강한 결합(Tight Coupling)
- 클래스들이 서로 의존성을 가지는 경우
- 아래 예시에서 Card가 없는 경우 Person이 만들어질 수 없다.
- 이 때 Person은 Card에 대한 의존성을 가진다.
- 핸드폰에서도 인증할 수 있는 새로운 카드가 도입된다면?
- 느슨한 결합(Loose Coupling)
- 실물에 의존하지 말고 추상적 설계에 의존하라. (DIP 원칙)
- 왜 Person은 Card가 필요한가? 출입을 확인해야 하기 때문
- 출입에 관련된 추상적인 설계에 의존하자
- ICheck를 상속받은 새로운 카드 인터페이스를 선언해 해결
- 새로운 인증 방법이 생겨도 ICheck만 상속받아 구현할 수 있으면 됨
- 이러한 느슨한 결합 구조는 유지 보수를 손쉽게 만들어줌

느슨한 결합의 간편한 구현 - 델리게이트(Delegate)
매번 인터페이스를 만드는 것이 번거로울 수 있음.
→ 함수를 오브젝트처럼 관리하면 어떨까
- 함수를 다루는 방법
- 함수 포인터를 활용한 콜백(callback)함수의 구현
- 가능은 하나 정의하고 사용하는 과정이 꽤나 복잡
- 안정성을 스스로 검증해주어야 함
- C++ 17 규약의 std::bind와 std::function은 느림
- 함수 포인터를 활용한 콜백(callback)함수의 구현
- C#의 델리게이트(delegate) 키워드
- 함수를 객체처럼 다룰 수 있음
- 안정적이고 간편한 선언
- 언리얼 C++도 델리게이트를 지원함
- 느슨한 결합 구조를 간편하고 안정적으로 구현

언리얼 C++의 델리게이트
어떤 객체를 사용할 때 객체 자체에 강한 결합을 하는 것이 아닌 어떤 객체가 가지고 있는 멤버 함수와 델리게이트를 연결해 느슨한 결합을 만들 수 있다.
델리게이트 오브젝트는 복사해도 완전히 안전. C의 함수 포인터와는 다른 점
델리게이트는 가급적 참조 전달해야함
다양한 종류의 델리게이트를 매크로를 사용해 활용할 수 있음
델리게이트 바인딩
선언한 델리게이트를 멤버 함수, 일반 함수, 정적 함수 등 다양한 함수와 연결해주는 함수가 있음
대부분 언리얼 오브젝트의 멤버 함수를 사용해 엮어주는 것을 많이 사용함. | BindUObject()
페이로드 데이터
묶는 객체에 대한 정보를 지정해서 하나의 구문으로 편하게 묶을 수 있음
MyDelegate.BinRaw(&MyFunction, true, 20);
델리게이트 실행
델리게이트에 바인딩하면 묶인 함수를 호출할 수 있게 됨
대표적으로 Execute(), 일대다 호출도 가능
class FLogWriter
{
void WriteToLog(FString); //이 함수 타입을 하나의 객체처럼 사용
}
//델리게이트 매크로로 함수를 객체처럼 지정
DECLARE_DELEGATE_OneParam(FStringDelegate, FString);
class FMyClass
{
//멤버 변수에 등록
FStringDelegate WriteToLogDelegate;
}
TSharedRef<FLogWriter> LogWriter(new FLogWriter());
WriteToLogDelegate.BindSP(LogWriter, &FLogWriter::WriteToLog);
//BindSP로 묶음
//클래스의 메서드에 델리게이트를 동적으로 바인딩
WriteToLogDelegate.Execute(TEXT("Log"));
//Execute로 호출
델리게이트를 사용하면 연결된 객체 정보를 몰라도 원하는 함수를 호출할 수 있음
언리얼 델리게이트 선언 시 고려사항
- 어떤 데이터를 전달하고 받을 것인가? 인자의 수와 각 인자의 타입을 설계
- 몇 개의 인자를 전달할 것인가?
- 어떤 방식으로 전달할 것인가?
- 일대일로 전달
- 일대다로 전달
- 프로그래밍 환경 설정
- C++ 프로그래밍에서만 사용할 것인가?
- UFUNCTION으로 지정된 블루프린트 함수와 사용할 것인가?
- 어떤 함수와 연결할 것인가?
- 클래스 외부에 설계된 C++함수와 연결
- 전역에 설계된 정적 함수와 연결
- 언리얼 오브젝트의 멤버 함수와 연결 (대부분의 경우에 이 방식을 사용)
언리얼 델리게이트 선언 매크로
DECLARE_{델리게이트유형}DELEGATE{함수정보}
- 델리케이트 유형 : 어떤 유형의 델리게이트인지 구상
- 일대일 형태로 C++만 지원한다면 유형은 공란으로 둔다
- DECLARE_DELEGATE
- 일대다 형태로 C++만 지원한다면 MULTICAST를 선언한다
- DECLARE_MULTICAST
- 일대일 형태로 블루프린트를 지원한다면 DYNAMIC을 선언한다
- DECALRE_DYNAMIC
- 일대다 형태로 블루프린트를 지원한다면 DYNAMIC과 MULTICAST를 조합한다
- DECALRE_DYNAMIC_MULTICAST
- 함수 정보: 연동될 함수 형태를 지정
- 인자가 없고 반환값도 없으면 공란으로 둔다
- DECLARE_DELEGATE
- 인자가 하나고 반환값이 없으면 OneParam으로 지정한다
- DECLARE_DELEGATE_OneParam
- 인자가 세 개고 반환값이 있으면 RetVal_ThreeParam’s’로 지정한다.
- DECLARE_DELEGATE_RetVal_ThreeParams (MULTICAST는 반환값을 지원하지 않음)
- 파라미터는 최대 9개까지 지원함
언리얼 델리게이트 매크로 선정 예시
- 학사 정보가 변경되면 알림 주체와 내용을 학생에게 전달
- 두 개의 인자
- 변경된 한사 정보는 다수 인원을 대상으로 발송
- MULTICAST 사용
- 오직 C++ 프로그래밍에서만 사용
- DYNAMIC은 사용하지 않음
→ DECLARE_MULTICAST_DELEGATE_TwoParams 사용
실습 예제
학교에서 진행하는 온라인 수업 활동 예시
- 학사정보(CourseInfo)와 학생(Student)
- 학교는 학사 정보를 관리
- 학사 정보가 변경되면 자동으로 학생에게 알림
- 학생은 학사 정보의 알림 구독을 해지할 수 있음
- 시나리오
- 학사 정보와 3명의 학생
- 시스템에서 학사 정보를 변경
- 학사 정보가 변경되면 알림 구독한 학생들에게 변경 내용을 자동으로 전달
언리얼 델리게이트(Delegate)
언리얼 엔진은 발행 구독 패턴 구현을 위해 델리게이트 기능을 제공
델리게이트의 사전적 의미는 ‘대리자’.
학사 정보의 구독과 알림을 대리해주는 객체
- 시나리오 구현을 위한 설계
- 학사 정보는 구독과 알림을 대행할 델리게이트를 선언
- 학생은 학사 정보의 델리게이트를 통해 알림을 구독
- 학사 정보는 내용 변경시 델리게이트를 사용해 등록한 학생들에게 알림

언리얼 델리게이트의 설계
학사 정보 클래스와 학생 클래스의 상호 의존성을 최대한 없앤다.
- 하나의 클래스는 하나의 작업에만 집중하도록 설계
- 학사 정보 클래스는 델리게이트를 선언하고 알림에만 집중
- 학생 클래스는 알림을 수신하는데만 집중
- 직원도 알림을 받을 수 있도록 유연하게 설계
- 학사 정보와 학생은 서로 헤더를 참조하지 않도록 신경쓸 것
이를 위해 발행과 구독을 컨트롤하는 주체를 설정
- 학사 정보에서 선언한 델리게이트를 중심으로 구독과 알림을 컨트롤하는 주체 설정
- 연결과 활동 주체(MyGameInstance-학교에 대입)

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "CourseInfo.generated.h"
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);
//보통 앞에 F를 붙임, 이벤트가 발생했다는 뜻으로 On, 학사 정보가 변경됐다는 뜻으로 Changed
//언리얼 소스코드를 보면 보통 델리게이트에 Signature라는 접미사를 붙임
//인자 2개
/**
*
*/
UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
GENERATED_BODY()
public:
UCourseInfo();
//델리게이트 클래스 정보를 멤버 변수처럼 등록
FCourseInfoOnChangedSignature OnChanged;
//외부에서 학사정보를 변경할 때 사용할 함수
void ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents);
private:
// 메모리를 동적관리할 일은 없기 때문에
// 추후 블루프린트나 리플렉션을 통해 참조할 일이 없다면
// UPROPERTY를 넣지 않아도 상관없음
FString Contents;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "CourseInfo.h"
UCourseInfo::UCourseInfo()
{
Contents = TEXT("기존 학사 정보");
}
void UCourseInfo::ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
Contents = InNewContents;
UE_LOG(LogTemp, Log, TEXT("[CourseInfo] 학사 정보가 변경되어 알림을 발송합니다."));
OnChanged.Broadcast(InSchoolName, Contents); // OnChanged에 연결된 모든 함수에게 전달
}
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Person.h"
#include "LessonInterface.h"
#include "Student.generated.h"
/**
*
*/
UCLASS()
class UNREALDELEGATE_API UStudent : public UPerson, public ILessonInterface
{
GENERATED_BODY()
public:
UStudent();
virtual void DoLesson() override;
void GetNotification(const FString& School, const FString& NewCourseInfo);
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Student.h"
#include "Card.h"
UStudent::UStudent()
{
Name = TEXT("이학생");
Card->SetCardType(ECardType::Student);
}
void UStudent::DoLesson()
{
ILessonInterface::DoLesson();
UE_LOG(LogTemp, Log, TEXT("%s님은 공부합니다."), *Name);
}
void UStudent::GetNotification(const FString& School, const FString& NewCourseInfo)
{
UE_LOG(LogTemp, Log, TEXT("[Student] %s님이 %s로부터 받은 메세지: %s"), *Name, *School, *NewCourseInfo);
}
Student에는 어디에도 CourseInfo에 대한 헤더를 포함하지 않았음
CourseInfo 또한 Student, Teacher, Staff 등에 관련된 헤더를 포함하지 않았음
완전히 개별적으로 구현
중간에서 중재해주는 존재가 있으면 좋음 (MyGameInstance)
CourseInfo = NewObject<UCourseInfo>(this);
생성한 객체는 클래스 멤버 변수에 들어가서 관리받음
C++에서 포인터로 참조되기 때문에 명시적으로 삭제하지 않는 한 메모리에 계속 있음
이때 MyGameInstance는 CourseInfo를 포함해야하기 때문에 Outer를 MyGameInstance로 선언해주어 CourseInfo가 MyGameInstance의 서브 오브젝트가 되는 확실한 관계(컴포지션)로 만들어 줌
컴포지션 관계가 되어 MyGameInstance가 존재하는 동안 CourseInfo가 메모리에 안전하게 남아 있고 MyGameInstance가 삭제되면 CourseInfo도 삭제됨
Outer가 없는 객체는 명시적으로 삭제하거나 모든 참조가 해제되었을 때 자동으로 소멸됨
Subobject는 Outer에 종속되어 Outer가 GC에 의해 관리되는 것에 따름
- 멤버 변수로 지정: MyGameInstance 내에서 CourseInfo에 대한 참조를 가지고 있는 상태. 하지만 객체의 생명 주기나 소유가 보장되진 않음.
- 소유 관계(Outer 설정): MyGameInstance가 CourseInfo의 Outer가 되어 MyGameInstance의 생명 주기에 종속되며, 가비지 컬렉션에 안전하게 포함됨.
두 개념을 모두 사용하는 것은 안정적인 메모리 관리와 명확한 관계 표현을 위해 필요함
UStudent* Student1 = NewObject<UStudent>();
구문이 실행되면 자동으로 소멸되기 때문에 굳이 Outer를 설정해줄 필요 없음
CourseInfo와는 다른 경우. 장기 유지X 단기적 사용O
NewObject에서 명시적으로 Outer를 지정하지 않으면 자동으로 가비지 컬렉터에 의해 관리됨
MyGameInstance.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"
/**
*
*/
UCLASS()
class UNREALDELEGATE_API UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UMyGameInstance(); //생성자로직
virtual void Init() override;
private:
// 학교는 학사 시스템을 소유하고 있어야함
// 학사 정보는 언리얼 오브젝트이고 포인터로 관리
// 포인터로 관리하면 전방 선언을 할 수 있음
// 언리얼 오브젝트의 포인터를 멤버 변수로 지정할 때는 TObjectPtr 사용
UPROPERTY()
TObjectPtr < class UCourseInfo > CourseInfo;
UPROPERTY()
FString SchoolName;
};
MyGameInstance.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyGameInstance.h"
#include "Person.h"
#include "Student.h"
#include "Teacher.h"
#include "Staff.h"
#include "Card.h"
#include "CourseInfo.h"
UMyGameInstance::UMyGameInstance()
{
SchoolName = TEXT("학교");
}
void UMyGameInstance::Init()
{
Super::Init();
// CDO 안에서 생성할 수도 있지만
// 외부에서 필요할 때만 생성하도록 함
// 원래는 당연히 필요한 것이라 CDO에 선언하는 것이 맞음
CourseInfo = NewObject<UCourseInfo>(this);
// 생성한 객체는 클래스 멤버 변수에 들어가서 관리받음
// C++에서 포인터로 참조되기 때문에 명시적으로 삭제하지 않는 한 메모리에 계속 있음
// 이때 MyGameInstance는 CourseInfo를 포함해야하기 때문에
// Outer를 MyGameInstance로 선언해주면
// CourseInfo가 MyGameInstance의 서브 오브젝트가 되는 컴포지션 관계가 됨
// MyGameInstance가 존재하는 동안 CourseInfo가 메모리에 안전하게 남아 있음
// MyGameInstance가 삭제되면 CourseInfo도 삭제됨
UE_LOG(LogTemp, Log, TEXT("======================================"));
// 구문이 실행되면 자동으로 소멸되기 때문에 굳이 Outer를 설정해줄 필요 없음
// 장기 유지X 단기적 사용
// NewObject에서 명시적으로 Outer를 지정하지 않으면 자동으로 가비지 컬렉터에 의해 관리됨
// Outer가 없는 객체는 명시적으로 삭제하거나 모든 참조가 해제되었을 때 자동으로 소멸
UStudent* Student1 = NewObject<UStudent>();
Student1->SetName(TEXT("학생1"));
UStudent* Student2 = NewObject<UStudent>();
Student2->SetName(TEXT("학생2"));
UStudent* Student3 = NewObject<UStudent>();
Student3->SetName(TEXT("학생3"));
// AddUObject: 클래스 인스턴스를 지정하고 멤버 함수를 직접 묶을 수 있음
// AddUObject(인스턴스, 인스턴스가 가진 클래스 멤버 함수를 레퍼런스로 지정)
CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);
CourseInfo->OnChanged.AddUObject(Student2, &UStudent::GetNotification);
CourseInfo->OnChanged.AddUObject(Student3, &UStudent::GetNotification);
CourseInfo->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));
UE_LOG(LogTemp, Log, TEXT("======================================"));
}

언리얼 C++ 델리게이트
느슨한 결합(Loose Coupling)이 가지는 장점
- 향후 시스템 변경 사항에 대해 손쉽게 대처할 수 있음
느슨한 결합(Loose Coupling)으로 구현된 발행 구독 모델의 장점
- 클래스는 자신이 해야 할 작업에만 집중할 수 있음
- 외부에서 발생한 변경 사항에 대해 영향받지 않음
- 자신의 기능을 확장하더라도 다른 모듈에 영향을 주지 않음
언리얼 C++ 델리게이트의 선언 방법과 활용
- 몇 개의 인자를 가지는가?
- 어떤 방식으로 동작하는가? (MULTICAST 사용 유무 결정)
- 언리얼 에디터와 함께 연동할 것인가? (DYNAMIC 사용 유무 결정)
- 이를 조합해 적합한 매크로 선택
델리게이트는 데이터 기반의 디자인 패턴을 설계할 때 유용하게 사용됨
이득우의 언리얼 프로그래밍 Part1 | 인프런
이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해 강의 | 이득우 - 인프런
이득우 | 대기업 현업자들이 수강하는 언리얼 C++ 프로그래밍 전문 과정입니다. 언리얼 엔진 프로그래머라면 게임 개발전에 반드시 알아야 하는 언리얼 C++ 기초에 대해 알려드립니다., [사진] 언
www.inflearn.com
'게임 개발 > 언리얼 C++' 카테고리의 다른 글
언리얼C++ - 언리얼 컨테이너 라이브러리 2 | 구조체와 Map (0) | 2024.11.18 |
---|---|
언리얼C++ - 언리얼 컨테이너 라이브러리 1 | Array와 Set (0) | 2024.11.17 |
언리얼C++ - 컴포지션(Composition) (2) | 2024.11.15 |
언리얼C++ - 인터페이스(Interface) (2) | 2024.11.14 |
언리얼C++ - 언리얼 오브젝트 리플렉션 시스템 2 (1) | 2024.11.13 |