개요
이번 강의에서는 Unreal Engine이 자체 제작한 컨테이너 라이브러리(UCL - Unreal Container Library)의 대표적인 자료구조인 TArray와 TSet의 내부 구조와 활용 방법을 학습합니다. 각 컨테이너의 장단점을 파악하여 상황에 맞게 효과적으로 활용하는 방법을 익힙니다.
학습 목표
- Unreal 컨테이너 라이브러리 TArrayy와 TSet의 내부 구조 이해
- 두 컨테이너의 장단점 파악 및 적절한 활용 방법 학습
- 디버그 빌드를 통한 메모리 구조 직접 확인
- 알고리즘 라이브러리 활용 방법 습득
Unreal 컨테이너 라이브러리 vs C++ STL


TArray - 동적 배열
개념 및 특징
: TArray는 크기를 동적으로 조절할 수 있는 가변 배열 자료구조입니다.
장점
- 메모리 연속 배치로 캐시 효율이 좋음
- 인덱스를 통한 빠른 접근이 가능 O(1)
- 고속 순회 가능
- 끝에 요소 추가 빠름 O(1)
단점
- 중간 삽입/삭제 비용 큼 O(n)
- 검색 느림 O(n)
- 데이터 많을수록 검색/수정 작업 지연
사용 시기
- 순차적 데이터 저장
- 인덱스 기반 빠른 접근 필요
- 순회가 빈번한 경우
- 검색/삭제가 적은 경우
내부 구조
TArray<int32> 구조
메모리: [1][2][3][4][5][6][7][8][9][10]
↑
GetData() - 시작 포인터
특징:
- 빈틈 없는 연속 메모리
- 인덱스 기반 O(1) 접근
- 끝에 추가: O(1)
- 중간 삽입/삭제: O(n) - 전체 재배치
선언 및 기본 사용
// 선언
TArray<int32> IntArray; // 비어있는 배열
TArray<FString> StringArray; // 문자열 배열
TArray<UObject*> ObjectArray; // UObject 포인터 배열
// 초기화
TArray<int32> InitializedArray = {1, 2, 3, 4, 5};
// 메모리 미리 할당
TArray<int32> ReservedArray;
ReservedArray.Reserve(100); // 100개 공간 미리 확보
// 기본값으로 채우기
TArray<int32> FilledArray;
FilledArray.Init(0, 10); // 0으로 10개 채우기
요소 추가
// Add - 외부에서 생성 후 복사
TArray<FString> Strings;
FString NewString = TEXT("Hello");
Strings.Add(NewString); // 복사 발생
// Emplace - 배열 내부에서 직접 생성
Strings.Emplace(TEXT("World")); // 복사 없음, 더 효율적
// Append - 여러 요소 한번에 추가
TArray<int32> Source = {1, 2, 3};
TArray<int32> Target = {4, 5, 6};
Target.Append(Source); // Target: {4, 5, 6, 1, 2, 3}
요소 접근
TArray<int32> Numbers = {10, 20, 30, 40, 50};
// 인덱스 연산자
int32 First = Numbers[0]; // 10
Numbers[0] = 100; // 수정 가능
// Top (마지막 요소)
int32 Last = Numbers.Top(); // 50
// Last (마지막 요소, Top과 동일)
int32 AlsoLast = Numbers.Last(); // 50
// 범위 체크
if (Numbers.IsValidIndex(10)) // false
{
// 안전하게 접근
}
// 포인터 접근 (C 스타일)
int32* Data = Numbers.GetData();
for (int32 i = 0; i < Numbers.Num(); ++i)
{
UE_LOG(LogTemp, Log, TEXT("%d"), Data[i]);
}
순회
TArray<FString> Names = {TEXT("Alice"), TEXT("Bob"), TEXT("Charlie")};
// Range-based for loop (권장)
for (const FString& Name : Names)
{
UE_LOG(LogTemp, Log, TEXT("Name: %s"), *Name);
}
// 수정 가능한 순회
for (FString& Name : Names)
{
Name += TEXT("_Modified");
}
// 인덱스 기반 순회
for (int32 i = 0; i < Names.Num(); ++i)
{
UE_LOG(LogTemp, Log, TEXT("%d: %s"), i, *Names[i]);
}
// 반복자 사용
for (auto It = Names.CreateConstIterator(); It; ++It)
{
UE_LOG(LogTemp, Log, TEXT("Iterator: %s"), **It);
}
검색
TArray<int32> Numbers = {10, 20, 30, 40, 50};
// Contains - 요소 존재 확인
bool bHas30 = Numbers.Contains(30); // true
// Find - 인덱스 찾기
int32 Index = Numbers.Find(30); // 2 (인덱스)
if (Index != INDEX_NONE)
{
UE_LOG(LogTemp, Log, TEXT("Found at index: %d"), Index);
}
// FindByPredicate - 조건으로 검색
auto* Found = Numbers.FindByPredicate([](int32 Val)
{
return Val > 35;
});
if (Found)
{
UE_LOG(LogTemp, Log, TEXT("Found: %d"), *Found); // 40
}
삽입 및 삭제
TArray<int32> Numbers = {1, 2, 3, 4, 5};
// Insert - 중간 삽입 (비용 큼)
Numbers.Insert(99, 2); // 인덱스 2에 99 삽입
// 결과: {1, 2, 99, 3, 4, 5}
// Remove - 값으로 제거 (첫 번째만)
Numbers.Remove(99);
// 결과: {1, 2, 3, 4, 5}
// RemoveAt - 인덱스로 제거
Numbers.RemoveAt(0); // 첫 번째 제거
// 결과: {2, 3, 4, 5}
// RemoveAll - 조건으로 모두 제거
Numbers.RemoveAll([](int32 Val)
{
return Val % 2 == 0; // 짝수 모두 제거
});
// 결과: {3, 5}
// Pop - 마지막 제거
int32 Popped = Numbers.Pop(); // 5 반환, 배열에서 제거
// Empty - 모두 제거
Numbers.Empty();
정렬
TArray<int32> Numbers = {5, 2, 8, 1, 9};
// 기본 정렬 (오름차순)
Numbers.Sort(); // {1, 2, 5, 8, 9}
// 내림차순 정렬
Numbers.Sort([](int32 A, int32 B)
{
return A > B;
});
// 구조체 정렬
struct FStudent
{
FString Name;
int32 Score;
};
TArray<FStudent> Students;
// ... 데이터 추가 ...
Students.Sort([](const FStudent& A, const FStudent& B)
{
return A.Score > B.Score; // 점수 높은 순
});
유틸리티 함수
TArray<int32> Numbers = {1, 2, 3, 4, 5};
// Num - 요소 개수
int32 Count = Numbers.Num(); // 5
// IsEmpty - 비어있는지
bool bEmpty = Numbers.IsEmpty(); // false
// SetNum - 크기 조정
Numbers.SetNum(3); // {1, 2, 3}
Numbers.SetNum(5, 0); // {1, 2, 3, 0, 0}
// AddUnique - 중복 방지 추가 (검색 발생, 비효율적)
Numbers.AddUnique(2); // 이미 있으므로 추가 안 됨
Numbers.AddUnique(6); // 없으므로 추가됨
// Reset - 메모리 유지하고 비우기
Numbers.Reset();
// Empty - 메모리까지 해제
Numbers.Empty();
// Shrink - 여유 메모리 제거
Numbers.Shrink();
TSet - 집합
개념 및 특징
: TSet은 중복 없는 데이터 집합을 저장하는 자료구조입니다.
장점
- 빠른 검색 O(1)
- 빠른 삽입 O(1)
- 빠른 삭제 O(1)
- 중복 자동 제거
- 빠른 순회 가능 (동적 배열 기반)
단점
- 순서 보장 안 됨
- 인덱스 접근 비효율적
- 메모리에 빈틈 발생 가능
사용 시기
- 중복 제거 필요
- 빠른 검색 필요
- 데이터 존재 여부만 확인
- 순서가 중요하지 않을 때
내부 구조
TSet<int32> 구조 (해시 테이블 + 동적 배열)
초기: [1][2][3][4][5][6][7][8][9][10]
Remove(2,4,6,8,10):
[1][X][3][X][5][X][7][X][9][X]
↑ Invalid (빈틈)
Add(2,4,6,8,10):
[1][10][3][8][5][6][7][4][9][2]
↑ 빈틈을 역순으로 채움
특징:
- 해시 테이블로 O(1) 검색
- 동적 배열 기반으로 빠른 순회
- 삭제 시 빈틈 발생 (Invalid 상태)
- 추가 시 빈틈 우선 채움 (역순)
- 재구축 없음 (삭제 후)
STL set vs Unreal TSet

선언 및 기본 사용
// 선언
TSet<int32> IntSet;
TSet<FString> StringSet;
TSet<UObject*> ObjectSet;
// 초기화 리스트
TSet<int32> InitializedSet = {1, 2, 3, 4, 5};
요소 추가
TSet<int32> Numbers;
// Add - 추가 (중복 무시)
Numbers.Add(10); // 추가됨
Numbers.Add(20); // 추가됨
Numbers.Add(10); // 이미 있으므로 무시됨
// Emplace - 직접 생성
Numbers.Emplace(30);
// Append - 다른 TSet 병합
TSet<int32> OtherSet = {40, 50};
Numbers.Append(OtherSet); // 집합 합집합
검색
TSet<int32> Numbers = {10, 20, 30, 40, 50};
// Contains - 존재 확인 (매우 빠름, O(1))
bool bHas30 = Numbers.Contains(30); // true
// Find - 포인터 반환
int32* Found = Numbers.Find(30);
if (Found)
{
UE_LOG(LogTemp, Log, TEXT("Found: %d"), *Found);
}
else
{
UE_LOG(LogTemp, Log, TEXT("Not found"));
}
// ⚠️ 잘못된 패턴 (비효율적, 2번 검색)
if (Numbers.Contains(30)) // 1번 검색
{
int32 Value = *Numbers.Find(30); // 2번 검색
}
// ✅ 올바른 패턴 (1번 검색)
if (int32* Value = Numbers.Find(30))
{
UE_LOG(LogTemp, Log, TEXT("Value: %d"), *Value);
}
순회
TSet<FString> Names = {TEXT("Alice"), TEXT("Bob"), TEXT("Charlie")};
// Range-based for loop
for (const FString& Name : Names)
{
UE_LOG(LogTemp, Log, TEXT("Name: %s"), *Name);
}
// ⚠️ 순서는 보장되지 않음!
// TArray로 변환 후 순회
TArray<FString> NameArray = Names.Array();
for (int32 i = 0; i < NameArray.Num(); ++i)
{
UE_LOG(LogTemp, Log, TEXT("%d: %s"), i, *NameArray[i]);
}
삭제
TSet<int32> Numbers = {1, 2, 3, 4, 5};
// Remove - 요소 제거
Numbers.Remove(3); // O(1)
// 여러 개 제거
Numbers.Remove(1);
Numbers.Remove(2);
// Empty - 모두 제거
Numbers.Empty();
// Reset - 메모리 유지하고 비우기
Numbers.Reset();
중요: 삭제 후 메모리에 빈틈(Invalid) 남음, 추가 시 자동으로 채워짐
집합 연산
TSet<int32> SetA = {1, 2, 3, 4, 5};
TSet<int32> SetB = {4, 5, 6, 7, 8};
// 합집합 (Union)
TSet<int32> Union = SetA.Union(SetB);
// {1, 2, 3, 4, 5, 6, 7, 8}
// 교집합 (Intersection)
TSet<int32> Intersect = SetA.Intersect(SetB);
// {4, 5}
// 차집합 (Difference)
TSet<int32> Difference = SetA.Difference(SetB);
// {1, 2, 3}
유틸리티 함수
TSet<int32> Numbers = {1, 2, 3, 4, 5};
// Num - 요소 개수
int32 Count = Numbers.Num(); // 5
// IsEmpty - 비어있는지
bool bEmpty = Numbers.IsEmpty(); // false
// Shrink - 빈틈 제거
Numbers.Shrink();
Slack (여유 메모리)
: 요소 추가마다 메모리를 재할당하면 오버헤드가 발생하기 때문에 언리얼 컨테이터 라이브러리에는 Slack이라는 개념이 구현되어 있다. 요소를 추가할 때 요소에 해당하는 메모리만큼 확보하는 것이 아닌 해당 메모리의 여유분을 미리 확보해둔다고 한다. 미리 넉넉하게 할당하여 성능을 향상하는 효과가 있다. 삭제 시에는 메모리를 즉시 해제하지 않는 방법으로 메모리의 여유를 둔다.
TArray<int32> Numbers;
Numbers.Reserve(100); // 100개 공간 확보
Numbers.Add(1); // 1개만 사용
// Num(): 1 (실제 요소)
// Max(): 100 (할당된 용량)
// Slack: 99 (여유 공간)
// 여유 공간 제거
Numbers.Shrink(); // Max를 Num에 맞춤
예제
MyGameInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"
UCLASS()
class UNREALCONTAINER_API UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
virtual void Init() override;
};
MyGameInstance.cpp - TArray
#include "MyGameInstance.h"
#include "Algo/Accumulate.h"
void UMyGameInstance::Init()
{
Super::Init();
UE_LOG(LogTemp, Log, TEXT("===== TArray 예제 시작 ====="));
// 1. TArray 생성 및 채우기
TArray<int32> IntArray;
for (int32 i = 1; i <= 10; ++i)
{
IntArray.Add(i);
}
// IntArray: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 2. 짝수 제거
IntArray.RemoveAll([](int32 Val)
{
return Val % 2 == 0;
});
// IntArray: {1, 3, 5, 7, 9}
// 3. 짝수 다시 추가
IntArray += {2, 4, 6, 8, 10};
// IntArray: {1, 3, 5, 7, 9, 2, 4, 6, 8, 10}
// 4. 메모리 직접 복사로 비교 배열 생성
TArray<int32> CompareArray;
CompareArray.AddUninitialized(IntArray.Num());
FMemory::Memcpy(
CompareArray.GetData(),
IntArray.GetData(),
IntArray.Num() * sizeof(int32)
);
// 5. 동일성 확인
ensure(IntArray == CompareArray);
// 6. 알고리즘 라이브러리 사용
int32 Sum = Algo::Accumulate(IntArray, 0); // 55
int32 ManualSum = 0;
for (int32 Num : IntArray)
{
ManualSum += Num;
}
ensure(Sum == ManualSum);
UE_LOG(LogTemp, Log, TEXT("배열 합계: %d"), Sum);
UE_LOG(LogTemp, Log, TEXT("===== TArray 예제 종료 ====="));
}
MyGameInstance.cpp - TSet
void UMyGameInstance::Init()
{
Super::Init();
UE_LOG(LogTemp, Log, TEXT("===== TSet 예제 시작 ====="));
// 1. TSet 생성 및 채우기
TSet<int32> IntSet;
for (int32 i = 1; i <= 10; ++i)
{
IntSet.Add(i);
}
// 2. 짝수 제거 (하나씩)
IntSet.Remove(2);
IntSet.Remove(4);
IntSet.Remove(6);
IntSet.Remove(8);
IntSet.Remove(10);
// IntSet: {1, 3, 5, 7, 9} (내부적으로 빈틈 존재)
// 3. 짝수 다시 추가 (빈틈을 역순으로 채움)
IntSet.Add(2);
IntSet.Add(4);
IntSet.Add(6);
IntSet.Add(8);
IntSet.Add(10);
// IntSet: {1, 10, 3, 8, 5, 6, 7, 4, 9, 2} (순서 보장 안 됨)
// 4. 검색 성능 테스트
bool bFound = IntSet.Contains(5); // 매우 빠름 (O(1))
UE_LOG(LogTemp, Log, TEXT("5 존재: %s"), bFound ? TEXT("예") : TEXT("아니오"));
// 5. TArray로 변환
TArray<int32> ConvertedArray = IntSet.Array();
UE_LOG(LogTemp, Log, TEXT("변환된 배열 크기: %d"), ConvertedArray.Num());
UE_LOG(LogTemp, Log, TEXT("===== TSet 예제 종료 ====="));
}
'기타 > [강의] 이득우의 언리얼 프로그래밍 Part1' 카테고리의 다른 글
| [이득우의 언리얼] 12. 언리얼 엔진의 메모리 관리 (0) | 2026.02.09 |
|---|---|
| [이득우의 언리얼] 11. 언리얼 컨테이너 라이브러리 - 구조체, Map (0) | 2026.02.08 |
| [이득우의 언리얼] 9. 언리얼 C++ 설계 - 델리게이트 (0) | 2026.02.06 |
| [이득우의 언리얼] 8. 언리얼의 C++ 설계 - 컴포지션 (0) | 2026.02.06 |
| [이득우의 언리얼] 7. 언리얼 C++ 설계 - 인터페이스 (0) | 2026.02.04 |