강의 내용
언리얼 델리게이트를 사용해 클래스 간의 느슨한 결합을 구현하기
강의 목표
느슨한 결합의 장점과 이를 편리하게 구현하도록 도와주는 델리게이트의 이해
발행 구독 디자인 패턴의 이해
언리얼 델리게이트를 활용한 느슨한 결합의 설계와 구현의 학습
느슨한 결합(Loose Coupling)이란?
강한 결합
class Card
{
public:
Card(int InId) : Id(InId) {}
ind Id = 0;
}
class Person
{
public:
Person(Card InCard) : IdCard(InCard) {}
protected:
Card IdCard;
};
앞선 예제에서 Person 클래스는 멤버 변수로 Card 클래스를 사용하고 있다. 이렇게 되면 Person은 Card에 대해 의존한다고 표현한다.
만약 학교에 카드가 아닌 핸드폰으로 인증하는 시스템이 도입되었다고 하면, Person과 이를 상속받은 자식 클래스들은 어떻게 대처해야 할까? Person에 멤버변수를 추가하거나 Card를 수정하게 되면 기존에 구현한 자식 클래스들의 코드를 다 수정해야 한다. 이런 문제를 해결하기 위해 제시되는 게 느슨한 결합이다.
실물이 아닌 추상적 설계에 의존하라고 했어. 즉 카드가 아닌 "카드를 통해 어떤 것을 할지"를 생각해야 한다. 왜 Person은 Card가 필요할까 출입을 체크하기 위함이다. 그렇다면 출입에 관련된 추상적인 설계를 구현하고 카드가 이것을 구현하게 구조를 만드는 것이 좋다.
Person은 Card에 의존하지 않고 출입과 관련된 ICheck에 의존하게 된다. 핸드폰으로 출입 수단이 바뀌더라도 핸드폰이 ICheck를 구현할 수 있다면 Person의 코드를 바꾸지 않아도 출입 체크 수단을 변경하는게 가능하다. 이것이 인터페이스가 가지는 장점.
class ICheck
{
public:
virtual bool check() = 0;
};
class Card : public ICheck
{
public:
Card(int InId) : Id(InId) {}
virtual bool check() { return true; }
private:
int Id = 0;
};
class Person
{
public:
Person(ICheck* InCheck) : Check(InCheck){ }
protected:
ICheck* Check;
};
ICheck에 대해서는 가상함수를 구현했다.
하지만 매번 이렇게 행동에 중심을 둔 추상화 작업을 위해서 매번 인터페이스를 만드는 것이 번거로울 수 있다.
느슨한 결함
그렇다면 행동에 대해 어떤 함수를 오브젝트처럼 관리하는 건 어떨까? 생각할 수 있다.
기존 언어에서도 함수를 중심으로 관리하는 기법들이 있었는데 대표적으로 C언어의 경우는 함수 포인터를 활용한 Callback방식이 있다. 하지만 복잡하고, 안정성 검증해줘야 해.
C++17의 bind, function은 느려서 게임에선 적극적으로 사용하기 좀 그래.
C#에서는 델리게이트 키워드를 새롭게 제시
언리얼 C++도 델리게이트 지원. 이걸로 느슨한 결합구조를 간편하고 안정적으로 구현 가능.
public class Card
{
public int Id;
public bool CardCheck() { return true; }
}
public delegate bool CheckDelegate();
public class Person
{
Person(CheckDelegate InCheckDelegate)
{
this.Check = InCheckDelegate;
}
public CheckDelegate Check;
}
delegate로 함수에 대한 객체처럼 선언하고, 맞는 유형의 함수를 인자로 받아서 대입하면 간편하게 사용할 수 있다.
언리얼 델리게이트 공식문서
https://docs.unrealengine.com/5.3/ko/delegates-and-lamba-functions-in-unreal-engine/
델리게이트
C++ 오브젝트 상의 멤버 함수를 가리키고 실행시키는 데이터 유형입니다.
docs.unrealengine.com
델리게이트를 이용하면 C++ 오브젝트 상의 멤버 함수 호출을 안전한 방식으로 할 수 있다.
객체 자체의 강한 결합이 아닌 객체가 가지고 있는 멤버 함수와 델리게이트를 연결해서 느슨한 결합을 만들 수 있다. 안전한 방식이다. 이게 C의 함수 포인터와 차별화 된 기능. 다양한 종류의 델리게이트가 있다. 일대일, 일대다, 블루프린트 연동 등 옵션에 따라 다양한 델리게이트 사용 가능.
델리게이트 바인딩 하기는 함수와 연결하는 거. 대부분 언리얼 오브젝트의 멤버함수를 사용해서 묶어주는 방식을 많이 사용한다.
페이로드 데이터는 묶는 객체에 대한 정보를 지정해서 하나의 구문으로 편하게 묶을 수 있다는 것을 의미함.
이렇게 묶으면 델리게이트를 실행해서 묶인 함수를 호출할 수 있게 된다. 대표적인 API로 Execute가 있다.
어떻게 작동하는지 보려면 예제를 구현해 보는게 좋다.
발행 구독 디자인 패턴
push 형태의 알림(notification)을 구현하는데 적합한 디자인 패턴
발행자와 구독자로 구분
-콘텐츠 제작자는 콘텐츠 생산
-발행자는 배포
-구독자는 콘텐츠 받아 소비
-제작자와 구독자가 서로 몰라도 발행자를 통해 콘텐츠 생산하고 전달 가능
발행구독 디자인 패턴의 장점
- 제작자와 구독자 서로 모르기 때문에 느슨한 결합으로 구성
- 유지 보수 쉽고 유연, 테스트 쉬움
- 시스템 스케일 유연하게 조절 가능, 기능 확장 용이
예시
학사정보(CourseInfo)와 학생(Student)
- 학교는 학사 정보 관리
- 학사정보 변경시 학생에게 알림
- 학생은 학사 정보 알림 구독 해지 가능
시나리오
- 학사 정보와 3명 학생 있음
- 시스템에서 학사정보 변경
- 정보 변경되면 알림을 구독학생에게 변경 내용 전달
언리얼 델리게이트
- 대리자의 의미 : 학사정보의 구독과 알림을 대리해주는 객체
- 시나리오 구현을 위한 설계
-- 학사정보 구독과 알림을 대행할 델리게이트 선언
-- 학생은 학사 정보의 델리게이트를 통해 알림 구독
-- 학사 정보는 내용 변경시 델리게이트를 사용해 등록한 학생들에게 알림
언리얼 델리게이트 선언시 고려사항
- 어떤 데이터르 전달하고 받을 것인가? 인자수와 각각의 타입을 설계
- 프로그래밍 환경 설정
-- C++ 프로그래밍에서만 사용할건인가?
-- UFUNCTION으로 지정된 블루프린트 함수와 사용할 것인가?
- 어떤 함수와 연결할것인가?
언리얼 델리게이트 선언 매크로
- 델리게이트 유형
-- DECLARE_유형
- 함수 정보 : 연동될 함수 형태 지정
언리얼 델리게이트 매크로 선정 예시
- 두개의 인자, 다수 인원 대상 발송, 블루프린트 사용 안함
- DECLARE_MULTICAST_DELEGATE_TwoParams
언리얼 델리게이트의 설계
- 학사 정보 클래스와 학생 클래스의 상호 의존성 최대한 없앤다.
- 발행과 구독을 컨트롤하는 주체 설정
실습
UnrealDelegate라는 이름의 프로젝트를 생성한다.
지난시간 UnrealComposition에 만들었던 모든 클래스를 복사해서 붙여 넣는다.
refresh를 해준다.
헤더들의 UNREALCOMPOSITION_API를 UNREALDELEGATE_API로 바꿔준다.
그리고 모두 저장 버튼을 누른다.
그리고 나서 클래스를 새롭게 추가한다.
Object를 상속 받은 CourseInfo 클래스를 생성한다.
그리고 언리얼 엔진을 종료하고, CourseInfo에 대한 코딩을 시작해 본다.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "CourseInfo.generated.h"
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);
/**
*
*/
UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
GENERATED_BODY()
public:
UCourseInfo();
FCourseInfoOnChangedSignature OnChanged;
void ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents);
private:
FString Contents;
};
그리고 함수 구현을 해준다.
#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);
}
그리고 Student로 이동한다. Student는 똑같은 형식을 가진 함수를 호출해줘야 한다.
#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);
};
이렇게 해준 다음에 구현을 해주면 된다.
#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);
}
이렇게 GetNotification을 구현해주고,
.cpp파일들의 인코딩을 한글 출력이 가능하게 다른 이름으로 저장하기로 가서 인코딩을 수정해준다.
이렇게 Student와 CourseInfo의 델리게이트에 대한 준비를 마쳤어.
눈여겨 볼건 Student.h, .cpp 어디에도 CourseInfo에 대한 헤더를 포함하지 않았어.
CourseInfo도 Student의 .h와 .cpp를 포함하지 않았다.
중간에서 중재해주는 객체는 MyGameInstance 가 진행하도록 할거야.
MyGameInstance는 학교를 대상으로 선언을 했고, 학교는 학사 시스템을 소유 해줘야 한다.
학사 정보는 언리얼 오브젝트이고, 포인터로 관리하기 때문에 전방선언으로 사용할 수 있다.
선언해서 언리얼 오브젝트의 포인터를 멤버변수로 지정할 때는 TObjectPtr을 사용해야 한다.
MyGameInstance.h에서
private:
UPROPERTY()
TObjectPtr<class UCourseInfo> CourseInfo;
를 선언한다.
그리고 MyGameInstance.cpp로 가서
기본학교를 학교로 변경을 하고,
UMyGameInstance::UMyGameInstance()
{
SchoolName = TEXT("학교");
}
CourseInfo를 CDO( Class Default Object )안(생성자)에서 생성할 수도 있는데, 이번에는 외부에서 필요할 때만 생성하게 해보기 위해, 이번에는 Init에서 생성을 해보자.
void UMyGameInstance::Init()
{
Super::Init();
CourseInfo = NewObject<UCourseInfo>(this);
이렇게 하면 CourseInfo는 MyGameInstance의 SubObject가 되고,
MyGameInstance는 CourseInfo의 Outer가 되는 컴포지션 관계를 설정할 수 있게 된다.
MyGameInstance.cpp의 Init에서 로그를 출력하는 부분을 제거를 하고,
#include "MyGameInstance.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();
CourseInfo = NewObject<UCourseInfo>(this);
UE_LOG(LogTemp, Log, TEXT("==================================="));
UStudent* Student1 = NewObject<UStudent>();
Student1->SetName(TEXT("학생1"));
UStudent* Student2 = NewObject<UStudent>();
Student2->SetName(TEXT("학생2"));
UStudent* Student3 = NewObject<UStudent>();
Student3->SetName(TEXT("학생3"));
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("==================================="));
}
이렇게 학생을 3명 만들고, 함수를 연결을 해주고,
CourseInfo의 OnChanged에 학생들을 더하고, 인자로 함수를 넣어준다.
그리고 CourseInfo의 ChangeCourseInfo에 SchoolName과 변경될 TEXT를 넣어준다.
그리고 빌드를 해서 문제가 없는지 확인한다.
성공하면 디버깅 없이 실행을 누른다.

먼저 ChangeCoureInfo가 호출되면
void UCourseInfo::ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
Contents = InNewContents;
UE_LOG(LogTemp, Log, TEXT("[CourseInfo] 학사 정보가 변경되어 알림을 발송합니다."));
OnChanged.Broadcast(InSchoolName, Contents);
}
학사 정보가 변경되어 알림을 발한다 메시지가 뜨고,
Broadcast로 OnChaged에 AddUObject 되었던 학생 3명의 GetNotification에 InSchoolName과 Contents가 전달되면서 메시지를 받게 된다.
학사정보를 학생에게 전달하는 기능을 구현했는데 학사정보와 학생은 어떠한 의존관계를 가지지 않는다. 코드도 심플하게 예제가 끝났어.
이게 언리얼 델리게이트가 가진 장점이다.
정리
언리얼 C++ 델리게이트
1. 느슨한 결합이 가지는 장점
- 향후 시스템 변경 사항에 대해 손쉽게 대처 가능
2. 느슨한 결합으로 구현된 발행 구독 모델의 장점
- 클래스는 자신이 해야 할 작업에만 집중 가능
- 외부에서 발생한 변경 사항에 영향 안받음
- 자신의 기능을 확장하더라도 다른 모듈에 영향 안줌
3. 언리얼 C++의 델리게이트의 선언 방법과 활용
- 몇개의 인자를 가지나?
- 어떤 방식으로 동작하나? (MULTICAST 사용 유무 결정)
- 언리얼 에디터와 함께 연동할 것인가? (DYNAMIC 사용 유무 결정)
- 이를 조합해 적합한 매크로 선택
데이터 기반의 디자인 패턴을 설계할 때 유용하게 사용
'Unreal engine > Unreal C++' 카테고리의 다른 글
| 3_2_UStruct(언리얼 구조체)와 Map (0) | 2023.10.05 |
|---|---|
| 3_1_언리얼 컨테이너 TArray와 TSet (0) | 2023.09.29 |
| 2_2_언리얼 C++ 모던객체지향 설계_컴포지션 (0) | 2023.09.25 |
| 2_1_언리얼 C++ 모던객체지향 설계_인터페이스 (0) | 2023.09.22 |
| 1_6_언리얼 오브젝트의 이해_언리얼 오브젝트 리플렉션 시스템2 (0) | 2023.09.18 |
댓글