UE5 TObjectPtr·TSubclassOf와 TArray/TSet/TMap
언리얼이 생 포인터를 감싸는 이유
언리얼 코드에는 생 포인터 대신 TObjectPtr이, 클래스 타입 대신 TSubclassOf가 나온다. 두 래핑이 각각 무엇을 대신 처리하는지, 그 위에서 컨테이너 셋을 고르는 기준까지.
언리얼 코드를 읽다 보면 생 포인터 대신 TObjectPtr이, 클래스 타입 대신 TSubclassOf가 나온다. 감싸는 데는 이유가 있고, 그 이유를 모르면 둘 중 아무거나 쓰게 된다. 이 글에서는 그 두 래핑 포인터가 각각 무엇을 대신 처리하는지, 그리고 그 위에서 컨테이너 세 종(TArray·TSet·TMap)을 어떤 기준으로 고르는지를 이야기하려 한다.
수업에서 함께 다룬 “좋은 코드” 이야기와, 이어서 잡은 인벤토리 필수 과제·Pawn 3D 캐릭터 도전 과제 계획은 이 글의 주제에서 벗어나 덜어냈다.
TObjectPtr
UE4까지 쓰던 UObject*(원시 포인터)를 대체하려고 UE5에서 도입된 템플릿 기반 포인터다. 지연 로딩(Lazy Loading)과 액세스 트래킹 두 기능을 제공한다.
지연 로딩 (Lazy Loading)
에디터에서 에셋을 불러올 때 모든 데이터를 한꺼번에 메모리에 올리면 무거워진다. TObjectPtr은 변수에 담긴 에셋을 진짜 필요할 때 로드하게 돕는 스마트 핸들이다. 여기서 “필요할 때”란 실제 코드가 그 객체에 접근하는 순간을 말한다.
1
2
3
4
5
6
7
8
// 1. 내부 멤버 함수를 호출할 때 (가장 흔한 케이스)
MyMesh->GetName();
// 2. 다른 변수에 대입하려고 할 때
UStaticMesh* RawMesh = MyMesh;
// 3. 조건문에서 검사할 때
if (MyMesh != nullptr) { ... }
반면 지역 변수나 잠깐 쓰이는 매개변수는 이미 메모리에 올라온 객체를 잠깐 가리키는 경우가 대부분이라 지연 로딩이 필요 없고, 원시 포인터가 더 효율적이다.
액세스 트래킹
언리얼 GC(가비지 컬렉터)는 UPROPERTY로 선언된 변수들을 추적해 사용 중인 객체를 판단한다. TObjectPtr은 여기에 더해 “이 객체가 어디서 참조되고 있는지”를 더 정밀하게 추적한다. 지역 변수는 함수가 끝나면 사라져 GC 추적 대상이 아니므로 이 기능이 필요 없다.
정리
| 상황 | 권장 |
|---|---|
헤더에서 UPROPERTY() 붙인 멤버 변수 | TObjectPtr<T> |
| 지역 변수, 매개변수 | 원시 포인터 T* |
주의할 점
성능 향상 도구가 아니라, 관리 효율을 높이는 에디터/개발자용 도구다. 최종 패키징 시 자동으로 원시 포인터로 변환되니 런타임 성능 저하는 없지만, 그 대신 패키징 후에는 지연 로딩·액세스 트래킹도 함께 사라진다. 실제 최적화는 레벨 스트리밍이나 소프트 포인터로 해야 한다.
1
2
3
4
5
6
7
// 기존 UE4 방식
UPROPERTY(EditAnywhere)
USceneComponent* RootComponent;
// 권장되는 UE5 방식
UPROPERTY(EditAnywhere)
TObjectPtr<USceneComponent> RootComponent;
반복문 주의 — auto* 대신 auto&
1
2
3
4
5
6
7
8
UPROPERTY(EditAnywhere, Category = "Test")
TArray<TObjectPtr<USceneComponent>> Components;
// ❌ 기존 (비효율적)
for (auto* Component : Components) { ... }
// ✅ 변경 권장 (캐싱 효율성)
for (auto& Component : Components) { ... }
왜 auto&인가? 포인터는 주소를 복사해 오는 것이고 레퍼런스는 원본을 그대로 가져다 쓴다. auto*로 꺼내면 TObjectPtr이 매번 “로드됐나 확인 → 주소 계산 → 반환”이라는 내부 로직을 실행하지만, auto&로 꺼내면 배열 안의 TObjectPtr 자체를 가리키므로 그 내부 로직이 한 번 더 돌지 않는다.
TSubclassOf
클래스를 담는 바구니. 원래 UClass*로 쓰이던 자리를 대체한다. TObjectPtr이 레벨에 배치된 인스턴스를 담는 용도라면, TSubclassOf는 컨텐츠 브라우저에 있는 Class를 담는 용도다(SpawnActor 등).
장점 — 필터링
1
2
3
4
5
6
7
// ❌ 필터링 안 됨 — 에디터에서 모든 클래스 노출, 잘못된 클래스 고를 위험
UPROPERTY(EditAnywhere, Category = "Test")
UClass* MyClass;
// ✅ 필터링 됨 — UActorComponent 파생 클래스만 선택 가능
UPROPERTY(EditAnywhere, Category = "Test")
TSubclassOf<UActorComponent> MyClass2;
성능 / 안전성
빌드 타임에는 말도 안 되는 클래스를 넣으면 빌드 자체가 실패하고, 런타임에는 잘못된 클래스가 들어오면 nullptr 처리된다. 코드에서 직접 클래스를 꽂을 때는 AActor::StaticClass() 형태로 쓴다.
컨테이너 비교 — TArray / TSet / TMap
| 구분 | TArray (Vector 기반) | TSet / TMap (Hash 기반) |
|---|---|---|
| 접근 (Access) | O(1) — 인덱스로 즉시 이동 (최강) | O(1) — 해시 계산 후 즉시 이동 |
| 탐색 (Search) | O(n) — 처음부터 다 뒤짐 | O(1) — 키/값으로 바로 점프 (최강) |
| 삽입 (Insert) | O(n) — 뒤에 붙이기는 빠름, 중간은 밀어야 함 | O(1) — 빈 칸 찾아 바로 넣음 |
| 삭제 (Remove) | O(n) — 지우고 빈틈 메워야 함 | O(1) — 해당 칸만 딱 지움 |
| 특징 | 순회·접근 빠름 | 메모리에 빈틈 있음 / 중복 방지 |
핵심 개념
TArray는 연속적인 메모리에 따닥따닥 붙어 있어서 인덱스 접근이 빠르다. 대신 중간 삭제/삽입 시 뒤에 있는 메모리를 밀거나 당겨야 한다.
TSet은 연속적이지 않다. 중간을 삭제하면 구멍이 뚫린 채 유지되고, 새 값을 추가하면 그 구멍에 들어간다. 그래서 순서가 보장되지 않고 인덱스 접근도 불가능하다.
해시 테이블 원리는 사물함 비유가 와닿았다. 사물함 [0][1][2][3][4][5]가 있을 때 그냥 찾으면 전부 열어봐야 하지만, 해시는 Key를 가지고 특정 사물함 번호로 바로 이동한다. 중간에 보안 정책(해시 함수)을 거쳐 Key로만 해당 값을 찾을 수 있게 꼬아서 저장하는 구조다.
TArray
T는 템플릿 약자 — 어떤 유형이 들어가도 호환된다.
주요 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TArray<int32> IntArray;
IntArray.Init(10, 5); // 10을 5개 채움
IntArray.Add(5); // 임시 공간에 복사해 삽입
IntArray.Emplace(6); // 복사 없이 내부에서 바로 생성
IntArray.AddUnique(6); // 중복 없을 때만 추가 (내부 순회 → 느림)
IntArray.Insert(500, 3); // 인덱스 3에 500 삽입
IntArray.Num(); // 요소 개수
IntArray.Find(값); // 인덱스 반환 (없으면 INDEX_NONE)
IntArray.Contains(값); // true/false
IntArray[5]; // 인덱스 접근
IntArray.Remove(10); // 값이 10인 요소 전부 삭제
IntArray.RemoveSingle(10); // 첫 번째 10만 삭제
IntArray.RemoveAt(Index); // 인덱스 기준 삭제
IntArray.IsValidIndex(Idx); // 유효 인덱스 확인
IntArray.RemoveAll([](int32 v){ return v == 5; }); // 조건부 삭제
IntArray.Empty(); // 전부 삭제
| 비교 | Add | Emplace |
|---|---|---|
| 언제 | 일반적인 경우 | 성능을 쥐어짜야 할 때 |
| 특징 | 복사 → 명확함 | 내부 생성 → 암시적 변환 개입 여지 |
AddUnique는 내부에서 배열을 순회하며 중복을 확인하므로 성능이 나쁨. 이럴 땐 TSet을 써라.
정렬 — 람다
1
2
IntArray.Sort(); // 오름차순
IntArray.Sort([](int32 a, int32 b){ return a > b; }); // 내림차순
캡처 문법: [&] 참조 / [=] 복사 / [] 캡처 안 함
필터링
1
2
TArray<int32> Filtered =
IntArray.FilterByPredicate([](const int32 v){ return v < 9; });
TSet
주요 메서드
1
2
3
4
5
6
7
8
9
10
TSet<int32> Numbers;
Numbers.Add(100); // 추가 (중복 무시)
Numbers.Contains(20); // true/false
int32* FoundPtr = Numbers.Find(20); // 인덱스 없음 → 포인터 반환
Numbers.Remove(100); // 삭제
Numbers.Compact(); // 구멍을 뒤로 몰아 빈 공간 모음
Numbers.Shrink(); // 빈 공간 OS에 반납
순회 — Iterator
메모리가 [10][구멍][30][40]처럼 비어 있을 수 있어서 Iterator로 순회한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 읽기 전용
for (TSet<int32>::TConstIterator It = Numbers.CreateConstIterator(); It; ++It)
{
int32 Value = *It;
}
// 순회 중 삭제
for (TSet<int32>::TIterator It = Numbers.CreateIterator(); It; ++It)
{
if (*It < 60)
{
It.RemoveCurrent(); // 안전하게 제거
}
}
타입이 길면
auto를 써도 됨. 단 확실한 경우 외엔 지양 (언리얼 공식 권장 사항).
집합 연산
1
2
3
4
5
TSet<int32> A = { 1, 2, 3 };
TSet<int32> B = { 3, 4, 5 };
A.Intersect(B); // 교집합: {3}
A.Union(B); // 합집합: {1,2,3,4,5}
TMap
Key-Value 쌍. 사물함 구조라 Key만 있으면 내용물을 매우 빠르게 찾는다.
주요 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
TMap<int32, FString> ItemMap;
ItemMap.Add(101, TEXT("Sword")); // 추가 (중복 키는 덮어쓰기)
ItemMap.Emplace(103, TEXT("Potion")); // 복사 없이 생성
ItemMap.Contains(101); // true/false
ItemMap.Find(101); // FString* 반환 (없으면 nullptr)
ItemMap.FindOrAdd(104); // 없으면 만들어서라도 반환 (FString&)
ItemMap[105]; // ⚠️ 키 없으면 크래시 → Contains 체크 필수
ItemMap.Remove(102); // 키 기준 삭제
ItemMap.Compact(); // 구멍 뒤로 몰기
ItemMap.Shrink(); // 빈 공간 반납
순회 — TPair vs Iterator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 일반 순회 (읽기)
for (const TPair<int32, FString>& Pair : ItemMap)
{
int32 Key = Pair.Key;
FString Value = Pair.Value;
}
// Iterator 순회 (삭제 가능)
for (TMap<int32, FString>::TIterator It = ItemMap.CreateIterator(); It; ++It)
{
if (It.Key() == 103)
{
It.RemoveCurrent();
}
}
Iterator를 쓰는 이유? → 삭제 시 안전성. 그냥 순회하면서 지우면 길을 잃을 수 있음. Iterator는 가이드라인이 있는 안전한 경로.
핵심 요약 —
TObjectPtr은 성능 도구가 아니라 에디터용 관리 도구(지연 로딩·액세스 트래킹)이고, 패키징하면 원시 포인터로 바뀌어 기능도 함께 사라진다. 컨테이너는 탐색이 잦으면TSet/TMap, 순회·인덱스 접근 위주면TArray로 고른다.