언리얼 엔진 5

[UE5] 컨테이너(TArray, TMap, TSet)

언린이 2026. 2. 24. 21:57

1. 언리얼 엔진에 자체 컨테이너가 필요한 이유

게임 프로젝트에서는 수천 개의 액터 목록, 인벤토리 아이템, 퀘스트 데이터 등 대량의 데이터를 효율적으로 관리해야 합니다. C++ 표준 라이브러리(STL)에도 std::vector, std::unordered_map, std::unordered_set 같은 훌륭한 컨테이너가 있지만, 언리얼 엔진은 이를 직접 사용하지 않고 자체 컨테이너 라이브러리(UCL)를 구현하여 사용합니다.

그 이유는 크게 세 가지입니다. 첫째, STL은 범용성을 중시하여 컴파일 시간이 길어지는 경향이 있으며, 대규모 게임 프로젝트에서는 이 비용이 누적되어 빌드 시간에 상당한 영향을 줍니다. 둘째, 언리얼 엔진의 리플렉션 시스템과 가비지 컬렉션을 지원하려면 UPROPERTY 매크로와 연동되는 컨테이너가 필요합니다. STL 컨테이너는 이러한 엔진 고유의 시스템과 통합될 수 없습니다. 셋째, 언리얼의 메모리 할당자와 직접 연동되어 게임 런타임에 최적화된 메모리 관리가 가능합니다.

이 글에서는 언리얼 엔진에서 가장 많이 사용되는 세 가지 컨테이너인 TArray, TMap, TSet의 장단점을 구체적인 게임 개발 상황 예시와 함께 비교하여 설명합니다. 각 컨테이너의 상세한 API 사용법보다는, 어떤 상황에서 어떤 컨테이너를 선택해야 하는지 판단하는 기준을 제공하는 것이 이 글의 목적입니다.

2. 세 가지 컨테이너의 핵심 구조

본격적인 비교에 앞서, 각 컨테이너의 내부 구조를 간단히 정리합니다.

TArraystd::vector와 유사한 동적 배열입니다. 요소를 메모리상에 연속적으로 배치하며, 인덱스를 통해 임의 접근이 가능합니다. 언리얼 엔진에서 가장 많이 사용되는 컨테이너이기도 합니다.

TMap은 해시 테이블 기반의 키-값 쌍 컨테이너입니다. 내부적으로 TPair<KeyType, ValueType>을 저장하는 TSet을 캡슐화하고 있으며, 키를 통한 빠른 검색을 지원합니다. std::unordered_map과 유사한 역할을 합니다.

TSet은 해시 테이블 기반의 고유 요소 집합입니다. TMap과 달리 별도의 키 없이 데이터 값 자체를 키로 사용합니다. std::unordered_set과 유사하며, 중복을 허용하지 않습니다.

다음은 세 컨테이너의 주요 연산에 대한 시간 복잡도를 정리한 표입니다.

연산 TArray TMap TSet
인덱스 접근 O(1) 지원 안 함 지원 안 함
키/값 검색 O(N) O(1) O(1)
끝에 추가 O(1) amortized O(1) amortized O(1) amortized
중간 삽입 O(N) 해당 없음 해당 없음
삭제 O(N) O(1) O(1)
순서 보장 보장 비보장 비보장
중복 허용 허용 키 불허 불허
메모리 배치 연속 비연속(Sparse) 비연속(Sparse)

이 표만 보면 TMapTSet이 거의 모든 면에서 우월해 보이지만, 실제 게임 개발에서는 상황에 따라 TArray가 훨씬 더 좋은 선택인 경우가 많습니다. 각 컨테이너의 장단점을 구체적인 상황과 함께 살펴보겠습니다.

3. TArray — 장점과 적합한 상황

3-1. 캐시 친화적인 연속 메모리 배치

TArray의 가장 큰 강점은 요소가 메모리상에 연속으로 배치된다는 점입니다. 현대 CPU는 캐시 라인 단위로 메모리를 읽어오기 때문에, 연속된 메모리에 있는 데이터를 순차적으로 접근하면 캐시 히트율이 극대화됩니다.

예를 들어, 매 프레임 월드에 존재하는 모든 적 캐릭터의 체력을 확인하여 체력 바 UI를 갱신하는 상황을 생각해 봅시다. 이 경우 적 캐릭터가 100명이라면 매 프레임 100개의 요소를 순회해야 합니다.

// 매 프레임 모든 적의 체력 바를 갱신하는 예제
UPROPERTY()
TArray<AEnemyCharacter*> ActiveEnemies;

void AMyGameMode::UpdateEnemyHealthBars()
{
    // TArray는 연속 메모리이므로 순회 시 캐시 효율이 극대화됩니다
    for (AEnemyCharacter* Enemy : ActiveEnemies)
    {
        if (IsValid(Enemy))
        {
            float HealthPercent = Enemy->GetCurrentHealth() / Enemy->GetMaxHealth();
            Enemy->UpdateHealthBarWidget(HealthPercent);
        }
    }
}

위의 코드에서 TArray는 포인터들이 연속된 메모리 블록에 저장되어 있으므로, CPU가 다음 요소를 예측하여 미리 캐시에 로드할 수 있습니다. 동일한 데이터를 TSet이나 TMap에 저장했다면, 내부적으로 Sparse Array 구조를 사용하기 때문에 요소 사이에 빈 공간(gap)이 존재할 수 있고, 이는 캐시 미스를 유발하여 순회 성능이 떨어집니다.

매 프레임 전체 목록을 순회해야 하는 상황이라면 TArray가 가장 적합합니다.

3-2. 인덱스 기반 접근과 순서 보장

TArray는 삽입 순서를 보장하며 인덱스로 O(1) 접근이 가능합니다. 이 특성은 순서가 의미를 갖는 데이터를 다룰 때 필수적입니다.

예를 들어, RPG 게임의 스킬 슬롯 시스템을 구현한다고 가정합니다. 플레이어는 1번~4번 슬롯에 스킬을 등록하고, 숫자 키를 눌러 해당 슬롯의 스킬을 발동합니다.

// 스킬 슬롯 시스템 — 인덱스가 슬롯 번호에 대응합니다
UPROPERTY()
TArray<USkillBase*> SkillSlots;

void APlayerCharacter::InitializeSkillSlots()
{
    // 4개의 슬롯을 미리 확보합니다
    SkillSlots.SetNum(4);
}

void APlayerCharacter::ActivateSkillBySlot(int32 SlotIndex)
{
    // 인덱스로 직접 접근 — O(1)
    if (SkillSlots.IsValidIndex(SlotIndex) && SkillSlots[SlotIndex] != nullptr)
    {
        SkillSlots[SlotIndex]->Activate(this);
    }
}

위 코드처럼 슬롯 번호가 곧 배열의 인덱스에 대응하므로, 별도의 검색 없이 즉시 원하는 스킬에 접근할 수 있습니다. TMap으로도 TMap<int32, USkillBase*> 형태로 구현할 수 있지만, 4개의 연속된 슬롯을 다루는 데에 해시 테이블의 오버헤드를 감수할 이유가 없습니다.

3-3. 소규모 컬렉션에서의 검색 성능

TArray의 검색은 이론상 O(N)이지만, 요소 수가 적을 때(일반적으로 수십 개 이하)는 오히려 TSet이나 TMap보다 빠를 수 있습니다. 해시 기반 컨테이너는 해시 함수 계산, 버킷 탐색 등의 고정 오버헤드가 존재하기 때문입니다. 반면 TArray의 선형 탐색은 연속 메모리 덕분에 캐시 효율이 높아 소규모 데이터에서는 해시 오버헤드보다 빠르게 동작합니다.

예를 들어, 캐릭터가 보유한 버프 목록을 관리한다고 가정합니다. 일반적으로 동시에 적용되는 버프는 5~10개 정도입니다.

// 소규모 버프 목록 — TArray의 선형 탐색으로 충분합니다
UPROPERTY()
TArray<FActiveBuffInfo> ActiveBuffs;

bool APlayerCharacter::HasBuff(EBuffType BuffType) const
{
    // 요소가 5~10개 정도라면 선형 탐색도 충분히 빠릅니다
    for (const FActiveBuffInfo& Buff : ActiveBuffs)
    {
        if (Buff.BuffType == BuffType)
        {
            return true;
        }
    }
    return false;
}

위 코드에서 버프 수가 적기 때문에 TArray의 선형 탐색이 TSet의 해시 검색보다 실질적으로 빠르거나 동등한 성능을 보입니다. 실제로 사용해보면, 요소 수가 수십 개 이하인 경우에는 TArray가 가장 단순하면서도 효율적인 선택인 점이 중요합니다.

4. TArray — 단점과 부적합한 상황

4-1. 대규모 데이터에서의 검색 성능 저하

TArray의 가장 큰 약점은 특정 요소를 찾을 때 선형 탐색(O(N))이 필요하다는 점입니다. 데이터가 수백, 수천 개에 달하면 이 비용은 무시할 수 없게 됩니다.

예를 들어, MMO 게임에서 서버가 관리하는 전체 아이템 데이터베이스에서 아이템 ID로 특정 아이템을 찾는 상황을 생각해 봅시다.

// 나쁜 예시 — 대규모 아이템 데이터에서 TArray 선형 탐색
UPROPERTY()
TArray<FItemData> AllItemDatabase; // 수천 개의 아이템

FItemData* FindItemById_Slow(int32 ItemId)
{
    // 수천 개의 아이템을 하나씩 비교 — O(N)으로 매우 느립니다
    for (FItemData& Item : AllItemDatabase)
    {
        if (Item.ItemId == ItemId)
        {
            return &Item;
        }
    }
    return nullptr;
}

위의 코드에서 아이템이 5,000개라면 최악의 경우 5,000번의 비교가 필요합니다. 이 검색이 아이템 습득, 사용, 거래 등 다양한 시점에서 빈번하게 호출된다면 심각한 성능 병목이 됩니다. 이런 상황에서는 TMap<int32, FItemData>를 사용하여 O(1) 검색을 활용하는 것이 올바른 선택입니다.

4-2. 중간 요소 삭제 시 재배열 비용

TArray는 연속 메모리를 유지해야 하므로, 중간 요소를 삭제하면 그 뒤의 모든 요소가 한 칸씩 앞으로 이동합니다. 이 재배열 비용은 O(N)이며, 삭제가 빈번한 상황에서는 심각한 성능 저하를 유발합니다.

다만, 순서가 중요하지 않은 경우에는 RemoveSwap 또는 RemoveAtSwap을 사용하여 삭제 비용을 O(1)로 줄일 수 있습니다. 이 함수는 삭제할 요소를 마지막 요소와 교체한 뒤 마지막 요소만 제거하므로 재배열이 발생하지 않습니다.

// RemoveSwap을 활용한 빠른 삭제 — 순서가 중요하지 않을 때 사용합니다
void AEnemyManager::RemoveDeadEnemy(int32 Index)
{
    if (ActiveEnemies.IsValidIndex(Index))
    {
        // 마지막 요소와 교체 후 삭제 — O(1)
        ActiveEnemies.RemoveAtSwap(Index);
    }
}

위 코드처럼 RemoveAtSwap은 배열의 순서를 변경하지만, 순회 순서가 중요하지 않은 상황에서는 매우 유용한 최적화 방법입니다.

5. TMap — 장점과 적합한 상황

5-1. 키 기반 즉시 검색

TMap의 핵심 강점은 키를 통한 O(1) 검색입니다. 고유한 식별자로 데이터를 빠르게 조회해야 하는 상황에서 TMap은 최적의 선택입니다.

앞서 TArray에서 문제가 되었던 아이템 데이터베이스 예시를 TMap으로 개선하면 다음과 같습니다.

// 좋은 예시 — TMap을 활용한 O(1) 아이템 검색
UPROPERTY()
TMap<int32, FItemData> ItemDatabase; // 키: 아이템 ID, 값: 아이템 데이터

FItemData* FindItemById_Fast(int32 ItemId)
{
    // 해시 기반 검색으로 아이템 수에 관계없이 즉시 검색 — O(1)
    return ItemDatabase.Find(ItemId);
}

위 코드처럼 Find 함수는 아이템이 5,000개든 50,000개든 거의 동일한 시간에 결과를 반환합니다. 아이템 ID, 플레이어 ID, 퀘스트 ID 등 고유 식별자를 키로 사용하는 모든 시스템에서 TMap은 탁월한 성능을 보여줍니다.

5-2. 복잡한 매핑 관계 표현

TMap은 두 데이터 간의 연관 관계를 명확하게 표현할 수 있습니다. 키와 값의 관계가 코드에 직접 드러나므로 가독성도 뛰어납니다.

예를 들어, 멀티플레이어 게임에서 각 플레이어의 점수를 관리하는 상황입니다.

// 플레이어별 점수 관리 — 키-값 관계가 명확합니다
UPROPERTY()
TMap<APlayerState*, int32> PlayerScores;

void AMyGameState::AddScore(APlayerState* Player, int32 Points)
{
    if (Player == nullptr) return;

    // 키가 이미 존재하면 값을 갱신하고, 없으면 새로 추가합니다
    int32& CurrentScore = PlayerScores.FindOrAdd(Player);
    CurrentScore += Points;
}

int32 AMyGameState::GetPlayerScore(APlayerState* Player) const
{
    const int32* Score = PlayerScores.Find(Player);
    return Score ? *Score : 0;
}

위의 코드에서 FindOrAdd는 키가 없으면 기본값으로 새 항목을 추가하고, 이미 존재하면 기존 값의 참조를 반환합니다. 이런 패턴은 TArray로는 구현하기 번거롭고 성능도 떨어집니다.

6. TMap — 단점과 부적합한 상황

6-1. 순회 성능의 한계

TMap은 내부적으로 Sparse Array 구조를 사용하므로 요소가 메모리상에 연속적으로 배치되지 않습니다. 따라서 전체 요소를 순회하는 성능이 TArray보다 떨어집니다.

예를 들어, 매 프레임 모든 파티클 이펙트의 상태를 갱신해야 하는 상황에서 TMap을 사용하면 불필요한 성능 손실이 발생합니다.

// 비효율적인 예시 — 매 프레임 순회가 필요한 데이터에 TMap 사용
TMap<int32, FParticleState> ActiveParticles;

void AParticleManager::TickAllParticles(float DeltaTime)
{
    // TMap 순회는 Sparse Array 구조로 인해 캐시 미스가 빈번합니다
    for (auto& Pair : ActiveParticles)
    {
        Pair.Value.RemainingTime -= DeltaTime;
        Pair.Value.UpdatePosition(DeltaTime);
    }
}

위의 코드처럼 매 프레임 전체 요소를 순회하는 것이 주 연산이라면, TArray에 데이터를 저장하고 별도로 TMap<int32, int32>를 인덱스 룩업용으로 유지하는 것이 더 효율적일 수 있습니다.

6-2. 메모리 오버헤드

TMap은 해시 테이블을 유지하기 위해 TArray보다 더 많은 메모리를 사용합니다. 해시 버킷, 해시 값 저장, Sparse Array의 빈 슬롯 등의 추가 메모리가 필요합니다. 소량의 단순한 데이터를 저장할 때는 이 오버헤드가 불필요하게 클 수 있습니다.

실제로 사용해보면, 요소가 10개 이하인 경우에는 TMap의 해시 테이블 오버헤드가 검색 성능 이점보다 클 수 있으므로, TArray를 사용하는 편이 메모리와 성능 양면에서 유리한 점이 중요합니다.

7. TSet — 장점과 적합한 상황

7-1. 고유성 보장과 빠른 존재 여부 확인

TSet의 핵심 강점은 중복을 자동으로 방지하면서 O(1) 시간에 특정 요소의 존재 여부를 확인할 수 있다는 점입니다. "이 요소가 포함되어 있는가?"라는 질문에 가장 효율적으로 답할 수 있는 컨테이너입니다.

예를 들어, 오픈 월드 게임에서 플레이어가 이미 방문한 지역을 추적하는 시스템을 생각해 봅시다.

// 방문한 지역 추적 — 중복 방지와 빠른 확인이 동시에 필요합니다
UPROPERTY()
TSet<FName> VisitedRegions;

void APlayerController::OnEnterRegion(FName RegionName)
{
    // 이미 방문했는지 O(1)로 확인합니다
    if (!VisitedRegions.Contains(RegionName))
    {
        VisitedRegions.Add(RegionName);
        // 최초 방문 시 탐험 보상을 지급합니다
        GrantExplorationReward(RegionName);
    }
}

bool APlayerController::HasVisitedRegion(FName RegionName) const
{
    // 해시 기반 O(1) 검색
    return VisitedRegions.Contains(RegionName);
}

위의 코드에서 TSetAdd 시 이미 동일한 요소가 있으면 자동으로 무시하므로, 별도의 중복 체크 로직을 작성할 필요가 없습니다. TArray로 동일한 기능을 구현하려면 매번 Contains(O(N))를 호출한 뒤 추가해야 하므로, 지역 수가 많아질수록 성능 차이가 커집니다.

7-2. 집합 연산 지원

TSet은 합집합, 교집합, 차집합 같은 집합 연산을 지원합니다. 게임 로직에서 두 그룹 간의 공통 요소나 차이를 구해야 할 때 매우 유용합니다.

예를 들어, 두 플레이어가 공통으로 보유한 아이템을 찾는 거래 시스템을 구현하는 상황입니다.

// 두 플레이어의 공통 보유 아이템 태그 찾기
TSet<FGameplayTag> PlayerA_ItemTags;
TSet<FGameplayTag> PlayerB_ItemTags;

TSet<FGameplayTag> CommonTags = PlayerA_ItemTags.Intersect(PlayerB_ItemTags);
TSet<FGameplayTag> UniqueToA = PlayerA_ItemTags.Difference(PlayerB_ItemTags);

위 코드처럼 IntersectDifference 같은 집합 연산을 한 줄로 표현할 수 있습니다. TArray로 동일한 로직을 구현하려면 중첩 루프나 정렬 후 비교 등 상당히 복잡한 코드가 필요합니다.

8. TSet — 단점과 부적합한 상황

8-1. 순서 비보장과 인덱스 접근 불가

TSet은 요소의 삽입 순서를 보장하지 않으며, 인덱스로 요소에 접근할 수 없습니다. 따라서 순서가 중요한 데이터에는 적합하지 않습니다.

예를 들어, 채팅 시스템에서 메시지 기록을 관리한다고 가정합니다. 메시지는 시간 순서대로 표시되어야 하고, "최근 10개 메시지"처럼 인덱스 기반 슬라이싱이 필요합니다. 이런 상황에서 TSet은 전혀 적합하지 않으며, TArray가 올바른 선택입니다.

8-2. 키-값 매핑이 필요한 경우 부적합

TSet은 값 자체가 키 역할을 하므로, 하나의 키로 연관된 다른 데이터를 조회하는 패턴에는 사용할 수 없습니다. "플레이어 ID로 점수를 조회"하는 식의 매핑이 필요하다면 TMap을 사용해야 합니다.

9. 일반 C++과의 비교

언리얼의 TArray, TMap, TSet은 각각 C++ 표준 라이브러리의 std::vector, std::unordered_map, std::unordered_set과 유사한 역할을 합니다. 기본적인 자료구조 개념은 동일하므로, STL에 익숙한 개발자라면 언리얼 컨테이너의 동작 방식을 빠르게 이해할 수 있습니다.

그러나 중요한 차이점이 존재합니다. 첫째, 언리얼 컨테이너는 UPROPERTY() 매크로를 통해 리플렉션 시스템과 통합됩니다. 이를 통해 블루프린트 노출, 자동 시리얼라이제이션, 가비지 컬렉션 연동이 가능합니다. std::vector<UObject*>로는 가비지 컬렉터가 해당 포인터를 추적할 수 없어 댕글링 포인터가 발생할 수 있습니다.

// 언리얼 컨테이너 — GC가 포인터를 추적합니다
UPROPERTY()
TArray<UMyObject*> ManagedObjects; // GC가 이 배열의 UObject 참조를 인식합니다

// STL 컨테이너 — GC가 포인터를 추적하지 못합니다
std::vector<UMyObject*> UnmanagedObjects; // GC가 이 참조를 모르므로 위험합니다

위 코드처럼 UObject 파생 클래스의 포인터를 저장할 때는 반드시 UPROPERTY가 붙은 언리얼 컨테이너를 사용해야 합니다.

둘째, TMapstd::unordered_map과 달리 노드 기반 할당이 아닌 오픈 어드레싱 방식의 해시 테이블을 사용합니다. 이로 인해 메모리 할당 횟수가 적고 캐시 효율이 상대적으로 높지만, 요소의 주소 안정성(pointer stability)은 보장되지 않습니다. 즉, 요소를 추가하거나 삭제한 후에는 기존에 얻어둔 요소의 포인터가 무효화될 수 있습니다.

셋째, 언리얼 컨테이너는 커스텀 할당자 시스템을 지원합니다. TArray의 경우 TInlineAllocator를 사용하여 소규모 배열의 힙 할당을 피할 수 있습니다. 다만 TInlineAllocator 등 비기본 할당자를 사용한 컨테이너는 UPROPERTY로 선언할 수 없다는 제약이 있습니다.

10. 유니티 엔진과의 비교

유니티에서는 C#의 List<T>, Dictionary<TKey, TValue>, HashSet<T>이 각각 언리얼의 TArray, TMap, TSet에 대응하는 역할을 합니다. 기본적인 자료구조 개념은 동일하지만, 엔진과의 통합 방식에서 차이가 있습니다.

유니티의 C# 컬렉션은 가비지 컬렉션이 언어 런타임(CLR) 수준에서 자동 관리되므로, 별도의 매크로 없이도 참조가 안전하게 추적됩니다. 반면 언리얼에서는 UPROPERTY() 매크로를 반드시 선언해야 가비지 컬렉터가 컨테이너 내부의 UObject 참조를 인식할 수 있습니다. 이 점은 C++을 사용하는 언리얼이 메모리 관리에서 더 명시적인 제어를 요구하는 대표적인 예입니다.

언리얼 컨테이너만의 강점은 메모리 레이아웃에 대한 세밀한 제어가 가능하다는 것입니다. TArray는 연속 메모리를 보장하므로 캐시 친화적인 반복이 가능하고, TInlineAllocator를 통해 소규모 배열의 힙 할당을 완전히 회피할 수 있습니다. 유니티의 List<T>는 내부적으로 배열 기반이지만, C#의 관리되는 힙(managed heap)에 할당되므로 네이티브 메모리에 비해 캐시 효율이 낮고 GC 부하가 발생할 수 있습니다. 대규모 게임에서 수천 개의 객체를 매 프레임 처리해야 하는 상황이라면, 이러한 메모리 레이아웃 수준의 제어가 의미 있는 성능 차이를 만들어낼 수 있습니다.

11. 주의사항

11-1. 컨테이너를 함수에 전달할 때 반드시 참조로 전달합니다

컨테이너를 값으로 전달하면 전체 배열의 복사가 발생합니다. 메모리를 새로 할당하고 모든 요소를 복사해야 하므로 성능에 심각한 영향을 줍니다. & 하나를 빠뜨리는 것만으로 큰 비용이 발생할 수 있습니다.

// 나쁜 예시 — 값 전달로 인해 전체 배열이 복사됩니다
void ProcessEnemies(TArray<AEnemyCharacter*> Enemies) { /* ... */ }

// 좋은 예시 — 참조 전달로 복사를 방지합니다
void ProcessEnemies(const TArray<AEnemyCharacter*>& Enemies) { /* ... */ }

위 코드처럼 읽기만 하는 경우 const 참조를, 수정이 필요한 경우 비const 참조를 사용하는 습관을 들여야 합니다.

11-2. TMap에서 [] 연산자 대신 Find를 사용합니다

TMap[] 연산자는 키가 존재하지 않을 때 어서트(assert)를 발생시킬 수 있습니다. 안전한 검색을 위해서는 포인터를 반환하는 Find 함수를 사용해야 합니다.

// 위험한 방식 — 키가 없으면 크래시가 발생할 수 있습니다
FItemData ItemData = ItemDatabase[999];

// 안전한 방식 — 키가 없으면 nullptr을 반환합니다
FItemData* ItemDataPtr = ItemDatabase.Find(999);
if (ItemDataPtr)
{
    // 안전하게 사용합니다
}

11-3. TMap과 TSet의 요소 순서에 의존하지 않습니다

TMap과 TSet은 내부적으로 해시 테이블을 사용하므로, 이터레이션 순서가 삽입 순서와 다를 수 있습니다. 요소를 추가하거나 삭제한 뒤에는 순회 순서가 변경될 수 있으므로, 순서에 의존하는 로직을 작성하면 안 됩니다. 순서가 보장되는 맵이 필요한 경우에는 TSortedMap을 사용할 수 있습니다. TSortedMap은 키를 기준으로 정렬된 상태를 유지하며, 검색은 이진 탐색(O(log N))으로 동작합니다.

11-4. 이터레이션 중 컨테이너를 수정하지 않습니다

세 가지 컨테이너 모두 이터레이션 도중 요소를 추가하거나 삭제하면 정의되지 않은 동작(undefined behavior)이 발생할 수 있습니다. 삭제가 필요하다면 역순 인덱스 루프를 사용하거나, 삭제 대상을 별도로 수집한 뒤 이터레이션 후에 일괄 삭제하는 패턴을 사용해야 합니다.

// 안전한 삭제 패턴 — 역순 인덱스 루프
for (int32 i = ActiveEnemies.Num() - 1; i >= 0; --i)
{
    if (ActiveEnemies[i]->IsDead())
    {
        ActiveEnemies.RemoveAtSwap(i);
    }
}

위 코드처럼 역순으로 순회하면 삭제된 요소 이후의 인덱스에 영향을 받지 않으므로 안전합니다.

11-5. Reserve로 사전 할당하여 재할당을 줄입니다

TArray, TMap, TSet 모두 Reserve 함수를 통해 예상되는 요소 수만큼 메모리를 미리 확보할 수 있습니다. 요소를 하나씩 추가할 때마다 내부 버퍼가 꽉 차면 재할당이 발생하는데, 이때 기존 데이터를 새 메모리로 복사하는 비용이 발생합니다. 예상 크기를 미리 알고 있다면 Reserve를 호출하여 이 비용을 사전에 방지하는 것이 좋습니다.

// 1000개의 적을 추가할 예정이라면 미리 공간을 확보합니다
TArray<FEnemySpawnInfo> SpawnQueue;
SpawnQueue.Reserve(1000);

컨테이너의 핵심은 상황에 맞는 올바른 선택입니다. 매 프레임 전체 순회가 중요하다면 TArray를, 고유 키로 빠르게 검색해야 한다면 TMap을, 중복 없는 멤버십 확인이 필요하다면 TSet을 선택하면 됩니다. 세 컨테이너의 시간 복잡도와 메모리 특성을 정확히 이해하고, 프로젝트의 핵심 경로(critical path)에서 사용하는 컨테이너가 적합한지 검토하는 습관을 들이면, 불필요한 성능 문제를 사전에 예방할 수 있습니다.

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

[UE5] 언리얼 오브젝트 생성과 소멸  (0) 2026.02.26
[UE5] 액터와 컴포넌트 최적화  (0) 2026.02.23
[UE5] 가비지 컬렉션  (0) 2026.02.23
[UE5] 리플렉션 시스템  (1) 2026.02.22