게임 엔진/Unreal
[Unreal Engine] Composition
Henzee
2025. 6. 16. 22:06
1. 언리얼 C++의 컴포지션 기법
1) 컴포지션 (Composition)

- 객체 지향 설계에서 상속이 가진 Is-A 관계만 의존해서는 설계와 유지보수가 어려움
- 컴포지션은 객체 지향 설계에서 Has-A 관계를 구현하는 설계 방법
- 컴포지션의 활용
- 복합적인 기능을 거대한 클래스를 효과적으로 설계하는데 유용하게 사용할 수 있음
2) 모던 객체 설계 기법과 컴포지션
- 좋은 객체 지향 설계 패턴을 제작하기 위한 모던 객체 설계 기법 (SOLID)
- Single Responsibility Principle (단일 책임 원칙)
- 하나의 객체는 하나의 의무만 가지도록 설계한다.
- Open-Closed Principle (개방 폐쇄 원칙)
- 기존에 구현된 코드를 변경하지 않으면서 새로운 기능을 추가할 수 있도록 설계한다.
- Liskov Substitution Principle (리스코프 치환 원칙)
- 자식 객체를 부모 객체로 변경해도 작동에 문제없을 정도로 상속을 단순히 사용한다.
- Interface Segregation Design (인터페이스 분리 원칙)
- 객체가 구현해야 할 기능이 많다면 이들을 여러 개의 단순한 인터페이스들로 분리해 설계한다.
- Dependency Injection Principle (의존성 역전 원칙)
- 구현된 실물보다 구축해야 할 추상적 개념에 의존한다.
→ 모던 객체 설계 기법의 설계 핵심은 상속을 단순화하고, 단순한 기능을 가진 다수의 객체를 조합해 복잡한 객체를 구성하는데 있음
3) 컴포지션 설계 예시
- 학교 구성원 시스템의 설계 예시
- 학교 구성원을 위해 출입증을 만들기로 한다.
- 출입증은 Person에서 구현해 상속시킬 것인가? 아니면 컴포지션으로 분리할 것인가?
- Person에서 직접 구현해 상속시키는 경우의 문제
- 새로운 형태의 구성원이 등장한다면(예를 들어, 출입증이 없는 외부 연수생) Person을 수정할 것인가?
- 상위 클래스 Person을 수정하면, 하위 클래스들의 동작은 문제 없음을 보장할 수 있는가?
- 따라서 설계적으로 출입증은 컴포지션으로 분리하는 것이 바람직함
- 그렇다면 컴포지션으로만 포함시키면 모든 것이 해결될 수 있는가?
→ 효과적인 설계를 위해 프로그래밍 언어가 제공하는 고급 기법을 활용해야 함
4) 예제를 위한 클래스 다이어그램

- 학교 구성원임을 증명하는 출입증 카드 부여
- 학생, 교사, 직원 모두가 상시 지니고 있음
- 향후 확장성을 고려해 컴포지션으로 구현
5) 언리얼 엔진에서 컴포지션 구현 방법

- 하나의 언리얼 오브젝트에는 항상 클래스 기본 오브젝트 (CDO) 존재
- 언리얼 오브젝트에 다른 언리얼 오브젝트를 조합하는 두 가지 방법
- 방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합 (필수적 포함)
- 방법 2: CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합 (선택적 포함)
- 언리얼 오브젝트 생성 시 컴포지션 정보 구축 가능
- 내가 소유한 언리얼 오브젝트: Subobject
- 나를 소유한 언리얼 오브젝트: Outer
2. 컴포지션을 활용한 예제 구현
1) Card 클래스 생성

// Header File
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Card.generated.h"
UENUM()
enum class ECardType : uint8
{
Student = 1 UMETA(DisplayName = "For Student"),
Teacher UMETA(DisplayName = "For Teacher"),
Staff UMETA(DisplayName = "For Staff"),
Invalid
};
/**
*
*/
UCLASS()
class HELLOUNREAL_API UCard : public UObject
{
GENERATED_BODY()
public:
UCard();
ECardType GetCardType() const { return CardType; }
void SetCardType(ECardType InCardType) { CardType = InCardType; }
private:
UPROPERTY()
ECardType CardType;
UPROPERTY()
uint32 Id;
};
// Cpp File
#include "Card.h"
UCard::UCard()
{
CardType = ECardType::Invalid;
Id = 0;
}
2) Person 클래스에 컴포지션 구현
- Person 클래스
// Header File
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Person.generated.h"
/**
*
*/
UCLASS()
class HELLOUNREAL_API UPerson : public UObject
{
GENERATED_BODY()
public:
UPerson();
FORCEINLINE const FString& GetName() const { return Name; }
FORCEINLINE void SetName(const FString& InName) { Name = InName; }
FORCEINLINE class UCard* GetCard() const { return Card; }
FORCEINLINE void SetCard(class UCard* InCard) { Card = InCard; }
protected:
UPROPERTY()
FString Name;
UPROPERTY()
TObjectPtr<class UCard> Card;
};
// Cpp File
#include "Person.h"
#include "Card.h"
UPerson::UPerson()
{
Name = TEXT("홍길동");
Card = CreateDefaultSubobject<UCard>(TEXT("NAME_Card")); // 인자로 FName을 넣어주기
}
- Student 클래스
// Cpp File
#include "Card.h"
UStudent::UStudent()
{
Name = TEXT("이학생");
Card->SetCardType(ECardType::Student);
}
- Teacher 클래스
// Cpp File
#include "Card.h"
UTeacher::UTeacher()
{
Name = TEXT("이선생");
Card->SetCardType(ECardType::Teacher);
}
- Staff 클래스
// Cpp File
#include "Card.h"
UStaff::UStaff()
{
Name = TEXT("이직원");
Card->SetCardType(ECardType::Staff);
}
3) MyGameInstance에서 로그 출력
// Cpp File
#include "MyGameInstance.h" // 해당 오브젝트가 선언된 헤더가 맨 위에 있어야 함
#include "Student.h"
#include "Teacher.h"
#include "Staff.h"
#include "Card.h"
UMyGameInstance::UMyGameInstance()
{
SchoolName = TEXT("기본학교"); // CDO에 기본값으로 저장됨
}
void UMyGameInstance::Init()
{
Super::Init();
UE_LOG(LogTemp, Log, TEXT("============================"));
TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };
for (const auto Person : Persons)
{
const UCard* OwnCard = Person->GetCard();
check(OwnCard);
ECardType CardType = OwnCard->GetCardType();
//UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 : %d"), *Person->GetName(), CardType);
// enum class에서 정의한 DisplayName 출력하기
const UEnum* CardEnumType = FindObject<UEnum>(nullptr, TEXT("/Script/HelloUnreal.ECardType"));
if (CardEnumType)
{
FString CardMetaData = CardEnumType->GetDisplayNameTextByValue((int64)CardType).ToString();
UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 : %s"), *Person->GetName(), *CardMetaData);
}
}
UE_LOG(LogTemp, Log, TEXT("============================"));
}
