TWeakObjectPtr<>은 GC Referencing은 하지 않으면서 Object가 유효한지 확인하고, 유효하면 값을 가져올 수 있는 기능을 제공합니다. 이름 그대로 약한 참조이며 C#의 WeakReference와 동일합니다. 즉, GC를 막지는 않으면서 값은 안전하게 확인 후 가져올 수 있는 기능을 제공합니다.
이번 포스팅에서는
등에 대해서 살펴보겠습니다. 들어가기 전에에 항상 언급하듯이 UE5.4.4 기준이며, 다른 버전에서는 궁금하실 때 따로 확인해 보시기 바랍니다. 하지만 크게 다르지는 않을 것입니다.
당연히 TWeakPtr<...>와는 아무 관련이 없습니다.
FWeakObjectPtr<...>을 위해서 GUObjectArray: FUObjectArray가 온 힘을 다해서 도와주는 형상입니다.

FUObjectArray는 global 변수 GUObjectArray의 타입으로, 엔진에 존재하는 모든 UObject를 Cluster Chunk로 나뉘어져 있는 Array 형태로 관리합니다. UObjectBase에 있는 int32 InternalIndex가 그 Array에서의 Index를 나타냅니다.
IndexToObject를 통해서 InternalIndex로부터 UObjectBase를 가져올 수 있는데, 중간에 FUObjectItem이 Wrapper로 한번 감싸서 가지고 있습니다. 이 중 눈여겨볼 부분은 FUObjectItem::SerialNumber입니다. 이 SerialNumber가 FWeakObjectPtr가 UObject로 참조를 걸 때 FUObjectArray::AllocateSerialNumber(...)로 받아오는 값이며, FUObjectArray는 SerialNumber를 FUObjectItem에 함께 저장하게 됩니다.
UMyObject* NewObject = NewObject<UMyObject>(this);
TWeakObjectPtr<UMyObject> WeakReferencedObject = NewObject;
결국 위와 같이 하고 나면 WeakReferencedObject에 저장되는 값 딱 2개는
ObjectIndex - NewObject의 InternalIndexObjectSerialNumber - NewObject를 감싸고 있는 FUObjectItem에 할당된 SerialNumber입니다.
FORCEINLINE_DEBUGGABLE bool FWeakObjectPtr::SerialNumbersMatch(FUObjectItem* ObjectItem) const
{
const int32 ActualSerialNumber = ObjectItem->GetSerialNumber();
ObjectSerialNumber); // serial numbers should never shrink
return ActualSerialNumber == ObjectSerialNumber;
}
WeakObjectPtr은 Get(...)이나 IsValid(...)에서 유효성 검사를 할 때, 자신이 가지고 있는 ObjectIndex에 해당하는 FUObjectItem이 저장하고 있는 SerialNumber와 자신이 참조를 걸 때 가져두었던 ObjectSerialNumber가 일치하는지만 확인하면 됩니다. 이것으로 끝입니다.
여기서 중요한 점은 SerialNumber가 언제 Counting되고 범위는 어디까지냐는 것입니다. 간략하게나마 살펴보겠습니다.
우선 Weak로 참조가 걸릴 때 작동되는 Code입니다.
void FWeakObjectPtr::operator=(const class UObject *Object)
{
if (Object // && UObjectInitialized() we might need this at some point, but it is a speed hit we would prefer to avoid
)
{ ObjectIndex = GUObjectArray.ObjectToIndex((UObjectBase*)Object);
ObjectSerialNumber = GUObjectArray.AllocateSerialNumber(ObjectIndex);
checkSlow(SerialNumbersMatch());
}
else
{
Reset();
}
}
코드를 보면 WeakObj로 참조가 걸릴 때만 GUObjectArray.AllocateSerialNumber가 호출되는 것을 알 수 있습니다.
AllocateSerialNumber는 아래와 같이 되어 있습니다.
int32 FUObjectArray::AllocateSerialNumber(int32 Index)
{
FUObjectItem* ObjectItem = IndexToObject(Index);
checkSlow(ObjectItem);
volatile int32 *SerialNumberPtr = &ObjectItem->SerialNumber;
int32 SerialNumber = *SerialNumberPtr;
if (!SerialNumber)
{ // Open around PrimarySerialNumber as if we fail/abort a transaction we dont need to undo this, simply allow it to grow for the next use
UE_AUTORTFM_OPEN({
SerialNumber = PrimarySerialNumber.Increment();
UE_CLOG(SerialNumber <= START_SERIAL_NUMBER, LogUObjectArray, Fatal, TEXT("UObject serial numbers overflowed (trying to allocate serial number %d)."), SerialNumber);
int32 ValueWas = FPlatformAtomics::InterlockedCompareExchange((int32*)SerialNumberPtr, SerialNumber, 0);
if (ValueWas != 0)
{ // someone else go it first, use their value
SerialNumber = ValueWas;
}
});
}
checkSlow(SerialNumber > START_SERIAL_NUMBER);
return SerialNumber;
}
간략히 살펴보면,
ObjectItem의 SerialNumber가 0으로 초기화되는 곳은, bool UObject::ConditionalFinishDestroy()에서 호출하는 GUObjectArray.ResetSerialNumber(this);입니다. 따라서 GC(flag도 확인하지만)되고 나면 WeakObj는 Invalid로 처리될 수 있습니다. TWeakObjectPtr에 있는 ObjectIndex가 가리키는 FUObjectItem의 SerialNumber가 일치하지 않기 때문입니다.
위에서 짚고 넘어갈 중요한 점 세 가지를 살펴보겠습니다.
즉, 아래 코드가 약 21억 회 넘게 작동하면 UObject가 중간에 GC되어서 몇 개가 존재하든지 관계없이 오류 처리됩니다.
UMyObject* NewObject = NewObject<UMyObject>(this);
TWeakObjectPtr<UMyObject> WeakReferencedObject = NewObject;
충분한 값이라면 값이지만, 그래도 발생 가능성이 있다는 것은 알고 넘어가시기 바랍니다.
Garbage Collection 포스팅에서 AActor의 경우 World(정확히는 Level)에서 참조하고 있으므로 AActor는 따로 UPROPERTY(...)가 아니어도 GC되지 않음을 살펴보았습니다. 그렇다면 UObject 계열이고 Blueprint에서 접근할 필요가 없다고 가정했을 때, Destroy가 호출되고 GC가 되었음을 확인만 하면 되는 상황에서
UPROPERTY(...) TObjectPtr<AMyActor> MyActor;TWeakObjectPtr<AMyActor> WeakMyActor;AMyActor*를 일반 멤버 변수로 두고, static void AddReferencedObjects(....)에서 처리위 셋 중 어느 것이 가장 가볍고 빠를까요? 하나씩 따져 보겠습니다.
따라서, Blueprint에서 접근할 필요가 없다면 '3'번이 가장 가볍다고 생각됩니다. 3번의 경우 값을 참조할 때 nullptr인지만 확인하면 충분합니다. static void AddReferencedObjects로의 Pointing은 UCLASS당 하나만 저장된다는 점도 기억해 두시기 바랍니다.
그렇다면 TWeakObjectPtr<...>은 언제 사용하는 것이 좋을까요? 당연히 UObject* 참조 변수를 UObject 계열이든 아니든 GC를 막지는 않고 Valid한지 확인해 가면서 사용할 때는 다른 대안이 없습니다. 또한 AGameState, AGameMode처럼 Application 생명 주기에서 새로 만들어지는 횟수가 적은 것을 대상으로 할 때는 더욱 적합할 것입니다.