언리얼 델리게이트를 사용해 클래스 간의 느슨한 결합을 구현하기 

  • 느슨한 결합의 장점과 이를 편리하게 구현하도록 도와주는 델리게이트의 이해
  • 발행 구독 디자인 패턴의 이해
  • 발행 구독 디자인 패턴을 구현하면서, 언리얼 델리게이트를 활용한 느슨한 결합의 설계 방법을 학습

 

 

인프런 이득우님의 '언리얼 프로그래밍 Part1 - 언리얼 C++의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해] 강의 들으러 가기!

 

 

목차

     

     


     

     

    느슨한 결합


     

     

    강한 결합(Tight Coupling) 과 느슨한 결합(Loose Coupling)

     

    강한 결합 ( Tight Coupling ) 

    • 클래스들이 서로 의존성을 가지는 경우를 의미한다.
    • 아래 예시에서 Person 클래스는 Card 클래스를 가졌는데,
    • 이 때 Person 클래스가 Card 클래스에 대한 의존성을 가진다고 한다.
    • 핸드폰에서도 인증할수 있는 새로운 카드가 도입된다면? 
      • Peson 클래스뿐만 아니라 이를 상속받는 자식 클래스들도 수정해야 한다.

     

    느슨한 결합 ( Loose Coupling )

    • 실물에 의존하지 말고 추상적 설계에 의존하라. (DIP 원칙)
    • 왜 Person은 Card가 필요한가? 출입을 확인해야 하기 때문.
    • 출입에 관련된 추상적인 설계에 의존하자. (⇒행동에 중심을 둔 추상화 작업)
    • ICheck를 상속받은 새로운 카드 인터페이스를 선언해 해결
      • Person은 Card에 의존하지 않고, ICheck 인터페이스에 의존.
      • ICheck 인터페이스를 상속받는 클래스들은 가상함수 check()를 재정의하면 된다. 
    • 이러한 느슨한 결합 구조는 유지 보수를 손쉽게 만들어줌.

     

     

    그러나 이처럼 매번 인터페이스를 만드는 것도 번거로울 수 있으니,

    • C++에서 함수 포인터를 이용했던 것 대신, 언리얼 C++에서는 델리게이트(Delegate)를 제공해준다.

    행동을 객체처럼 관리해보자는 아이디어가 등장했다.

     


     

     

    느슨한 결합의 간편한 구현

     

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

     

     

    언리얼 엔진의 델리게이트

     

    "

    Delegate(델리게이트)로 C++ 오브젝트 상의 멤버 함수 호출을 일반적이고 유형적으로 안전한 방식으로 할 수 있습니다. 델리게이트를 사용하여 임의 오브젝트의 멤버 함수에 동적으로 바인딩시킬 수 있으며, 그런 다음 그 오브젝트에서 함수를 호출할 수 있습니다. 호출하는 곳에서 오브젝트의 유형을 몰라도 말이지요.

    델리게이트 오브젝트는 복사해도 완벽히 안전합니다. 델리게이트는 값으로 전달 가능하나 보통 추천할 만 하지는 않는데, heap 에 메모리를 할당해야 하기 때문입니다. 가급적이면 델리게이트는 항상 참조 전달해야 합니다.

    "

     

     

     

    https://dev.epicgames.com/documentation/ko-kr/unreal-engine/delegates-and-lamba-functions-in-unreal-engine

     

    델리게이트

    C++ 오브젝트 상의 멤버 함수를 가리키고 실행시키는 데이터 유형입니다.

    dev.epicgames.com


     

     

     

     

    발행 구독 디자인 패턴

     


     

     

    발행 구독 디자인 패턴

     

    발행 구독 디자인 패턴 ( Publisher-Subscriber Pattern ) 

    • 푸시(Push) 형태의 알림(Notification)을 구현하는데 적합한 디자인 패턴
    • 발행자(Publisher)와 구독자(Subscriber)로 구분된다.
      • 콘텐츠 제작자는 콘텐츠를 생산한다.
      • 발행자는 콘텐츠를 배포한다.
      • 구독자는 배포된 콘텐츠를 받아 소비한다.
      • 제작자와 구독자가 서로를 몰라도, 발행자를 통해 콘텐츠를 생산하고 전달할 수 있다. (느슨한 결합)

     

    발행 구독 디자인 패턴의 장점

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


     

     

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

     

    • 학교에서 진행하는온라인 수업 활동 예시
    • 학사정보(CourseInfo)와 학생(Student)
      • 학교는 학사 정보를 관리한다.
      • 학사 정보가 변경되면 자동으로 학생에게 알려준다.
      • 학생은 학사 정보의 알림 구독을 해지할 수 있다.
    • 시나리오
      1. 학사 정보와 3명의 학생이 있다.
      2. 시스템에서 학사 정보를 변경한다.
      3. 학사 정보가 변경되면알림 구독한 학생들에게 변경 내용을 자동으로 전달한다. 

     


     

     

    언리얼 델리게이트


     

     

    언리얼 델리게이트 ( Delegate )

     

    • 언리얼 엔진은 발행 구독 패턴 구현을 위해 델리게이트 기능을 제공함.
    • 델리게이트의 사전적 의미는 대리자.
      • 학사 정보의 구독과 알림을 대리해주는 객체
    • 시나리오 구현을 위한 설계 
      • 학사 정보는 구독과 알림을 대행할 델리게이트를 선언.
      • 학생은 학사 정보의 델리게이트를 통해 알림을 구독.
      • 학사 정보는 내용 변경 시 델리게이트를 사용해 등록한 학생들에게 알림.  

     


     

     

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

     

    어떤 데이터를 전달하고 받을 것인가? 인자의 수와 각각의 타입을 설계 

    • 몇 개의 인자를 전달할 것인가?
    • 어떤 방식으로 전달할 것인가?
    • 일대일로 전달
    • 일대다로 전달

     

    프로그래밍 환경 설정

    • C++ 프로그래밍에서만 사용할 것인가?
    • UFUNCTION으로 지정된 블루프린트 함수와 사용할 것인가?

     

    어떤 함수와 연결할 것인가?

    • 클래스 외부에 설계된 C++ 함수와 연결
    • 전역에 설계된 정적 함수와 연결
    • 언리얼 오브젝트의 멤버 함수와 연결 (대부분의 경우 이 방식을 사용)

     


     

     

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

     

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

     

     

    델리게이트 유형 :  어떤 유형의 델리게이트인지 구상한다.

    • DECLARE_DELEGATE:  일대일 형태로 C++만 지원한다면 공란으로 둔다.
    • DECLARE_MULTICAST:  일대다 형태로 C++만 지원한다면 MULTICAST 를 선언한다. (MULTICAST는 여러 개의 함수를 바인딩하고, 모두를 한 번에 실행할 수 있는 델리게이트다.)
    • DECLARE_DYNAMIC:  일대일 형태로 블루프린트를 지원한다면 DYNAMIC 을 선언한다.
    • DECLARE_DYNAMIC_MULTICAST:  일대다 형태로 블루프린트를 지원한다면 DYNAMIC, MULTICAST 를 조합

     

    C++ 블루프린트

    일대일 DECLARE_DELEGATE DECLARE_DYNAMIC
    일대다 DECLARE_MULTICAST DECLARE_DYNAMIC_MULTICAST

     

     

    함수 정보: 연동될 함수 형태

    • 인자가 없고 반환값도 없으면 공란으로 둔다. ex. DECLARE_DELEGATE
    • 인자가 하나, 반환값이 없으면 OneParam
      • DECLARE_DELEGATE_OneParam
    • 인자가 셋, 반환값이 있으면 RetVal_ThreeParams
      • DECLARE_DELEGATE_RetVal_ThreeParams (MULTICAST는 반환값을 지원하지 않는다)
      • 반환값이 있어서 RetVal 을 추가한다. 다만 이 RetVal 는 블루프린트 형식을 지원하는 DYNAMIC 만 선언할 수 있다.
    • 파라미터는 최대 9개까지 지원된다.

     


     

     

     

    언리얼 델리게이트 매크로 선정 예시

     

    • 학사 정보(CourseInfo)가 변경되면 알림 주체와 내용을 학생(Student)에게 전달한다.
      • 두 개의 인자(알림 주체, 알림 내용)를 가진다.
    • 변경된 학사 정보는 다수 인원을 대상으로 발송한다.
      • MULTICAST를 사용
    • 오직 C++ 프로그래밍만 사용한다. 
      • DYNAMIC은 사용하지 않음.

     

    DECLARE_MULTICAST_DELEGATE_TwoParams 매크로를 사용


     

     

     

    언리얼 델리게이트의 설계

     

    학사 정보(CourseInfo) 클래스와 학생(Student) 클래스의 상호 의존성을 최대한 없앤다.

    • 하나의 클래스는 하나의 작업에만 집중하도록 설계
    • 학사 정보 클래스는 델리게이트를 선언하고 알림에만 집중
    • 학생 클래스는 알림 수신하는에만 집중
    • 직원도 알림을 받을 수 있도록 유연하게 설계
    • 학사 정보와 학생은 서로 헤더를 참조하지 않도록 신경쓸 것

    이를 위해 발행과 구독을 컨트롤하는 주체를 설정

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


     

     

    실습 예제


     

     

    예제 코드

     

    CourceInfo.h

    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/NoExportTypes.h"
    #include "CourseInfo.generated.h"
    
    // 뒤에 Signature 접미사를 붙이는 컨벤션이 있다.
    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; // 학사 정보
    };

     

    CourceInfo.cpp

    #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에 연결된 모든 함수들에게 브로드캐스팅.
    }

     

     

    Student.h

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

     

    Student.cpp

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

     

     

    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:
    	UPROPERTY()
    	TObjectPtr<class UCourseInfo> CourseInfo;
    
    	UPROPERTY()
    	FString SchoolName;
    }

     

    MyGameInstance.cpp

    #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();
    
    	// 컴포지션 관계 설정. NewObject<클래스>(여기는 Outer가 들어가는 자리. this 작성함)
        // MyGameInstance는 UCourseInfo의 Outer가 된다(= UCourseInfo는 MyGameInstance의 SubObject가 된다)
    	CourseInfo = NewObject<UCourseInfo>(this); 
    
    	UE_LOG(LogTemp, Log, TEXT("============================"));
    
    	// 아래의 구문이 실행이되면 이 오브젝트는 자동으로 소멸되기 때문에 위와 같이 NewObject<클래스>()에 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 함수에다가 Student 객체 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("============================"));
    }

     

     

    실행화면

     

     

    학사 정보를 학생에게 전달하는 기능을 구현하였다.

    학사 정보와 학생은 어떠한 의존 관계를 가지지 않는다.


     

     

    정리


     

     

    언리얼 C++ 델리게이트

     

    느슨한 결합(Loose Coupling)이 가지는 장점

    • 향후 시스템 변경 사항에 대해 손쉽게 대처할 수 있음.

     

    느슨한 결합(Loose Coupling)으로 구현된 발행 구독 모델의 장점

    • 클래스는 자신이 해야 할 작업에만 집중할 수 있음.
    • 외부에서 발생한 변경 사항에 대해 영향받지 않음.
    • 자신의 기능을 확장하더라도 다른 모듈에 영향을 주지 않음.

     

    언리얼 C++ 델리게이트 선언 방법과 활용

    • 몇 개의 인자를 가지는가?
    • 어떤 방식으로 동작하는가? ( MULTICAST 사용 유무 결정 )
    • 언리얼 에디터와 함께 연동하는가? (DYNAMIC 사용 유무 결정 >> 블루프린트)
    • 이를 조합해 적합한 매크로를 선택

     

    데이터 기반디자인 패턴설계할 때 유용하게 사용 


     

     

     

    참고하면 좋은 사이트

     

    https://community.gamedev.tv/t/difference-between-adddynamic-adduniquedynamic/37003

     

    Difference between AddDynamic & AddUniqueDynamic?

    I have tried to find it on google, UE4 wiki, answer and forum but still can’t figure out what difference between them although they have same result. Actually, I can’t find anything that relate to AddUniqueDynamic. Sorry about my bad English!

    community.gamedev.tv

     

    https://darkcatgame.tistory.com/66

     

    UE4 C++ Delegate 정리 & 샘플 프로젝트

    개인적으로 UE4 C++에서 Delegate를 사용할 때 처음에 굉장히 에러를 많이 겪었습니다, 그래서 이번 포스팅은 UE4 C++에서 Delegate를 사용하는 방법에 대해 정리하고 샘플프로젝트도 만들었습니다. https

    darkcatgame.tistory.com