인터페이스

김지윤·2024년 12월 22일
0

UE5

목록 보기
6/16

인터페이스에 대해 처음 배울 땐
'이 인터페이스를 상속받을 클래스는 반드시 이러한 기능을 갖고 있어야 합니다.'
딱 이 정도 의미인 줄 알았고, 약속 같은 개념이라 이해했다.
물론 위와 같은 이유로 인터페이스를 선언하는 것도 있지만 막상 언리얼 샘플 프로젝트를 뜯어보면 꼭 이렇게만 쓰는 건 아니다.
대표적인 활용법은 참조 관계를 줄이기다.
예를 들어 화면에 있는 몬스터에 마우스 커서를 올리면 윤곽선이 나타나 하이라이팅되는 기능을 구현해보자.

// AGoblin.cpp
void AGoblin::HighlightActor()
{
	GetMesh()->SetRenderCustomDepth(true);
	GetMesh()->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED);
}

// AGhost.cpp
void AGhost::HighlightActor()
{
	GetMesh()->SetRenderCustomDepth(true);
	GetMesh()->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED);
}

몬스터마다 이런 함수를 전부 갖도록 로직을 작성했다고 가정해보자.

// AMyPlayerController.cpp
void AMyPlayerController::CursorTrace()
{
	FHitResult CursorHit;
	GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
	if (!CursorHit.bBlockingHit) return;
    
	AActor* ThisActor = CursorHit.GetActor();
    
    ThisActor->HighlightActor(); <- 어떻게 호출할 거임?
}

마우스 아래에 있는 액터를 변수로 가져오는 데까진 성공해도 Highlight를 호출할 수 없다.
그럼 필요할 때마다 ThisActor를 AGoblin으로 캐스팅하고.. AGhost로 캐스팅하고.. APlayerController의 참조 관계가 아주 난리가 나고 말 거다.
이럴 때 Interface를 쓰면 된다.

// EnemyInterface.h

// Unreal Reflection을 위한 UInterface 선언
// 블루프린트나 가비지 컬렉션 시스템에서 올바르게 동작하도록 보장
UINTERFACE(MinimalAPI)
class UEnemyInterface : public UInterface
{
	GENERATED_BODY()
};

// 내가 실제로 구현해야 하는 인터페이스 정의 부분
class PROJECTNAME_API IEnemyInterface
{
	GENERATED_BODY()

public:
	virtual void HighlightActor() = 0;
};

EnemyInterface를 만들어 HighlightActor라는 이름의 순수 가상 함수를 선언했다.
상속받는 클래스들은 반드시 override해야 하며, 그렇지 않으면 컴파일 오류가 발생한다.
이제 AGoblin과 AGhost가 이 인터페이스를 상속받도록 해주자.
(상속하는 코드는 생략)

// AMyPlayerController.h

// EnemyInterface 전방 선언
class IEnemyInterface;

UCLASS()
class AURA_API AAuraPlayerController : public APlayerController
{
	...
    
	void CursorTrace();

	TScriptInterface<IEnemyInterface> ThisActor;
    ...
};

전방 선언과 함께 멤버 변수로 선언해주고

// AMyPlayerController.cpp
#include "Interaction/EnemyInterface.h"

...

void AMyPlayerController::CursorTrace()
{
	FHitResult CursorHit;
	GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
	if (!CursorHit.bBlockingHit) return;
    
	ThisActor = CursorHit.GetActor();
    
    ThisActor->HighlightActor();
}

아까 로직은 그대로 사용한다.
이러면 AMyPlayerController가 불필요하게 클래스들을 참조할 필요가 없어진다.
당연히 블루프린트로도 가능하다.

상황을 하나 가정하자.
인벤토리를 열 때, 위젯이 화면 오른쪽에서 나타나 화면 가운데까지 온다.
위젯을 오른쪽으로 미는 게 아니라 카메라를 오른쪽으로 밀어서 캐릭터가 왼쪽에 있게끔 하고 싶다.

그래서 카메라의 Location을 오른쪽으로 미는 함수를 캐릭터 블루프린트 에디터에서 구현하고, 인벤토리가 GetPlayerPawn을 통해 가져온 인스턴스를 캐릭터로 캐스팅해 호출했다.
이러면 작동은 한다. 하지만

위 사진은 InventoryPanel의 사이즈 맵인데, 참조 관계를 확인해 어느 정도의 메모리를 차지하는지 알아볼 수 있다.
보면 캐릭터를 참조하면서 쓸데없이 엄청난 메모리를 잡아먹고 있다.

이건 수정한 로직인데, 마찬가지로 GetPlayerPawn을 가져오지만 묻지도 따지지도 않고 BPI_CameraMove라는 인터페이스의 가상 함수를 호출하고 있다.
이렇게 호출해도 동작의 차이는 전혀 없다.
"함수 있으면 호출하고, 아님 말고."
이렇게 이해하면 된다.

다시 사이즈 맵을 확인해보면 캐릭터만큼의 메모리가 빠지면서 가벼워진 걸 볼 수 있다.

인터페이스의 또다른 장점은 다중 상속에 있는데, 기본적으로 일반적인 클래스도 다중 상속이 가능하지만 여러 문제점들이 발생할 수 있다.

  1. 다이아몬드 상속 문제로 중복 멤버 발생
class A {
public:
    void Function() {}
};

class B : public A {};
class C : public A {};

class D : public B, public C {}; // 다이아몬드 상속

이런 식으로 구현하게 되면 컴파일러는 어떤 Function을 호출하라는 건지 알 수 없다.

class A {
public:
    void Function() {}
};

class B : virtual public A {};
class C : virtual public A {};

class D : public B, public C {};

하지만 이렇게 가상 상속을 사용한다면 모두 A의 단일 인스턴스만 상속되기 때문에 D에서 오버라이드한 함수를 호출하게 된다.

  1. 모호성 문제

다중 상속에서는 이름 충돌 문제가 발생할 수 있다.

class A {
public:
    void Function() {}
};

class B {
public:
    void Function() {}
};

class C : public A, public B {
    void CallFunction() {
        Function(); // 컴파일 오류: A::Function인지 B::Function인지 모호함
    }
};

물론 개발자가 주의한다면 예방할 수 있는 문제겠지만, 협업하는 환경이라면 충분히 발생할 수 있는 문제다.

등등.. 이런 문제들이 있지만
인터페이스를 사용한다면 해결할 수 있는 문제들이다.

결론

  1. 상속받을 클래스가 어떤 역할을 수행해야 하는지 미리 약속하고 클래스를 작성할 수 있다.
  2. 불필요한 참조 관계를 제거, 퍼포먼스를 향상시킨다.
  3. 안전한 다중 상속을 통해 유지보수성과 확장성을 높인다.
profile
공부한 거 시간 날 때 작성하는 곳

0개의 댓글