나만의 작은 도서관
[TIL][C++] 250917 MMO 서버 개발 103일차: [언리얼] TObjectPtr은 C++의 스마트 포인터와 같은 역할이 아니다. 본문
Today I Learn
[TIL][C++] 250917 MMO 서버 개발 103일차: [언리얼] TObjectPtr은 C++의 스마트 포인터와 같은 역할이 아니다.
pledge24 2025. 9. 17. 22:27주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
[언리얼] TObjectPtr은 C++의 스마트 포인터와 같은 역할이 아니다.
- 헤딩하면서 언리얼에 쌓아올리다보니 TObjectPtr에 대해서 잘 모르고 사용해왔다. 대충 언리얼 방식의 스마트 포인터인가보다 하고 넘어갔었는데, 알고보니 스마트 포인터 역할을 하는 애가 아니였다.
그럼 TObjectPtr은 무엇인가?
- TObjectPtr은 UObject 타입의 생 포인터(raw pointer)를 감싼 래퍼 클래스로, 언리얼의 GC(가비지 컬렉션) 시스템과 UPROPERTY 시스템을 최적화하기 위한 용도로 사용된다.
- 즉, C++의 스마트 포인터처럼 “메모리 관리”를 하는 것이 목적이 아니라, “언리얼 GC와 호환되는 포인터”로 쓰기 위해 사용하는 것이다.
TObjectPtr 사용에 대한 강제성은 없다.
- 그렇다면 UObject를 사용하면 안되고, 반드시 TObjectPtr로 감싸서 사용해야할까? UObject를 사용하면 GC와 호환이 전혀 안되는 것일까?
- 또 그렇지는 않다. UObject* 자체도 UPROPERTY()와 함께 선언하면 언리얼 GC가 해당 포인터를 똑같이 추적한다. 즉, GC 호환을 위해 반드시 TObjectPtr을 사용해야하는건 아니라는 것이다. 중요한건 UPROPERTY() 추가 유무이다.
// UObject* => GC에 등록 안 됨
UObject* ObjectPtr0;
// UPROPERTY() + UObject* => GC에 정상 등록됨
UPROPERTY()
UObject* ObjectPtr1;
// UPROPERTY() + TObjectPtr => GC에 정상 등록됨
UPROPERTY()
TObjectPtr<UObject> ObjectPtr2;
TObjectPtr와 UObject*의 차이점
- 그럼 TObjectPtr을 굳이 사용해야하는 이유는 무엇일까? 사실 TObjectPtr은 언리얼 5에 와서 생긴 새로운 방식으로, 이전까지는 UObject* 방식을 사용하였다. 굳이 TObjectPtr을 만들었던 이유는 잘못된 포인터 사용에 의한 오류 발생을 예방하고 안전하게 사용하기 위함이다. 정리하면 아래와 같다.
- UObject* + UPROPERTY
- 오래전부터 사용된 방식.
- GC는 UPROPERTY 매크로에 등록된 포인터를 리플렉션 시스템을 통해 추적.
- 단순 포인터 연산에서 잘못된 접근 가능성이 높음(특히 null, dangling 등).
- TObjectPtr<UObject> + UPROPERTY
- 언리얼 5에서 도입된 새로운 방식.
- 여전히 GC는 UPROPERTY를 기준으로 추적.
- 내부적으로 FUObjectHandle을 사용하여 UObject 접근 시 안정성 향상(잘못된 포인터 접근을 더 빨리 감지).
- 향후 엔진 레벨 최적화 및 안전성 검증을 위한 권장 방식.
TObjectPtr 안정성 향상 예시
UPROPERTY()
UObject* RawObj; // 0xLastAddr -> 0xLastAddr
UPROPERTY()
TObjectPtr<UObject> SafeObj; // 0xLastAddr -> nullptr
- 두 방식의 포인터가 가리키던 메모리가 해제되었다고 가정해보자. 가리키던 메모리가 해제된 상황이라면 더이상 메모리에 접근하여 무언가 하려고 하면 안된다.
- 이때 생 포인터인 UObject는 여전히 해제된 메모리 주소를 들고있게 되는데, 이때 UObject를 통해 해제된 메모리에 접근하는 dangling pointer 접근 문제가 발생할 수 있다.
- 반면, TObjectPtr은 내부적으로 FUObjectHandle을 통해 접근하므로, 똑같이 해제된 메모리에 접근하려고 시도하면 nullptr을 반환하거나 내부 검증에서 통과를 시켜주지 않는다.
- 즉, 크래시가 발생하지 않고 안전하게 nullptr로 처리가 가능해진다.
TObjectPtr Get()
- TObjectPtr을 사용할때 Get함수로 가져와도 되지만, 아래와 같이 연산자 오버로딩이 잘되어있어 그냥 ‘→’를 사용해도 동일한 결과를 얻을 수 있다.
FORCEINLINE T* Get() const { return (T*)(FObjectPtr::Get()); }
FORCEINLINE T* operator->() const { return Get(); }
FORCEINLINE T& operator*() const { return *Get(); }