Deeper Learning

[UE5] Interface를 활용한 의존성 관리 (Dependency Inversion) 본문

Game Development/Unreal Engine

[UE5] Interface를 활용한 의존성 관리 (Dependency Inversion)

Dlaiml 2024. 6. 25. 18:44

객체지향에서 종속성 관리는 매우 중요하다. 다른 클래스의 method를 사용하기 위해 여러 종속성을 주입하다 보면 결국 클래스의 본 목적과 달리 다른 클래스가 없이는 사용할 수 없는 클래스가 되어버린다. SOLID 원칙 중 추상화에 의존해야 한다는 Dependency Inversion Principal을 언리얼 엔진의 UInterface를 사용하여 만족시키려 한다.

 

앞으로 다른 프로젝트에서도 활용가능하도록 하고 싶은 UI 클래스에서 리슨 서버를 호스팅 하는 Host 버튼, 리슨 서버에 입장하는 Join 버튼을 만들게 되었다.

 

 

Host 버튼을 누르면 UWorld의 ServerTravel을 실행하고 Join을 누르면 입력한 IP 주소로 ClientTravel을 사용하여 서버에 참가한다.

 

특정 Level에 종속되지 않고 멀티플레이 게임에서 전반적으로 사용하기 위한 Host, Join 함수는 GameInstance 클래스에 구현이 되어 있다.

 

해당 구조에서 UI(메인 메뉴)의 Host, Join 버튼에 Host, Join 함수를 바인딩하기 위해서는 현재 프로젝트의 GameInstance의 Host, Join 함수를 호출하여야 한다.

 

HostButton->OnClicked.AddDynamic(this, &UCustomGameInstance::Host);

 

이제 메인메뉴 UI 클래스는 현재 프로젝트의 GameInstance인 UCustomGameInstance 클래스가 없이는 사용이 불가능한 클래스가 되었다.

 

구체적인 로직 구현이 작성된 비교적 low-level의 클래스에 high-level 클래스가 의존하고 있는 형태이다.

 

SOLID의 Dependency Inversion Principle(DIP)은 아래 내용을 포함하고 있다

고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.

 

DIP를 지키면서 현재 UI가 의존성으로 인해 다른 프로젝트에서 쓰일 수 없는 문제를 해결하는 방법은 추상화에 의존하도록 하는 것이다. 

 

언리얼 엔진의 UInterface 클래스를 상속하는 UI Interface 클래스를 만들어 이를 해결해 보자.

 

Unreal Editor에서 Unreal Interface를 상속하는 C++ 클래스를 만들자.

// MenuInterface.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "MenuInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UMenuInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class MYGAME_API IMenuInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void Host() = 0;
	virtual void Join(const FString& HostIPAddress) = 0;
};

 

UMenuInterface는 UObject 기반 클래스로 언리얼 엔진의 리플렉션 시스템을 이용할 수 있도록 하는 클래스라서 내부에 코드를 작성하지 않는다.

IMenuInterface는 순수 가상 함수만 멤버로 가지고 있는 Interface이다. (언리얼 코딩 컨벤션을 따라 Interface를 의미하는  I가 붙어있다)

 

Host와 Join 함수를 순수 가상 함수로 작성하였다.

 

다음으로 메인 메뉴 UI 클래스가 low-level 구현이 포함된 클래스인 UCustomGameInstance가 아닌 Inteface에 의존하도록 변경하였다.

 

// UMainMenu.h에 Interface 추가
private:
  IMenuInterface* MenuInterface:
//

// UMainMenu.cpp

// Before
HostButton->OnClicked.AddDynamic(this, &UCustomGameInstance::Host);
//

// After
HostButton->OnClicked.AddDynamic(this, &UMainMenu::Host);
// ..

// ..
UMainMenu::Host()
{
  MenuInterface->Host(); // Interface의 순수 가상 함수 호출
}
// ..

 

다음으로는 구현이 포함되어 있는 현재 프로젝트의 클래스인 UCustomGameInstance가 IMenuInterface를 상속받도록 하여 의존성을 주입하고 Interface의 순수 가상 함수에 기존 구현 코드를 연결하였다. 

 

// UCustomGameInstance.h
class MYGAME_API UCustomGameInstance : public UGameInstance,
	public IMenuInterface
{
// ..
public:
	UFUNCTION()	
	virtual void Host() override; // Override Pure Virtual Function 

	UFUNCTION()
	virtual void Join(const FString& HostIPAddress) override; // Override Pure Virtual Function
// ..

};

 

cpp 파일에서는 아래와 같이 상속한 Interface의 Host 함수에 기존 Host 구현 코드를 연결하였다.

// UCustomGameInstance.cpp

void UCustomGameInstance::Host() 
{
	// ..
	World->ServerTravel("/Game/MyGame/Maps/MainLevel?listen");
    // ..
}

 

이제 메인메뉴 UI 클래스는 Interface의 기능인 Host, Join을 정의할 수 있는 모든 클래스와 함께 사용이 가능하다. 이전과 다르게 Host, Button을 클릭할 때 이를 처리하는 클래스가 무엇인지 UI 클래스는 알지 못하고 low-level 클래스에 의존하는 문제도 해결되었다.

 

Reference

[0] https://en.wikipedia.org/wiki/Dependency_inversion_principle

 

Dependency inversion principle - Wikipedia

From Wikipedia, the free encyclopedia Software programming object-oriented design methodology In object-oriented design, the dependency inversion principle is a specific methodology for loosely coupled software modules. When following this principle, the c

en.wikipedia.org

 

[1] https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/GameplayArchitecture/Interfaces/

 

Interfaces

Reference to creating and implementing interfaces.

docs.unrealengine.com

 

Comments