daisy0461 2023. 12. 29. 01:50

객체지향 결합 

  1. 강한 결합 (Tight Coupling)
    클래스들이 서로 의존성을 가지는 경우를 의미한다.
  2. 느슨한 결합
    실물에 의존하지 않고 추상적 설계에 의존 (DIP 원칙)

강한 결합은 바로 전 글에서 나왔던 CreateDefaultSubobject()로 오브젝트를 추가하는 것과 비슷하다.

결국 생성자에서 오브젝트를 추가하면 해당 오브젝트는 반드시 그 오브젝트를 갖기 때문이다.

 

느슨한 결합에서는 Interface를 활용해서 설계가 가능하다.

이건 예시가 필요할거 같은데

 

강한 결합은 다음과 같다.

class Card
{
public:
	Card(int InId) : Id(InId)
	int Id = 0;
};

class Person
{
public:
	Person(Card InCard) : IdCard(InCard) {}
protected
	Card IdCard;
}

위 처럼 Person Class를 가지는 객체를 만들게 되면 해당 Person Class는 반드시 Card를 가지고 있어야한다.

하지만 Card의 목적은? 결제이다. 요즘 애플페이, 삼성페이 등 실질적인 Card가 없어도 가능하다.

반드시 Person이 Card를 들고있어야하는 것이 아니다.

Card가 아닌 다른 수단으로 결제를 하고 싶다면 Person 코드를 생성자에 Card를 받는 것이 아닌 다른 것으로 바꿔야한다.

 

이를 느슨한 결합으로 해결이 가능하다.

class IPay
{
public:
	virtual bool pay() = 0;		//추상가상함수
};

class Card : public IPay		//Card는 추상가상함수를 상속받아 구현
{
public:
	Card(int InId) : Id(InId) {}
    
	virtual bool pay() {return true;}

private:
	int Id = 0;
}

class Person
{
public:
	Person(IPay* InPay) : Check(InPay) {}
    
protected:
	IPay* Pay;		//IPay에 대한 멤버변수
}

위 코드를 간단하게 강한 결합과 차이점을 통해 정리해보면

Card의 주요 기능인 Pay를 Interface로 뺐다.

그리고 Card는 Pay의 Interface를 상속받고 구현하도록 되어있다.

Person도 IPay라는 객체를 포인터로 만들어서 생성자에 인자로 넣어서 실행하고 있다.

이렇게 된다면 뭐가 다른걸까?

우리는 결제를 Card에 의존하지 않고 IPay에 의존한다. 이후에 애플페이나 삼성페이로 바뀌더라도 결제 방법은 IPay를 상속받아 구현하면 해결이 된다는 것이다.

 

이렇게 결제라는 행동에 중심을 둔 추상화 작업을 위해서 매번 Interface를 만드는 것은 번거롭다.

이때 함수를 오브젝트처럼 관리하면 어떨까? 라는 생각에서 나온 방법이 Delegate이다.

Delegate를 통해 느슨한 결합 구조를 간편하고 안정적으로 구현할 수 있다.

 

Delegate

위 느슨한 결합에 관한 코드를 Delegate를 활용하면 다음과 같이 확실히 줄어든다.

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;
}

public delegate bool CheckDelegate();처럼 함수와 관련된 것을 객체처럼 선언을 하고

public CheckDelegate Check;로 객체를 변수로 선언한 후

Card는 해당 Delegate와 똑같은 유형(bool)의 함수만 지정해서 Person과 Card를 묶어주면 원하는 기능을 구현가능하다.

 

Unreal Delegate

Unreal Delegate 고려사항

  • 어떤 데이터를 전달하고 받을 것인가? -> 인자의 갯수, 일대일, 일대다, 전달 방식
  • 프로그래밍 환경 -> C++에서만 사용할 것인가? BP에서도 사용할 것인가?
  • 어떤 함수와 연결할 것인가? -> 클래스 외부에 설계된 C++, 전역에 설계된 정적 함수, Unreal Object의 멤버함수

Unreal Delegate 매크로

 

Delegate 예시

//CourseInfo.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "CourseInfo.generated.h"

//이처럼 매크로를 적당하게 골라서 사용하면 된다.
//아래는 일대다(MULTICAST), 두개의 인자(TwoParams)를 사용한다.
//여기서 만들어지는 Delegate의 이름은 FCourseInfoOnChangedSignature이며 보통 Delegate이름 앞에 F, 뒤에 Signature를 붙인다.
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);

/**
 * 
 */
UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
	GENERATED_BODY()
	
public:
	//생성자
	UCourseInfo();

	//객체를 생성하는 것처럼 FCourseInfoOnChangedSignature Delegate를 사용하는 OnChanged를 만듦
	FCourseInfoOnChangedSignature OnChanged;

	void ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents);

private:
	FString Contents;
};
//CourseInfo.cpp
#include "CourseInfo.h"

UCourseInfo::UCourseInfo()
{
	//Contents 멤버변수 초기화
	Contents = TEXT("기존 학사 정보");
}

void UCourseInfo::ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
	//Contents를 인자로 들어오는 InNewContents로 변경
	Contents = InNewContents;

	UE_LOG(LogTemp, Log, TEXT("[CourseInfo] 학사 정보가 변경되어 알림을 발송합니다."));
    //OnChanged에 연결된 모든 함수들에게 변경된 것을 알림
	OnChanged.Broadcast(InSchoolName, Contents);
}

 

 

//Student.cpp이며 h파일은 GetNotification과 Name만 있어서 따로 블로그에 넣지 않음
#include "Student.h"

UStudent::UStudent()
{
	Name = TEXT("이학생");
}

void UStudent::GetNotification(const FString& School, const FString& NewCourseInfo)
{
	UE_LOG(LogTemp, Log, TEXT("[Student] %s님이 %s로부터 받은 메시지 : %s"), *Name, *School, *NewCourseInfo);
}

 

//MyGameInstance.h
#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:
	//전방선언 후 UCourseInfo Class를 갖는 객체 CourseInfo를 만듦
	UPROPERTY()
	TObjectPtr<class UCourseInfo> CourseInfo;

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

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

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

	//NewObject<ClassName>(Outer)를 통해 Subobject를 만드는 것이다.
    //Outer는 this로 CourseInfo라는 Subobject가 여기에 속한다는 것이다.
	CourseInfo = NewObject<UCourseInfo>(this);

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

	//Student Subobject를 생성한다.
	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는 FCourseInfoOnChangedSignature의 객체이다.
    //OnChanged라는 Delegate에 같이 사용될 Object를 Add해준다.(AddUObject)
    //그리고 &UStudent::GetNotification과 CourseInfo를 연결해준다.
	CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);
	CourseInfo->OnChanged.AddUObject(Student2, &UStudent::GetNotification);
	CourseInfo->OnChanged.AddUObject(Student3, &UStudent::GetNotification);

	//ChangeCourseInfo를 통해 Delegate에 두 인자를 넣어주고
    //CouseInfo에 있는 ChangeCourseInfo를 실행시키고
    //Broadcast를 통해 알려지면 연결된 GetNotification에 두 인자가 전달되서 실행시킨다.
	CourseInfo->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));

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

위를 보면 결국 Student와 CourseInfo는 각각 CourseInfo.h와 Studnet.h가 없어서 완전히 분리된 상태이다.

가능한 이유가 MyGameInstance가 중간에 중재해주는 객체가 되서 연결 고리가

CourseInfo - (중재자) MyGameInstace - Student 와 같이 중재자를 통해 연결이 되서 분리가 된 것이다.