언리얼 엔진 C++ 코드를 보다 보면 UObject 계열 객체를 raw pointer로 참조하는 코드를 볼 수 있다.
하지만 UE5에서는 UObject* 같은 raw pointer 대신 TObjectPtr 사용을 권장하는 경우가 많다.
처음에는 UPROPERTY만 붙이면 GC 추적 대상이 되는데, 왜 굳이 TObjectPtr을 사용하는지 궁금했다.
그래서 관련 내용을 찾아보니 TObjectPtr 외에도TWeakObjectPtr, TSoftObjectPtr, TStrongObjectPtr 같은 포인터들이 있었다.
또 일반 C++에서 사용하는 shared_ptr, unique_ptr과 비슷해 보이는TSharedPtr, TUniquePtr 같은 타입도 따로 존재했다.
그래서 이번 글에서는 언리얼 엔진에서 사용하는 포인터들을
일반 C++ 객체용 스마트 포인터와 UObject 계열 객체용 포인터로 나누어 정리해보려고 한다.
일반 C++ 객체용 스마트 포인터
언리얼에는 일반 C++ 객체를 관리하기 위한 스마트 포인터들이 있다.
대표적으로 다음과 같은 타입이 있다.
TSharedPtrTSharedRefTWeakPtrTUniquePtr
이 포인터들은 언리얼의 스마트 포인터 라이브러리에 포함되어 있으며,
일반 C++ 객체의 소유권과 수명을 관리하기 위해 사용한다.
다만 중요한 점은 이 포인터들은 UObject 계열 객체를 관리하기 위한 포인터가 아니라는 것이다.UObject는 언리얼의 GC 시스템으로 관리되기 때문에 일반 C++ 스마트 포인터와는 별도로 생각해야 한다.
TSharedPtr
TSharedPtr은 C++의 std::shared_ptr과 비슷한 공유 소유 스마트 포인터이다.
여러 TSharedPtr이 같은 객체를 함께 참조할 수 있고, 참조 카운트가 0이 되면 객체가 삭제된다.
TSharedPtr<FMyData> DataPtr = MakeShared<FMyData>();
TSharedPtr은 null을 가질 수 있다.
따라서 사용하기 전에 유효한지 확인하는 습관이 필요하다.
if (DataPtr.IsValid())
{
DataPtr->DoSomething();
}
정리하면 TSharedPtr은 여러 곳에서 같은 일반 C++ 객체를 공유해서 사용할 때 적합하다.
TSharedRef
TSharedRef는 TSharedPtr과 비슷하지만 null을 허용하지 않는다.
즉, 항상 유효한 객체를 참조해야 한다.
TSharedRef<FMyData> DataRef = MakeShared<FMyData>();
TSharedRef는 null이 될 수 없기 때문에
반드시 유효한 객체가 필요하다는 의도를 코드에서 드러낼 수 있다.
예를 들어 Slate UI 코드에서 TSharedRef를 자주 볼 수 있다.
간단히 정리하면 다음과 같다.
- null 가능성이 있으면
TSharedPtr - 항상 유효해야 하면
TSharedRef
TWeakPtr
TWeakPtr은 객체를 소유하지 않고 관찰만 하는 포인터이다.TSharedPtr처럼 참조 카운트를 증가시키지 않기 때문에 객체의 수명을 연장하지 않는다.
이 특징은 순환 참조를 끊을 때 유용하다.
TWeakPtr<FMyData> WeakData = DataPtr;
TWeakPtr을 사용할 때는 바로 객체에 접근하지 않고 Pin()을 이용해 임시 TSharedPtr을 얻어 사용한다.
if (TSharedPtr<FMyData> PinnedData = WeakData.Pin())
{
PinnedData->DoSomething();
}
Pin()으로 얻은 TSharedPtr이 유효하다면, 그 범위 안에서는 객체에 안전하게 접근할 수 있다.
즉, TWeakPtr은 객체를 소유하지 않고 관찰해야 할 때 사용한다.
TUniquePtr
TUniquePtr은 하나의 포인터만 객체를 소유할 수 있는 스마트 포인터이다.
C++의 std::unique_ptr과 비슷하다.
TUniquePtr<FMyData> Data = MakeUnique<FMyData>();
TUniquePtr은 복사할 수 없다.
하지만 소유권을 다른 TUniquePtr로 이동하는 것은 가능하다.
TUniquePtr<FMyData> DataA = MakeUnique<FMyData>();
TUniquePtr<FMyData> DataB = MoveTemp(DataA);
이후 DataA는 더 이상 객체를 소유하지 않고 DataB가 객체의 소유권을 가진다.
그리고 TUniquePtr은 스코프를 벗어나면 소유한 객체를 자동으로 삭제한다.
따라서 단독 소유권이 명확한 일반 C++ 객체를 관리할 때 사용하기 좋다.
UObject 계열 객체용 포인터
언리얼의 UObject 계열 객체는 일반 C++ 객체와 다르게 언리얼의 GC 시스템에 의해 관리된다.
따라서 UObject를 TSharedPtr이나 TUniquePtr으로 소유하려고 하면 안 되고,
언리얼에서 제공하는 오브젝트 포인터 타입을 사용해야 한다.
대표적으로 다음과 같은 타입이 있다.
TObjectPtrTWeakObjectPtrTSoftObjectPtrTStrongObjectPtr
TObjectPtr
TObjectPtr은 UE5에서 UObject 포인터를 표현할 때 자주 사용하는 타입이다.
기존의 raw pointer를 대체하는 용도로 사용할 수 있으며 UObject 계열 타입에만 사용할 수 있다.
UPROPERTY()
TObjectPtr<UStaticMeshComponent> Mesh;
TObjectPtr은 UPROPERTY와 함께 사용했을 때 언리얼 GC가 참조를 추적할 수 있는 강한 참조가 된다.
즉, UPROPERTY로 등록된 TObjectPtr이 어떤 UObject를 참조하고 있다면
그 참조는 GC가 객체를 수집할지 판단할 때 고려된다.
다만 TObjectPtr을 사용했다고 해서 무조건 GC에 의해 추적되는 것은 아니다.
일반적으로 멤버 변수로 UObject를 오래 참조해야 한다면 UPROPERTY()와 함께 사용하는 것이 중요하다.
또한 TObjectPtr은 cook-time dependency tracking이나
점진적 GC를 위한 barrier 같은 기능과도 연결되기 때문에
UE5에서는 UObject 멤버 참조에 TObjectPtr 사용이 권장된다.
TWeakObjectPtr
TWeakObjectPtr은 UObject를 약하게 참조하는 포인터이다.
대상을 소유하지 않고, GC에 의해 객체가 제거되는 것을 막지 않는다.
TWeakObjectPtr<AActor> TargetActor;
대상 객체가 Destroy되거나 GC에 의해 제거되면TWeakObjectPtr은 자동으로 유효하지 않은 상태가 된다.
따라서 사용하기 전에는 유효성 검사를 해야 한다.
if (TargetActor.IsValid())
{
TargetActor->Destroy();
}
TWeakObjectPtr은 객체를 잠시 캐싱하거나 소유하지 않는 참조를 저장해야 할 때 사용하기 좋다.
TSoftObjectPtr
TSoftObjectPtr은 에셋이나 객체를 경로 기반으로 참조하는 포인터이다.
직접 강하게 참조하지 않기 때문에 참조만으로 대상 에셋을 즉시 로드하지 않는다.
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UTexture2D> IconTexture;
TSoftObjectPtr은
대상 에셋이 아직 메모리에 로드되어 있지 않아도 경로 정보를 가지고 있을 수 있고,
필요한 시점에 동기 또는 비동기 로드로 대상 에셋을 불러올 수 있다.
따라서 항상 필요한 에셋이 아니라 필요할 때만 로드하고 싶은 에셋을 참조할 때 적합하다.
예를 들어 아이템 아이콘, VFX 에셋, 사운드 에셋처럼
상황에 따라 필요한 리소스를 나중에 로드하고 싶을 때 사용할 수 있다.
TStrongObjectPtr
TStrongObjectPtr은 UObject를 강하게 붙잡는 포인터이다.
이 포인터가 살아 있는 동안 대상 UObject가 GC에 의해 제거되지 않도록 막는다.
TStrongObjectPtr<UMyObject> StrongObject;
하지만 TStrongObjectPtr은 UPROPERTY를 지원하지 않는다.
그래서 UObject 계열 클래스 안에서 멤버 변수로 객체를 관리해야 한다면,
보통은 UPROPERTY()가 붙은 TObjectPtr을 사용하는 것이 더 적합하다.
대신 일반 C++ 클래스나 구조체처럼UPROPERTY를 사용할 수 없는 곳에서 UObject를 강하게 참조해야 할 때 사용할 수 있다.
다만 TStrongObjectPtr은 GC에 영향을 주기 때문에
불필요하게 오래 유지하면 객체가 예상보다 오래 살아남을 수 있다.
그래서 필요한 경우에만 신중하게 사용하는 것이 좋다.
GC에 영향을 주는 포인터
UObject 계열 포인터를 사용할 때 중요한 기준 중 하나는 GC에 영향을 주는지 여부이다.
간단히 정리하면 다음과 같다.
| 포인터 타입 | GC 영향 |
|---|---|
UPROPERTY()가 붙은 TObjectPtr |
영향을 줌 |
TStrongObjectPtr |
영향을 줌 |
TWeakObjectPtr |
영향을 주지 않음 |
TSoftObjectPtr |
영향을 주지 않음 |
TObjectPtr은 UPROPERTY와 함께 사용해야 GC 추적에 의미가 있다.TStrongObjectPtr은 UPROPERTY 없이도 대상 객체를 강하게 붙잡는다.
반면 TWeakObjectPtr과 TSoftObjectPtr은 대상 객체를 소유하지 않기 때문에
GC로부터 객체를 보호하지 않는다.
어떤 포인터를 사용해야 할까?
상황에 따라 대략 다음과 같이 선택할 수 있다.
| 상황 | 사용할 포인터 |
|---|---|
| 일반 C++ 객체를 여러 곳에서 공유 | TSharedPtr |
| 일반 C++ 객체를 공유하면서 항상 유효해야 함 | TSharedRef |
| 일반 C++ 객체를 관찰만 함 | TWeakPtr |
| 일반 C++ 객체를 단독 소유 | TUniquePtr |
UObject를 UCLASS 멤버로 강하게 참조 |
UPROPERTY() + TObjectPtr |
UObject를 소유하지 않고 관찰 |
TWeakObjectPtr |
| 에셋을 필요할 때 로드 | TSoftObjectPtr |
| non-UObject 클래스에서 UObject를 강하게 유지 | TStrongObjectPtr |
가장 중요한 기준은 대상이 일반 C++ 객체인지, UObject 계열 객체인지 먼저 구분하는 것이다.
일반 C++ 객체라면 TSharedPtr, TUniquePtr 같은 스마트 포인터를 사용할 수 있다.
하지만 UObject 계열 객체라면 GC 시스템과 연결된 언리얼 오브젝트 포인터를 사용해야 한다.
마무리
이번에 언리얼 엔진 스마트 포인터를 정리하면서
UE5에서 raw pointer 대신 TObjectPtr 사용을 권장하는 이유가,
단순한 문법 변화가 아니라 언리얼의 GC와 에디터, 에셋 관리 방식과도 연결되어 있다는 점을 알게 되었다.
또한 일반 C++ 객체를 다룰 때와 UObject 계열 객체를 다룰 때
사용해야 하는 포인터가 다르다는 점을 알 수 있었다.
처음에는 포인터 종류가 많아서 복잡하게 느껴졌지만
결국 핵심은 대상 객체가 무엇인지, 그리고 그 객체를 소유할 것인지,
관찰만 할 것인지, 필요할 때 로드할 것인지를 구분하는 것이었다.
앞으로 언리얼 C++에서 포인터를 사용할 때는
습관적으로 raw pointer만 사용하기보다,
해당 객체의 수명과 GC, 로드 방식까지 함께 고려해서 적절한 포인터를 선택해야겠다.
'언리얼 엔진' 카테고리의 다른 글
| [언리얼 엔진] GAS 기본 구성 요소 정리 (0) | 2026.06.17 |
|---|---|
| [언리얼 엔진] Delegate 정리 (1) | 2026.06.16 |
| [언리얼 엔진] Replication이란? Actor와 Property 복제 정리 (0) | 2026.06.12 |
| [언리얼 엔진] std::map과 TMap 차이 정리 (0) | 2026.06.11 |
| [언리얼 엔진] FString, FName, FText 차이 정리 (0) | 2026.06.10 |