언리얼 엔진 5

[UE5] 언리얼 오브젝트 생성과 소멸

언린이 2026. 2. 26. 22:57

1. 언리얼만의 오브젝트 생성 방식이 필요한 이유

C++ 개발자라면 객체를 생성할 때 new 키워드를 사용하고, 사용이 끝나면 delete로 해제하는 것이 자연스럽습니다. 그러나 언리얼 엔진에서는 UObject 파생 클래스에 대해 newdelete를 직접 사용하는 것이 금지되어 있습니다.

그 이유는 언리얼 엔진의 핵심 시스템들이 오브젝트의 생성과 소멸 과정에 깊이 관여하기 때문입니다. 리플렉션 시스템은 오브젝트가 생성될 때 클래스 정보를 등록해야 하고, 가비지 컬렉션(GC) 시스템은 오브젝트의 참조 관계를 추적하여 자동으로 메모리를 회수해야 합니다. 시리얼라이제이션 시스템은 오브젝트를 저장하거나 불러올 때 올바른 초기화 순서를 보장해야 합니다. new를 직접 사용하면 이러한 시스템들이 오브젝트를 인식하지 못하여, 가비지 컬렉션이 동작하지 않거나 리플렉션 정보가 누락되는 심각한 문제가 발생합니다.

이 때문에 언리얼 엔진은 오브젝트의 종류와 생성 시점에 따라 세 가지 전용 생성 함수를 제공합니다. 바로 NewObject, CreateDefaultSubobject, SpawnActor입니다. 이 글에서는 세 함수의 역할과 차이를 심층적으로 비교하고, 오브젝트가 소멸되는 과정까지 함께 다룹니다.

2. 세 가지 생성 함수의 핵심 구조

세 함수는 모두 UObject 파생 클래스의 인스턴스를 생성한다는 공통점이 있지만, 생성 대상과 호출 가능한 시점이 명확하게 구분됩니다.

함수 생성 대상 호출 시점 핵심 특징
CreateDefaultSubobject<T>() UObject / UActorComponent 생성자(Constructor) 내부에서만 CDO(Class Default Object)에 등록, 자동 복제
NewObject<T>() UObject / UActorComponent 런타임(생성자 외부) 가장 범용적인 UObject 생성 함수
SpawnActor<T>() AActor 파생 클래스 런타임 월드에 배치하며 전체 액터 라이프사이클 실행

이 표만으로는 각 함수를 정확히 구분하기 어렵습니다. 각 함수가 어떤 상황에서 사용되어야 하는지, 내부적으로 어떤 과정을 거치는지 하나씩 살펴보겠습니다.

3. CreateDefaultSubobject — 생성자 전용 서브오브젝트 생성

3-1. 역할과 동작 방식

CreateDefaultSubobject는 액터의 생성자 내부에서만 호출할 수 있는 함수입니다. 이 함수로 생성된 서브오브젝트는 CDO(Class Default Object)에 등록되어, 해당 클래스의 모든 인스턴스가 동일한 기본 구성을 갖도록 보장합니다.

CDO란 언리얼 엔진이 각 클래스마다 하나씩 생성하는 기본 인스턴스로, 해당 클래스가 가져야 할 기본 프로퍼티 값과 컴포넌트 구성의 템플릿 역할을 합니다. CreateDefaultSubobject로 생성된 컴포넌트는 이 CDO에 기록되어, 이후 이 클래스의 인스턴스가 스폰될 때마다 CDO를 기반으로 동일한 컴포넌트가 자동으로 복제됩니다.

다음은 가장 일반적인 사용 패턴인 액터의 컴포넌트 구성 예제입니다.

AMyCharacter::AMyCharacter()
{
    // 메시 컴포넌트를 기본 서브오브젝트로 생성합니다
    CharacterMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("CharacterMesh"));
    RootComponent = CharacterMesh;

    // 카메라 붐을 루트에 부착합니다
    CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
    CameraBoom->SetupAttachment(RootComponent);

    // 카메라를 붐에 부착합니다
    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->SetupAttachment(CameraBoom);
}

위의 코드에서 TEXT("CharacterMesh") 같은 문자열은 서브오브젝트의 고유 이름입니다. 이 이름은 동일 클래스 내에서 중복될 수 없으며, 시리얼라이제이션과 네트워크 복제에서 서브오브젝트를 식별하는 데 사용됩니다.

3-2. 생성자 외부에서 호출하면 안 되는 이유

CreateDefaultSubobject를 생성자 외부에서 호출하면 어서트(assert)가 발생하며 크래시가 일어납니다. 이는 의도된 동작입니다. CDO는 엔진 초기화 시점에 한 번만 구성되어야 하고, 런타임에 CDO의 구조를 변경하면 이미 생성된 인스턴스들과 불일치가 발생하여 예측 불가능한 동작을 유발하기 때문입니다.

런타임에 동적으로 컴포넌트를 추가해야 하는 경우에는 뒤에서 설명할 NewObject를 사용해야 합니다.

3-3. 네트워크 복제에서의 이점

멀티플레이어 게임에서 CreateDefaultSubobject로 생성된 서브오브젝트는 서버와 클라이언트 모두에서 동일하게 생성됩니다. 액터가 스폰될 때 CDO를 기반으로 각 측에서 독립적으로 서브오브젝트를 구성하므로, 서브오브젝트 자체를 네트워크로 복제할 필요가 없습니다. 이는 네트워크 대역폭을 절약하는 데 중요한 역할을 합니다.

4. NewObject — 런타임 범용 UObject 생성

4-1. 역할과 동작 방식

NewObject는 생성자 외부, 즉 런타임에 UObject 파생 클래스의 인스턴스를 생성하는 범용 함수입니다. 컴포넌트뿐만 아니라 모든 종류의 UObject를 생성할 수 있어, 세 함수 중 가장 넓은 사용 범위를 가집니다.

다음은 NewObject의 기본 사용 예제입니다.

// 런타임에 UObject 파생 클래스를 생성합니다
UMyInventoryItem* NewItem = NewObject<UMyInventoryItem>(this);

위 코드에서 첫 번째 인자 this는 새로 생성되는 오브젝트의 Outer, 즉 소유자를 지정합니다. Outer는 가비지 컬렉션에서 참조 체인의 일부로 사용되며, 오브젝트의 패키지 경로를 결정합니다.

4-2. 런타임 컴포넌트 동적 추가

게임 플레이 중에 액터에 컴포넌트를 동적으로 추가해야 하는 경우 NewObject를 사용합니다. 이때 중요한 점은 생성 후 반드시 RegisterComponent()를 호출해야 한다는 것입니다.

CreateDefaultSubobject는 CDO 구성 단계에서 컴포넌트 등록이 자동으로 처리되지만, NewObject로 런타임에 생성한 컴포넌트는 엔진이 자동으로 등록하지 않습니다. RegisterComponent()를 호출해야 렌더링, 물리, 틱 등 엔진 시스템에 컴포넌트가 인식됩니다.

다음은 런타임에 스태틱 메시 컴포넌트를 동적으로 추가하는 예제입니다.

void AMyActor::AddArmorPlate(UStaticMesh* ArmorMesh)
{
    // 런타임에 새 컴포넌트를 생성합니다
    UStaticMeshComponent* ArmorComp = NewObject<UStaticMeshComponent>(this, TEXT("DynamicArmor"));

    if (ArmorComp)
    {
        ArmorComp->SetStaticMesh(ArmorMesh);
        ArmorComp->SetupAttachment(RootComponent);

        // 반드시 RegisterComponent를 호출해야 엔진이 인식합니다
        ArmorComp->RegisterComponent();
    }
}

위의 코드에서 RegisterComponent() 호출을 빠뜨리면 컴포넌트가 생성되었음에도 화면에 아무것도 렌더링되지 않고, 틱도 호출되지 않습니다. 실제로 사용해보면 이 실수가 매우 흔하게 발생하므로 주의하셔야 합니다.

4-3. UObject 서브클래스 생성

컴포넌트가 아닌 일반 UObject 파생 클래스를 생성하는 데에도 NewObject를 사용합니다. 예를 들어, 인벤토리 아이템, 어빌리티 인스턴스, 데이터 래퍼 객체 등을 런타임에 생성하는 상황입니다.

void AMyPlayerController::GrantAbility(TSubclassOf<UGameplayAbility> AbilityClass)
{
    // UObject 파생 클래스를 런타임에 생성합니다
    UGameplayAbility* AbilityInstance = NewObject<UGameplayAbility>(this, AbilityClass);

    if (AbilityInstance)
    {
        ActiveAbilities.Add(AbilityInstance);
    }
}

위 코드처럼 NewObject의 두 번째 인자로 TSubclassOf를 전달하면 실제 클래스 타입을 런타임에 결정할 수 있습니다. 이 패턴은 블루프린트에서 파생된 클래스를 C++ 코드에서 생성해야 할 때 자주 사용됩니다.

5. SpawnActor — 월드에 액터 스폰

5-1. 역할과 동작 방식

SpawnActorAActor 파생 클래스를 월드에 배치하면서 생성하는 함수입니다. NewObject와 달리, SpawnActor는 단순히 오브젝트를 메모리에 생성하는 것을 넘어서 액터의 전체 라이프사이클을 실행합니다.

SpawnActor 호출 시 내부적으로 다음 순서대로 초기화가 진행됩니다.

  1. 생성자 호출 (CDO 기반 복제)
  2. PostSpawnInitialize — 월드에 등록
  3. PostActorCreated — 스폰된 액터의 후처리
  4. OnConstruction — 블루프린트 컨스트럭션 스크립트 실행
  5. PreInitializeComponents — 컴포넌트 초기화 전 처리
  6. InitializeComponent — 각 컴포넌트 초기화
  7. PostInitializeComponents — 컴포넌트 초기화 후 처리
  8. BeginPlay — 게임플레이 로직 시작

다음은 기본적인 SpawnActor 사용 예제입니다.

void AMyWeapon::Fire()
{
    if (ProjectileClass == nullptr) return;

    FVector SpawnLocation = GetMuzzleLocation();
    FRotator SpawnRotation = GetMuzzleRotation();

    FActorSpawnParameters SpawnParams;
    SpawnParams.Owner = GetOwner();           // 발사한 액터를 소유자로 지정합니다
    SpawnParams.Instigator = GetInstigator(); // 대미지 처리에 사용될 인스티게이터입니다

    // 월드에 투사체 액터를 스폰합니다
    AMyProjectile* Projectile = GetWorld()->SpawnActor<AMyProjectile>(
        ProjectileClass,
        SpawnLocation,
        SpawnRotation,
        SpawnParams
    );
}

위의 코드에서 FActorSpawnParameters를 통해 스폰 시 여러 옵션을 설정할 수 있습니다. Owner는 이 액터를 소유하는 다른 액터를 지정하며, Instigator는 대미지 시스템에서 가해자를 추적하는 데 사용됩니다.

5-2. SpawnActorDeferred — 지연 스폰

일반적인 SpawnActor는 호출 즉시 액터의 전체 초기화 과정이 완료되며, BeginPlay까지 한 번에 실행됩니다. 그러나 스폰하기 전에 액터의 프로퍼티를 먼저 설정해야 하는 상황이 있습니다. 예를 들어, 데이터 테이블에서 읽어온 스탯 값을 액터가 BeginPlay에서 참조해야 하는 경우입니다.

이런 상황에서 SpawnActorDeferred를 사용하면, 액터가 생성된 후 BeginPlay가 호출되기 전에 프로퍼티를 설정할 수 있는 시간 창을 확보할 수 있습니다.

void AEnemySpawner::SpawnEnemyWithData(FEnemyDataTableRow EnemyData)
{
    // 지연 스폰 — BeginPlay가 아직 호출되지 않습니다
    AEnemyCharacter* Enemy = GetWorld()->SpawnActorDeferred<AEnemyCharacter>(
        EnemyData.EnemyClass,
        GetSpawnTransform()
    );

    if (Enemy)
    {
        // BeginPlay 전에 데이터를 설정합니다
        Enemy->MaxHealth = EnemyData.Health;
        Enemy->AttackPower = EnemyData.Attack;
        Enemy->EnemyName = EnemyData.DisplayName;

        // 설정이 끝나면 FinishSpawning을 호출하여 나머지 초기화를 완료합니다
        Enemy->FinishSpawning(GetSpawnTransform());
    }
}

위의 코드에서 SpawnActorDeferred는 생성자와 PostActorCreated까지만 실행한 뒤 멈춥니다. 이후 개발자가 원하는 프로퍼티를 설정하고, FinishSpawning을 호출하면 OnConstructionPostInitializeComponentsBeginPlay 순서로 나머지 초기화가 이어집니다. 이렇게 하면 BeginPlay에서 이미 올바른 데이터를 참조할 수 있습니다.

SpawnActorDeferred를 사용한 뒤 FinishSpawning을 호출하지 않으면 액터가 불완전한 상태로 남게 되므로, 반드시 호출해야 합니다.

6. 세 함수의 심층 비교

6-1. 호출 시점과 내부 동작 차이

세 함수의 가장 근본적인 차이는 호출 가능한 시점과 그에 따른 내부 동작입니다.

CreateDefaultSubobject는 생성자 실행 시점에 호출됩니다. 이 시점에는 아직 월드(UWorld)가 존재하지 않을 수 있으며, 실제 게임 인스턴스가 아닌 CDO를 구성하는 중일 수도 있습니다. 따라서 이 함수로 생성된 서브오브젝트는 CDO의 일부로 등록되고, 이후 인스턴스가 생성될 때 CDO로부터 복제됩니다.

NewObject는 런타임에 호출되며, 전달받은 Outer 오브젝트의 서브오브젝트로 생성됩니다. 컴포넌트를 생성한 경우 RegisterComponent()를 수동으로 호출해야 엔진 시스템에 등록됩니다. CDO와는 무관하게 동작하므로 동적인 구성이 가능합니다.

SpawnActor는 내부적으로 NewObject를 호출하여 액터를 생성한 뒤, 월드에 등록하고 전체 액터 라이프사이클(컴포넌트 초기화, BeginPlay 등)을 실행하는 상위 수준 함수입니다. AActor 파생 클래스에만 사용할 수 있으며, 반드시 유효한 UWorld가 필요합니다.

6-2. 선택 기준 정리

어떤 함수를 사용해야 할지 판단하는 기준을 정리하면 다음과 같습니다.

"생성자 안에서 컴포넌트를 구성하는가?"CreateDefaultSubobject를 사용합니다. 이 경우 컴포넌트는 CDO에 등록되어 모든 인스턴스에 동일하게 적용됩니다. 블루프린트 에디터의 컴포넌트 패널에도 표시됩니다.

"런타임에 AActor가 아닌 UObject를 생성하는가?"NewObject를 사용합니다. 인벤토리 아이템, 데이터 객체, 어빌리티 인스턴스 등 월드에 배치할 필요가 없는 오브젝트에 적합합니다.

"런타임에 동적으로 컴포넌트를 추가하는가?"NewObject를 사용하고 RegisterComponent()를 호출합니다. 장비 장착/해제 등 게임 플레이 중 컴포넌트가 달라지는 상황에 적합합니다.

"런타임에 월드에 액터를 배치하는가?"SpawnActor를 사용합니다. 투사체, 적 캐릭터, 아이템 드롭 등 월드 공간에 존재해야 하는 오브젝트에 적합합니다.

"스폰 전에 액터의 프로퍼티를 설정해야 하는가?"SpawnActorDeferred를 사용한 뒤 FinishSpawning을 호출합니다.

7. 오브젝트 소멸 과정

7-1. 액터의 소멸 — Destroy()

AActor의 소멸은 Destroy() 함수로 시작됩니다. 다만, Destroy()를 호출한다고 해서 즉시 메모리에서 제거되는 것은 아닙니다. Destroy()는 액터를 "소멸 대기(Pending Kill)" 상태로 표시하고, 다음 가비지 컬렉션 주기에서 실제로 메모리가 회수됩니다.

Destroy() 호출 시 내부적으로 진행되는 과정은 다음과 같습니다.

  1. EndPlay가 호출되어 게임플레이 로직이 종료됩니다.
  2. 소유한 컴포넌트들이 월드에서 등록 해제됩니다.
  3. 액터가 월드의 액터 목록에서 제거됩니다.
  4. 액터가 GC 대상으로 표시됩니다.
  5. 다음 GC 주기에서 실제 메모리가 해제됩니다.

다음은 액터를 소멸시키는 기본 예제입니다.

void AMyProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor,
    UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
    // 충돌한 대상에 대미지를 적용합니다
    UGameplayStatics::ApplyDamage(OtherActor, Damage, GetInstigatorController(), this, DamageType);

    // 투사체를 소멸 대기 상태로 표시합니다
    Destroy();
}

위 코드처럼 Destroy()를 호출한 후에도 해당 프레임 내에서는 액터에 대한 포인터가 여전히 유효할 수 있습니다. 그러나 이후 프레임에서는 IsValid() 또는 IsPendingKillPending() 체크가 필요합니다.

7-2. UObject의 소멸 — 가비지 컬렉션

AActor가 아닌 일반 UObjectDestroy() 함수가 없습니다. 대신, 가비지 컬렉션 시스템이 더 이상 참조되지 않는 오브젝트를 자동으로 회수합니다.

언리얼 엔진의 GC는 Mark-and-Sweep 알고리즘을 사용합니다. Mark 단계에서 루트 셋(Root Set)으로부터 도달 가능한 모든 오브젝트를 표시하고, Sweep 단계에서 표시되지 않은 오브젝트를 메모리에서 해제합니다.

UObject가 GC에 의해 회수되지 않도록(살아있게) 유지하는 세 가지 방법이 있습니다.

첫째, UPROPERTY() 매크로가 붙은 포인터로 참조하는 것이 가장 일반적인 방법입니다. GC는 UPROPERTY로 선언된 참조를 추적하므로, 이 참조가 유효한 한 오브젝트가 회수되지 않습니다.

둘째, UObject::AddReferencedObjects()를 오버라이드하여 GC에 직접 참조를 알려줄 수 있습니다. 이 방법은 UPROPERTY를 사용할 수 없는 특수한 상황에서 활용됩니다.

셋째, 오브젝트를 루트 셋에 추가하는 방법(AddToRoot())이 있으나, 이 방법은 일반적인 게임플레이 코드에서는 거의 사용하지 않습니다.

7-3. 명시적 소멸 요청 — MarkAsGarbage

참조가 남아있는 UObject를 강제로 GC 대상으로 지정하고 싶을 때 MarkAsGarbage()를 사용합니다. 이 함수를 호출하면 해당 오브젝트로의 UPROPERTY 참조가 모두 null로 설정되고, 다음 GC 주기에서 소멸 처리됩니다.

과거에는 MarkPendingKill()이 사용되었으나, 최근 엔진 버전에서는 MarkAsGarbage()를 통한 RF_MirroredGarbage 플래그 방식으로 변경되었습니다.

7-4. UObject 소멸 시 호출되는 함수들

GC에 의해 오브젝트가 실제로 소멸될 때, 다음 세 가지 함수가 순서대로 호출됩니다.

BeginDestroy()는 소멸 과정의 시작입니다. 그래픽 스레드 프록시 같은 멀티스레드 리소스를 해제하는 데 사용됩니다.

IsReadyForFinishDestroy()는 엔진이 호출하여 오브젝트가 최종 소멸 준비가 되었는지 확인합니다. false를 반환하면 다음 GC 주기까지 소멸이 유예됩니다. 비동기 리소스 해제가 아직 진행 중인 경우에 활용됩니다.

FinishDestroy()는 내부 데이터 구조를 해제할 마지막 기회입니다. 이 함수가 완료되면 오브젝트의 메모리가 완전히 회수됩니다.

8. 일반 C++과의 비교

일반 C++에서는 new로 객체를 생성하고 delete로 해제하거나, 스마트 포인터(std::shared_ptr, std::unique_ptr)로 수명을 관리합니다. 언리얼 엔진도 C++ 기반이므로, 일반 C++ 구조체나 UObject를 상속받지 않는 클래스에 대해서는 new/delete를 사용할 수 있습니다.

그러나 UObject 파생 클래스에 대해서는 반드시 언리얼 전용 생성 함수를 사용해야 합니다. 그 이유를 구체적으로 비교하면 다음과 같습니다.

일반 C++의 new는 메모리를 할당하고 생성자를 호출하는 것이 전부입니다. 반면 NewObject는 메모리 할당 후 오브젝트를 엔진의 오브젝트 관리 시스템에 등록하고, 리플렉션 정보를 초기화하며, GC 추적 대상에 포함시킵니다.

// 일반 C++ — 개발자가 직접 수명을 관리합니다
FMyPlainStruct* PlainObj = new FMyPlainStruct();
// ... 사용 ...
delete PlainObj; // 반드시 직접 해제해야 합니다

// 언리얼 — 엔진이 수명을 관리합니다
UMyObject* UnrealObj = NewObject<UMyObject>(GetTransientPackage());
// delete 호출 금지 — GC가 자동으로 회수합니다

위 코드처럼 일반 C++에서는 delete를 잊으면 메모리 누수가 발생하고, 이미 해제된 객체를 접근하면 댕글링 포인터 문제가 발생합니다. 언리얼의 GC 시스템은 이러한 문제를 UPROPERTY 참조 추적과 자동 회수를 통해 방지합니다.

소멸 측면에서도 차이가 있습니다. 일반 C++의 delete는 소멸자를 호출하고 즉시 메모리를 해제합니다. 반면 언리얼의 GC는 오브젝트를 즉시 해제하지 않고, Mark-and-Sweep 주기에 맞춰 비동기적으로 처리합니다. 이는 한 프레임에 대량의 오브젝트가 해제되어 프레임 드롭이 발생하는 것을 방지하는 설계입니다.

9. 유니티 엔진과의 비교

유니티에서는 게임 오브젝트(GameObject)와 컴포넌트를 생성할 때 Instantiate() 함수를 사용하고, 소멸시킬 때 Destroy() 또는 DestroyImmediate()를 사용합니다. 언리얼처럼 생성 함수가 세 가지로 나뉘어 있지 않고, Instantiate() 하나로 프리팹 인스턴스화와 오브젝트 복제를 모두 처리합니다. 컴포넌트를 추가할 때는 AddComponent<T>()를 사용하며, 이 역시 생성자 내부인지 런타임인지에 따라 함수가 구분되지 않습니다.

언리얼이 생성 함수를 세 가지로 구분한 데에는 C++ 특유의 메모리 관리 요구사항과 CDO 기반 아키텍처가 반영되어 있습니다. CreateDefaultSubobject는 CDO에 등록하여 시리얼라이제이션과 네트워크 복제를 자동화하고, SpawnActor는 월드 등록과 액터 라이프사이클을 보장합니다. 유니티의 C#은 런타임 리플렉션이 언어 수준에서 지원되고, CLR이 GC를 자동 관리하므로 이러한 구분이 필요하지 않습니다.

언리얼만의 강점은 이렇게 생성 시점을 명확히 분리함으로써, 각 단계에서 엔진이 수행해야 할 최적화(CDO 복제, 컴포넌트 배치 등록, 네트워크 동기화)를 정확한 시점에 적용할 수 있다는 것입니다. 특히 SpawnActorDeferredBeginPlay 전에 프로퍼티를 설정할 수 있는 시간 창을 제공하여, 데이터 드리븐 방식의 액터 구성에서 초기화 순서 문제를 깔끔하게 해결합니다. 유니티에서는 Instantiate 후 별도의 초기화 함수를 호출하는 패턴을 사용해야 하지만, 이 경우 AwakeStart에서 아직 설정되지 않은 값을 참조하는 실수가 발생할 수 있습니다.

10. 주의사항

10-1. UObject에 new를 절대 사용하지 않습니다

UObject 파생 클래스에 new를 사용하면 해당 오브젝트가 엔진의 관리 시스템에 등록되지 않습니다. GC가 이 오브젝트를 추적하지 못하므로 메모리 누수가 발생하며, 리플렉션 정보도 초기화되지 않아 UPROPERTYUFUNCTION이 정상 동작하지 않습니다. 반드시 NewObject, CreateDefaultSubobject, SpawnActor 중 적합한 함수를 사용해야 합니다.

10-2. CreateDefaultSubobject는 생성자에서만 호출합니다

이 함수를 BeginPlay나 다른 런타임 함수에서 호출하면 어서트가 발생하며 에디터가 크래시됩니다. 런타임에 컴포넌트를 추가해야 한다면 반드시 NewObject를 사용하고 RegisterComponent()를 호출해야 합니다.

10-3. NewObject로 생성한 컴포넌트는 반드시 RegisterComponent를 호출합니다

RegisterComponent()를 호출하지 않으면 컴포넌트가 렌더링, 물리, 틱 등의 엔진 시스템에 등록되지 않습니다. 컴포넌트가 생성되었음에도 화면에 보이지 않거나 동작하지 않는 버그의 가장 흔한 원인이 이것입니다.

10-4. Destroy() 후에도 즉시 해제되지 않습니다

Destroy()를 호출한 후 같은 프레임 안에서 해당 포인터에 접근하면 아직 유효한 상태일 수 있습니다. 그러나 다음 프레임 이후에는 GC에 의해 메모리가 회수될 수 있으므로, Destroy() 호출 후에는 포인터를 nullptr로 초기화하는 습관을 들여야 합니다. 또한, UPROPERTY로 선언된 참조는 GC가 자동으로 null 처리하지만, 일반 raw 포인터는 자동으로 null 처리되지 않아 댕글링 포인터 위험이 있습니다.

10-5. SpawnActorDeferred 사용 후 FinishSpawning을 반드시 호출합니다

SpawnActorDeferred로 지연 생성한 액터에 FinishSpawning을 호출하지 않으면, BeginPlay가 실행되지 않고 액터가 불완전한 상태로 월드에 남게 됩니다. 이런 액터는 물리 시뮬레이션이나 틱 처리에서 예측 불가능한 동작을 유발할 수 있습니다.

10-6. Outer 설정에 주의합니다

NewObject의 첫 번째 인자인 Outer는 생성되는 오브젝트의 소유자를 결정합니다. Outer가 GC에 의해 회수되면 그에 속한 서브오브젝트도 함께 회수됩니다. 따라서 Outer를 nullptr이나 GetTransientPackage()로 설정하면 오브젝트가 예상보다 빨리 회수되거나, 반대로 레벨 전환 시에도 살아남는 문제가 발생할 수 있습니다. 오브젝트의 수명이 소유자와 일치하도록 적절한 Outer를 지정하는 것이 중요합니다.

언리얼의 오브젝트 생성은 CreateDefaultSubobject(생성자 전용 CDO 등록), NewObject(런타임 범용 생성), SpawnActor(월드에 액터 배치)의 세 가지로 명확하게 구분됩니다. 소멸은 액터의 경우 Destroy(), 일반 UObject는 참조 제거를 통한 GC 자동 회수로 이루어집니다. 생성 시점과 대상에 맞는 올바른 함수를 선택하고, UPROPERTY로 참조를 관리하여 GC가 정상 동작하도록 보장하는 것이 안정적인 언리얼 C++ 개발의 기본입니다.

'언리얼 엔진 5' 카테고리의 다른 글

[UE5] 컨테이너(TArray, TMap, TSet)  (1) 2026.02.24
[UE5] 액터와 컴포넌트 최적화  (0) 2026.02.23
[UE5] 가비지 컬렉션  (0) 2026.02.23
[UE5] 리플렉션 시스템  (1) 2026.02.22