1-8강 언리얼 설계 - Composition

Ryan Ham·2024년 7월 18일
0

이득우 Unreal

목록 보기
23/23
post-thumbnail

언리얼 C++ 만의 Composition 기법을 사용해 복잡한 언리얼 오브젝트를 효과적으로 생성하기

강의 목표

  • 언리얼 C++의 컴포지션 기법을 사용해 오브젝트의 포함 관계를 설계하는 방법의 학습
  • 언리얼 C++이 제공하는 확장 열거형 타입의 선언과 활용 방법의 학습

SOLID Principle

객체 지향 언어들이 추구하는 원칙의 뼈대를 이루는 SOLID principle이다. S는 Single Responsibility Principle, O는 Open Closed Principle, L은 Liskov Substitution Principle, I는 Interface Segregation Principle, D는 Dependency Inversion Principle을 의미한다. 중요한 원칙이므로 앞글자를 따서 이름까지 외우자!

이번 글에서 다룰 Composition도 이 SOLID 원칙에 기반하여 탄생한 객체 지향 구조이다.


Composition 이란?

  • 객체 지향 프로그래밍의 설계는 크게 상속과 Composition의 활용으로 요약해 볼 수 있다.
  • 객체 지향 설계에서 상속이 가진 Is-A 관계만으로는 설계와 유지보수가 어려움.
  • Composition은 객체 지향 설계에서 Has-A 관계를 구현하는 설계 방법

계층 구조 예시

모든 직업을 가지는 존재는 사람일 수 밖에 없지만, 이 사람들이 모두 다 출입증을 소지하지 않을 수 있다.

위 그림에서, Student, Staff, Teacher는 Is-A 관계라서 다 Person 클래스를 상속받지만, Card는 Has-A 관계이기 때문에 Composition의 개념으로 접근하여야 한다.


언리얼에서 Composition을 구현할 수 있는 2가지 방법

설명에 앞서 용어 정리부터 하자면, 언리얼 Composition에서 자신이 소유하고 있는 언리얼 오브젝트를 SubObject라 하고, SubObject 입장에서 자신을 소유하고 있는 언리얼 오브젝트를 Outer라 한다.

방법 1 : CDO에 미리 언리얼 오브젝트를 생성해 조합(필수적 포함)

우리가 계층 구조의 pawn을 만들때 자주 사용했던 CreateDefaultSubobject()가 바로 이 방법이다. CDO 단에서부터 완성된 object를 SubObject로 넣고, 오브젝트를 NewObject()로 생성하게 되면 계층적으로 완성된 object가 탄생한다. Pawn 밑에 Camera, Camera Arm 등등을 CreateDefaultSubobject()을 통해 붙였던 것을 다시 상기시켜보자.

방법 2 : CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합(선택적 포함)

이 오브젝트를 NewObject()를 통해 생성하면 빈 SubObject를 가진 Object가 생성되게 되고, 나중에 필요하면 SubObject에 대해 NewObject()로 동적 생성한다.


코드를 통해 알아보는 언리얼 Composition

3가지 클래스를 만들것이다. Has-A 관계에서 SubObject에 해당되는 Card 클래스, Card 클래스를 가지는 Person 클래스, Person 클래스를 상속 받는 Student 클래스.

Card 클래스

// Card.h
UENUM()
enum class ECard Type : uint8
{
Student = 1 UMETA(DisplayName = "For Student"),
Teacher UMETA(DisplayName = "For Student"),
Staff UMETA(DisplayName = "For Student"),
Invalid UMETA(DisplayName = "For Student")
}

Card 클래스에서는 enum class 변수 타입을 잡아준다. 기본적으로 열거형의 기본 타입으로는 8byte으로 설정.

UENUM() 매크로를 통해 언리얼 reflection 시스템에 이 열거형을 등록시키고 enum class의 각 field에 UMETA()를 넣어 각 열거형 값에 메타데이터를 추가한다.

// Card.h
UPROPERTY()
ECardType CardType;

타입 선언을 했으면 변수 선언을 위와 같이 한다.

// Card.cpp
CardType = ECardType::Invalid;

cpp 파일에서는 기본 enum class 값을 잡아준다.


Person 클래스

Person 클래스가 Card를 가지도록 설정해보자.
방법1을 사용할 것임.

Composition에서의 전방 선언

// Person.h
// 언리얼 4 스타일 선언
UPROPERTY()
class UCard* Card;
// Person.h
// 언리얼 5에서의 새로운 선언 표준
UPROPERTY()
TObjectptr<class UCard> Card

Person 헤더에서 Card 변수를 선언한다. 언리얼 4까지는 원시 포인터 형태로 변수를 선언했다면, 언리얼 5에서는 TObjectptr를 통해 선언부를 작성하라고 권한다.

Composition 관계시 헤더에서는 전방 선언을 해 주는 것이 좋다. 전방 선언을 하면 헤더에서는 아직 include 하지 않아도 되므로, 의존성을 최대한 없앨 수 있다.

// Person.h에서 Getter & Setter
FORCEINLINE class UCard* GetCard() const {return Card;}
FORCEINLINE void SetCard(class UCard* InCard) {Card = InCard;}

Getter와 Setter 또한 만들어준다. 전방 선언을 하였음으로 class UCard*가 꾸준히 나타나는 것을 확인할 수 있다.

// Person.cpp
UPerson::UPerson()
{
	// 이제는 UCard의 파일을 include 해준다.
	// 
	Card = CreateDefaultSubobject<UCard>();
}

사실 이 부분이 핵심이다. 이전에 언리얼에서 Composition을 하는 2가지 방법이 있다고 했다. 우리는 방법 1을 사용할 것이고 이는 CDO가 만들어질때 CreateDefaultSubobject API를 통해 SubObject를 같이 생성해 버리는 방법이다. CDO 부분에 이렇게 작업을 해 놓으면, 나중에 UPerson을 생성할때 Card 또한 함께 생성되게 된다.

// Student.cpp

UStudent::UStudent()
{
	// Student는 Person을 상속한다.
	// Person의 생성자가 돌고 나서 Student의 생성자가 돌게 된다.
	// 
	Card -> SetCardType(ECardType::Student);
}

Person 클래스를 상속한 Student 클래스. Person 헤더에서 만든 Setter를 통해 Card의 enum 타입을 결정해준다.


정리

  • 언리얼 C++는 Composition을 구현하는 독특한 패턴이 있다.
  • 클래스 기본 객체를 생성하는 생성자 코드를 사용해 복잡한 언리얼 오브젝트를 생성할 수 있다.
  • 언리얼 C++ Composition의 Has-A 관계에 사용되는 용어
    • 내가 소유한 하위 오브젝트 : SubObject
    • 나를 소유한 상위 오브젝트 : Outer
  • 언리얼 C++이 제공하는 확장 열거형을 사용해 다양한 메타 정보를 넣고 활용할 수 있다.
profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글