언리얼 C++ 인터페이스 클래스를 사용해 보다 안정적으로 클래스를 설계하는 기법을 학습하자.

 

 

목차

     

     


     

     

    언리얼 C++  - 인터페이스


     

    언리얼 C++ 인터페이스

     

    인터페이스란?

    • 객체가 반드시 구현해야 할 행동을 지정하는데 활용되는 타입
    • 다형성(Polymorphism)의 구현, 의존성이 분리(Decouple)된 설계에 유용하게 활용

     

     

    언리얼 엔진에서 게임 콘텐츠를 구성하는 오브젝트의 설계 예시

    • 언리얼 엔진 월드에 배치되는 모든 오브젝트.
      • 월드에 배치되는 모든 오브젝트를 액터라고 한다. 액터(Actor)는 움직이는 물체와 안 움직않는 물체를 모두 통틀은 상위 개념이다. 
    • 움직이지 오브젝트 ( Pawn )
    • 길찾기 시스템을 반드시 사용하면서 움직이는 오브젝트 ( INavAgentInterface  인터페이스를 구현한 Pawn)
      • Pawn 인스턴스가 길찾기 시스템을 반드시 사용하면서 움직이고 싶다면, INavAgentInterface 를 구현하도록 설계되어 있다.

     

     

    예제를 위한 클래스 다이어그램

      • 수업에 참여하는 사람과 참여하지않는 사람의 구분
        • 수업에 반드시 참여해야 하는 학교 구성원:  학생, 선생
        • 수업에 참여하지 않는 학교 구성원:  교직원
        • 수업 행도에 관련된 인터페이스:  ILessonInterface 

     

     

    언리얼 C++ 인터페이스 특징

    • 언리얼 엔진에서 하나의 인터페이스를 생성하면 두 개의 클래스가 생성
      • U 로 시작하는 타입 클래스
      • I 로 시작하는 인터페이스 클래스
    • 객체를 설계할 때 I 인터페이스 클래스를 사용한다.
      • U 타입 클래스 정보는 런타임에서 인터페이스 구현 여부를 파악하는 용도로 사용됨. (이는 언리얼 엔진의 객체 시스템과 호환되기 위함이 목적이다.)
      • 실제로 U 타입 클래스에서 작업할 일은 없음.
      • 실질적으로 인터페이스에 관련된 구성 및 구현은 I 인터페이스 클래스에서 진행.
    • C++ 인터페이스의 특징
      • 추상 타입으로만 선언할 수 있는 Java, C#과 달리 언리얼 엔진에서는 인터페이스에도 구현이 가능함. (언리얼 내부 구현이 C++ 언어를 기반으로 하다 보니 그렇다. 하지만 C++와 마찬가지로 인터페이스에서의 구현은 지양해야 한다)

     

     

    실습


    예제 코드 상속 구조

     


     

     

    예제 코드

     

    MyGameInstance.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "Engine/GameInstance.h"
    #include "MyGameInstance.generated.h"
    
    UCLASS()
    class UNREALINTERFACE_API UMyGameInstance : public UGameInstance
    {
    	GENERATED_BODY()
        
    public:
    	UMyGameInstance();
    	virtual void Init() override;
    
    private:
    	UPROPERTY()
    	FString SchoolName;
    };

     

    MyGameInstance.cpp

    #include "MyGameInstance.h"
    #include "Student.h"
    #include "Teacher.h"
    #include "Staff.h"
    
    UMyGameInstance::UMyGameInstance(){
    	SchoolName = TEXT("학교");
    }
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    	
    	UE_LOG(LogTemp, Log, TEXT("=========================================="));
    	TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() }; // Student, Teacher, Staff 하나씩 추가
    	for (const auto Person : Persons) 
    	{
    		UE_LOG(LogTemp, Log, TEXT("구성원 이름: %s"), *Person->GetName());
    	}
    	UE_LOG(LogTemp, Log, TEXT("=========================================="));
    }

     

     

    LessonInterfac.h

     

    #pragma once
    #include "CoreMinimal.h"
    #include "UObject/Interface.h"
    #include "LessonInterface.generated.h"
    
    // 타입 정보를 저장하기 위함.
    UINTERFACE(MinimalAPI)
    class ULessonInterface : public UInterface
    {
    	GENERATED_BODY()
    };
    
    class UEPARTONE_API ILessonInterface
    {
    	GENERATED_BODY()
    public:
    	virtual void DoLesson() = 0; // 순수 가상함수로 선언
    };

     

     

     

    Student.h

    UCLASS()
    class UEPARTONE_API UStudent : public UPerson, public ILessonInterface
    {
    	GENERATED_BODY()
    	
    public:
    	UStudent();
    	virtual void DoLesson() override;
    };

     

    Student.cpp

    #include "Student.h"
    
    UStudent::UStudent(){
    	Name = TEXT("학생");
    }
    
    void UStudent::DoLesson(){
    	UE_LOG(LogTemp, Log, TEXT("%s 님은 공부합니다."), *Name);
    }

     

     

    Teacher도 Stduent과 같이 Interface 가상함수를 재정의한다.

    Teacher.h

    #pragma once
    #include "CoreMinimal.h"
    #include "Person.h"
    #include "LessonInterface.h"
    #include "Teacher.generated.h"
    
    UCLASS()
    class UNREALINTERFACE_API UTeacher : public UPerson, public ILessonInterface
    {
    	GENERATED_BODY()
    	
    public:
    	UTeacher();
    	virtual void DoLesson() override;
    };

     

    Teacher.cpp

    #include "Teacher.h"
    
    UTeacher::UTeacher(){
    	Name = TEXT("이선생");
    }
    
    void UTeacher::DoLesson()
    {
    	//ILessonInterface::DoLesson(); // 만약 인터페이스의 함수가 순수 가상함수가 아니고 구현부가 있으면 Super키워드가 아닌 Interface::함수로 위의 함수를 가져온다. 하.지.만. 인터페이스에서의 구현은 지.양.해야한다. 웬만하면 하지말자.
    	UE_LOG(LogTemp, Log, TEXT("%s님은 가르칩니다."), *Name);
    }

     


     

     

    정리


     

     

    언리얼 C++ 인터페이스

     

    • 클래스가 반드시 구현해야 하는 기능을 지정하는데 사용함.
    • C++은 기본적으로 다중상속을 지원하나, 언리얼 C++의 인터페이스를 사용해 가급적 축소된 다중상속의 형태로 구현하는 것이 향후 유지보수에 도움된다.
    • 언리얼 C++ 인터페이스는 두 개의 클래스를 생성한다. (U, I 형태 2개)
    • 언리얼 C++ 인터페이스는 추상 타입으로 강제되지 않고, 내부에 기본 함수를 구현할 수 있다.

     

    언리얼 C++ 인터페이스를 사용하면, 
    클래스가 수행해야 할 의무를 명시적으로 지정할 수 있어, 
    좋은 객체 설계를 만드는데 도움을 줄 수 있다.

     


     

     

     

    더 알아보기


     

     

    C++ 언어가 제공하는 다중 상속 기능에서 문제가 될 수 있는 부분

     

    다이아몬드 상속 문제

    이름 충돌(=모호한 이름)

    • 문제:  다이아몬드 구조 - BaseClass, Derived Class x 2, DerivedDerived Class가 있는 상황에서 DerivedDerived Class에서 가상함수를 상속받아 재정의할 때 같은 이름의 함수 때문에 문제 발생.  
    • 해결방법 2가지:
      • 1. dynamic_cast()로 객체를 명시적으로 업캐스팅해서 원하지 않는 버젼을 컴파일러가 볼 수 없게 가린다.
      • 2. 스코프 지정 연산자(::)로 원하는 버젼을 구체적으로 지정한다.

    모호한 베이스 클래스

    • 가장 흔한 사례는 부모가 겹칠 때 발생한다. 

     

    전문가를 위한 C++ 책 p460 ~ 464 참고


     

     

    Java와 C#과 같은 후발언어들은 왜 다중 상속을 사용하지 않고 인터페이스를 사용한 제한적 상속을 구현했을까?

     

    객체지향 설계(OOP)에서 다중 상속은 지양해야된다. 중복 상속으로 인한 문제를 야기할 수 있기 때문이다.

     

    Java와 C#의 인터페이스는 순수 가상함수로만 이루워져 구현부가 없어 온전히 추상 클래스의 역할을 수행한다. 그렇기에 인터페이스에서 선언된 함수들의 구현부에 대한 걱정이 없다.

     

    C++는 이러한 개념이 대두되기 전에 탄생한 언어여서 Java, C#과 달리 인터페이스 전용 클래스가 없다.  그래서 인터페이스 클래스 생성 시 구현부를 정의하지 않는 방법으로 다중상속 관련 문제를 방지하는게 좋다.