futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

9강: 언리얼 C++ 설계 III - 델리게이트 #35

Closed fkdl0048 closed 4 months ago

fkdl0048 commented 4 months ago

9강: 언리얼 C++ 설계 III - 델리게이트

느슨한 결합(Loose Coupling)

느슨한 결합의 간편한 구현 - 델리게이트(Delegate)

하지만 위 방식은 매번 인터페이스를 만들어야 하기에 번거로울 수 있다. 따라서 함수를 오브젝트와 같이 관리한다면 더욱 편리하게 느슨한 결합을 구현할 수 있다.

C#의 유용한 델리게이트를 따라 언리얼 C++도 델리게이트를 지원한다.

발행 구독 디자인 패턴

델리게이트를 좀 더 깊게 이해하기 위해선 발행 구독 디자인 패턴을 이해하면 좋다.

발행 구독 디자인 패턴은 푸시형태의 알림을 구현하는데 적합한 디자인 패턴으로 발행자(Publisher)와 구독자(Subscriber)로 나뉜다.

장점으로는 제작자와 구독자는 서로를 모르기 때문에 느슨한 결합으로 구성된다. 유지 보수가 쉽고, 유연하게 활용할 수 있으며 테스트가 쉬워진다. 시스템 스케일을 유연하게 조절할 수 있으며, 기능 확장에 용이하다.

옵저버 패턴(Observer Pattern)

예제를 위한 클래스 다이어그램과 시나리오

이번 예제는 학교에서 진행하는 온라인 수업을 예시로 학사 정보와 학생의 관계에 집중한다.

언리얼 델리게이트

언리얼 엔진은 발행 구독 패턴 구현을 위해 델리게이트 기능을 제공한다. 델리게이트의 사전적 의미는 대리자로 학사정보의 구독과 알림을 대리해주는 객체를 의미한다.

언리얼 델리게이트의 선언

언리얼 델리게이트 선언시 고려사항

델리게이트를 설계하기 위해선 많은 사항을 고려해야 한다.

언리얼 델리게이트 선언 매크로

DECLARE_{델리게이트 유형}DELEGATE{함수정보}

C#, Unity의 Action, Event, Func과 유사하다. 미리 구현해 놓은 델리게이트 유형이 있어서 편리하게 사용할 수 있다.

언리얼 델리게이트 선정 예시

DECLARE_MULTICAST_DELEGATE_TwoParams

언리얼 델리게이트의 설계

학사 정보 클래스와 학생 클래스의 상호 의존성을 최대한 없앤다. 즉, 하나의 클래스는 하나의 작업에만 집중하도록 설계한다. (자동적으로 SRP를 준수하게 되면서 리스코프를 제외한 나머지 원칙들도 준수하게 된다.)

학사 정보 클래스는 델리게이트를 선언하고 알림에만 집중하며 학생 클래스는 알림을 수신하는데만 집중한다. 직원도 알림을 받을 수 있도록 유연하게 설계하며, 학사 정보와 학생은 서로 헤더를 참조하지 않도록 신경쓴다.

이를 위해 발행과 구독을 컨트롤하는 주체를 설정한다. 학사 정보에서 선언한 델리게이트를 중심으로 구독과 알림을 컨트롤하는 주체 설정

실습

#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);
}
#pragma once

#include "CoreMinimal.h"
#include "LessonInterface.h"
#include "person.h"
#include "Student.generated.h"

UCLASS()
class UNREALDELEGATE_API UStudent : public Uperson, public ILessonInterface
{
 GENERATED_BODY()

public:
 UStudent();

 virtual void DoLessson() 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::DoLessson()
{
 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);
}
#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:
 UPROPERTY()
 TObjectPtr<class UCourseInfo> CourseInfo;

 UPROPERTY()
 FString SchoolName;
};
#include "MyGameInstance.h"

#include "Card.h"
#include "CourseInfo.h"
#include "Staff.h"
#include "Student.h"
#include "Teacher.h"

UMyGameInstance::UMyGameInstance()
{
 SchoolName = TEXT("학교");
}

void UMyGameInstance::Init()
{
 Super::Init();

 // CDO가 아닌 런타임에 합성으록 가져감
 // CourseInfo가 서브 오브젝트로 등록되기 위해 Outer로 this를 전달 즉, GameInstance의 서브 오브젝트로 등록
 // 이렇게 코드로 컴포지션 관계를 만들 수 있는데, 이는 CDO가 아닌 런타임에 할당 하는 것이다.
 // 좀 더 나가아서 this가 아닌 다른 오브젝트의 서브 오브젝트로 등록할 수도 있다.
 CourseInfo = NewObject<UCourseInfo>(this); 

 UE_LOG(LogTemp, Log, TEXT("======================"));

 // but 이 오브젝트는 지역 변수로 선언되어 함수가 끝나면 메모리에서 해제되기에 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"));

 // 가지고 있는 학사 정보 시스템의 델리게이트에 학생과 델리게이트와 같은 형식을 가진 함수를 바인딩
 CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);
 CourseInfo->OnChanged.AddUObject(Student2, &UStudent::GetNotification);
 CourseInfo->OnChanged.AddUObject(Student3, &UStudent::GetNotification);

 CourseInfo->OnChanged.AddLambda([](const FString& School, const FString& NewCourseInfo)
 {
  UE_LOG(LogTemp, Log, TEXT("[Lambda] 학사 정보가 변경되었습니다. 학교: %s, 정보: %s"), *School, *NewCourseInfo);
 });

 // 학사 정보를 변경 내부에서 Invoke
 CourseInfo->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));

 UE_LOG(LogTemp, Log, TEXT("======================"));
}

제일 중요한 점은 실제로 StudentCourseInfo는 서로 헤더를 참조하지 않는다. 이는 느슨한 결합을 구현한 것이다.

정리

데이터 기반의 디자인 패턴을 설계할 때 유용하게 사용된다.