UE5 C++ 스마트 포인터 완전 정복
개요 — 언리얼 엔진의 두 가지 메모리 관리 세계
Section titled “개요 — 언리얼 엔진의 두 가지 메모리 관리 세계”언리얼 엔진 C++에는 메모리를 관리하는 방식이 두 가지가 공존합니다.
| 세계 | 대상 | 관리 주체 | 사용 도구 |
|---|---|---|---|
| UObject 세계 | UObject 파생 클래스 (AActor, UComponent 등) | 언리얼 GC (가비지 컬렉터) | UPROPERTY, TObjectPtr, TWeakObjectPtr |
| 일반 C++ 세계 | 일반 C++ 클래스/구조체 (F 접두사) | 개발자 또는 스마트 포인터 | TSharedPtr, TUniquePtr, TWeakPtr |
이 두 세계를 혼용하면 심각한 버그가 발생할 수 있습니다. 이 가이드는 두 시스템을 명확히 구분하고 올바르게 사용하는 기준을 제시합니다.
1. UObject GC 시스템 복습
Section titled “1. UObject GC 시스템 복습”1.1 GC가 UObject를 추적하는 방법
Section titled “1.1 GC가 UObject를 추적하는 방법”언리얼 GC는 UPROPERTY로 등록된 포인터를 통해서만 UObject를 추적합니다.
UCLASS()class AMyActor : public AActor{ GENERATED_BODY()
// GC가 추적함 — 안전 UPROPERTY() TObjectPtr<UMyComponent> SafeComponent;
// GC가 추적하지 않음 — 참조가 끊기면 댕글링 포인터 위험 UMyComponent* UnsafeComponent; // 절대 하지 마세요};1.2 UObject에 TSharedPtr를 쓰면 안 되는 이유
Section titled “1.2 UObject에 TSharedPtr를 쓰면 안 되는 이유”TSharedPtr<UObject>는 언리얼 GC와 독립된 레퍼런스 카운팅을 사용합니다. GC와 TSharedPtr가 동시에 같은 객체를 관리하려 하면 다음 문제가 발생합니다.
// 위험: UObject에 TSharedPtr 사용TSharedPtr<UMyComponent> SharedComp = MakeShared<UMyComponent>();// - GC는 이 컴포넌트를 UPROPERTY를 통해 추적하지 못함// - TSharedPtr의 레퍼런스 카운트와 GC의 추적이 충돌// - SharedComp가 범위를 벗어나 카운트가 0이 되면 delete 호출// - 하지만 GC도 이 객체를 아직 관리 중 → 이중 해제(double-free) 크래시
// 올바른 방법: UObject는 항상 UPROPERTY + TObjectPtrUPROPERTY()TObjectPtr<UMyComponent> SafeComp;핵심 규칙:
UObject파생 클래스에는 절대TSharedPtr/TUniquePtr를 사용하지 않습니다. UObject의 소유와 참조는 반드시 UE GC 시스템(UPROPERTY)으로 관리합니다.
2. TObjectPtr — UE5 표준 UObject 멤버 포인터
Section titled “2. TObjectPtr — UE5 표준 UObject 멤버 포인터”TObjectPtr<T>는 UE5에서 클래스 멤버에서 UObject를 가리키는 표준 포인터 타입입니다. UE4의 날(raw) 포인터(UMyComp*)를 대체합니다.
// UE4 스타일 (여전히 동작하지만 권장하지 않음)UPROPERTY(VisibleAnywhere)UStaticMeshComponent* MeshComp;
// UE5 표준 스타일UPROPERTY(VisibleAnywhere)TObjectPtr<UStaticMeshComponent> MeshComp;TObjectPtr의 장점:
| 특징 | 설명 |
|---|---|
| 접근 추적 | 에디터 빌드에서 잘못된 포인터 접근 감지 |
| Lazy Loading | 쿠킹된 빌드에서 에셋 지연 로딩 지원 |
| 명시적 의도 | UObject 멤버임을 코드에서 명확히 표현 |
| GC 통합 | UPROPERTY와 함께 사용 시 GC 추적 완벽 지원 |
UCLASS()class MYGAME_API AMyCharacter : public ACharacter{ GENERATED_BODY()
// 올바른 UE5 스타일 UPROPERTY(VisibleAnywhere, Category = "Components") TObjectPtr<USkeletalMeshComponent> CharacterMesh;
UPROPERTY(EditDefaultsOnly, Category = "Config") TObjectPtr<class USoundBase> FootstepSound;
// TObjectPtr 사용 방법 — 일반 포인터와 동일하게 접근 void PlayFootstep() { if (CharacterMesh && FootstepSound) { UGameplayStatics::SpawnSoundAtLocation(this, FootstepSound, CharacterMesh->GetComponentLocation()); } }};3. TWeakObjectPtr — 약한 UObject 참조
Section titled “3. TWeakObjectPtr — 약한 UObject 참조”TWeakObjectPtr<T>는 GC 수집을 막지 않는 **약한 참조(Weak Reference)**입니다. 참조 대상이 소멸되면 자동으로 무효화(null)됩니다.
3.1 사용 상황
Section titled “3.1 사용 상황”// 소유하지 않는 참조 — 대상이 먼저 소멸될 수 있는 경우// 예: AI가 추적 중인 플레이어, 캐시된 참조, 순환 참조 방지
UCLASS()class MYGAME_API UAIPerceptionComponent : public UActorComponent{ GENERATED_BODY()
// 약한 참조 — 플레이어가 소멸되어도 크래시 없음 TWeakObjectPtr<APlayerCharacter> TrackedPlayer;
void UpdateTracking(APlayerCharacter* Player) { TrackedPlayer = Player; }
void Tick(float DeltaTime) { // 접근 전 반드시 IsValid() 체크 if (TrackedPlayer.IsValid()) { FVector Direction = TrackedPlayer->GetActorLocation() - GetOwner()->GetActorLocation(); // ... 추적 로직 } else { // 플레이어가 소멸됨 — 추적 중단 StopTracking(); } }};3.2 IsValid() vs nullptr 체크
Section titled “3.2 IsValid() vs nullptr 체크”TWeakObjectPtr<AMyActor> WeakRef;
// 잘못된 방법 — nullptr 체크만으로는 부족if (WeakRef.Get() != nullptr) // Pending Kill 상태를 잡지 못함{ WeakRef->DoSomething();}
// 올바른 방법 — IsValid()는 null + Pending Kill 모두 체크if (WeakRef.IsValid()){ WeakRef->DoSomething();}
// 또는 전역 IsValid() 함수 사용 (UObject* 와 TWeakObjectPtr 모두 지원)if (IsValid(WeakRef.Get())){ WeakRef->DoSomething();}4. UE 스마트 포인터 — 비 UObject 관리
Section titled “4. UE 스마트 포인터 — 비 UObject 관리”TSharedPtr, TSharedRef, TWeakPtr, TUniquePtr는 UObject가 아닌 일반 C++ 클래스/구조체를 힙에서 관리할 때 사용합니다.
4.1 TSharedPtr — 공유 소유권 (레퍼런스 카운팅)
Section titled “4.1 TSharedPtr — 공유 소유권 (레퍼런스 카운팅)”#include "Templates/SharedPointer.h"
// 일반 C++ 클래스 (UObject 아님)class FNetworkSession{public: FNetworkSession(const FString& InAddress) : Address(InAddress) {} FString Address; void Connect() { /* ... */ }};
// TSharedPtr — 여러 소유자가 같은 객체를 공유TSharedPtr<FNetworkSession> SessionA = MakeShared<FNetworkSession>(TEXT("192.168.0.1"));TSharedPtr<FNetworkSession> SessionB = SessionA; // 레퍼런스 카운트 2
// 접근if (SessionA.IsValid()){ SessionA->Connect();}
// 명시적 해제 — 카운트 1 감소SessionA.Reset();// SessionB가 아직 참조 중이므로 객체는 살아있음
// SessionB도 해제 — 카운트 0, 자동으로 delete 호출SessionB.Reset();4.2 TSharedRef — null이 될 수 없는 TSharedPtr
Section titled “4.2 TSharedRef — null이 될 수 없는 TSharedPtr”// TSharedRef는 생성 시점부터 항상 유효한 객체를 가리킴// null 체크 불필요 — 컴파일 타임에 null 불가 보장
TSharedRef<FNetworkSession> SessionRef = MakeShared<FNetworkSession>(TEXT("10.0.0.1"));
// IsValid() 없이 바로 사용SessionRef->Connect();
// TSharedPtr로 변환 가능 (역방향은 ToSharedRef() 사용)TSharedPtr<FNetworkSession> SessionPtr = SessionRef;4.3 TWeakPtr — 공유 객체의 약한 참조
Section titled “4.3 TWeakPtr — 공유 객체의 약한 참조”TSharedPtr<FNetworkSession> Session = MakeShared<FNetworkSession>(TEXT("10.0.0.1"));TWeakPtr<FNetworkSession> WeakSession = Session;
// 약한 참조로 접근 — Pin()으로 일시적인 강한 참조 획득if (TSharedPtr<FNetworkSession> Pinned = WeakSession.Pin()){ // Pinned가 유효한 동안 Session이 소멸되지 않음 Pinned->Connect();}// Pinned 범위 종료 → 일시적 강한 참조 해제
// Session.Reset() 후 WeakSession.IsValid()는 false 반환Session.Reset();check(!WeakSession.IsValid());4.4 TUniquePtr — 단독 소유권
Section titled “4.4 TUniquePtr — 단독 소유권”// TUniquePtr — 복사 불가, 이동만 가능. 소유자가 하나임을 보장TUniquePtr<FNetworkSession> UniqueSession = MakeUnique<FNetworkSession>(TEXT("10.0.0.1"));
// 복사 불가 — 컴파일 에러// TUniquePtr<FNetworkSession> Other = UniqueSession;
// 이동은 가능 — 소유권 이전TUniquePtr<FNetworkSession> NewOwner = MoveTemp(UniqueSession);// 이제 UniqueSession은 null, NewOwner가 소유
// 범위 종료 시 자동 delete5. 스마트 포인터 vs UPROPERTY 판단 기준
Section titled “5. 스마트 포인터 vs UPROPERTY 판단 기준”올바른 도구 선택이 중요합니다. 다음 기준으로 결정하세요.
대상 클래스가 UObject를 상속하는가?├─ YES → UPROPERTY + TObjectPtr (멤버) / TWeakObjectPtr (약한 참조)│ 절대 TSharedPtr/TUniquePtr 사용 금지└─ NO → 어떤 소유권이 필요한가? ├─ 단독 소유 → TUniquePtr ├─ 공유 소유 → TSharedPtr / TSharedRef └─ 약한 참조 → TWeakPtr| 상황 | 올바른 도구 |
|---|---|
| Actor, Component 등 UObject 멤버 포인터 | UPROPERTY() TObjectPtr<T> |
| UObject를 관찰만 하는 참조 (캐시) | TWeakObjectPtr<T> (UPROPERTY 없어도 됨) |
| 일반 C++ 클래스 단독 소유 | TUniquePtr<T> |
| 일반 C++ 클래스 공유 소유 | TSharedPtr<T> |
| null이 없어야 하는 공유 참조 | TSharedRef<T> |
| 공유 객체 약한 참조 | TWeakPtr<T> |
6. 실전 — Non-UObject 관리 예시
Section titled “6. 실전 — Non-UObject 관리 예시”6.1 네트워크 세션 관리 — TSharedPtr
Section titled “6.1 네트워크 세션 관리 — TSharedPtr”#pragma once
#include "CoreMinimal.h"#include "Subsystems/GameInstanceSubsystem.h"#include "NetworkManager.generated.h"
// 일반 C++ 클래스 — UObject 아님class FGameSession{public: FGameSession(const FString& SessionName, int32 MaxPlayers) : Name(SessionName), MaxPlayers(MaxPlayers) {}
FString Name; int32 MaxPlayers; TArray<FString> ConnectedPlayers;
bool AddPlayer(const FString& PlayerName); void RemovePlayer(const FString& PlayerName);};
UCLASS()class MYGAME_API UNetworkManager : public UGameInstanceSubsystem{ GENERATED_BODY()
public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override;
UFUNCTION(BlueprintCallable, Category = "Network") void CreateSession(const FString& SessionName, int32 MaxPlayers);
UFUNCTION(BlueprintCallable, Category = "Network") void DestroySession();
// TWeakPtr으로 세션 참조 공유 (소유권은 UNetworkManager) TWeakPtr<FGameSession> GetCurrentSession() const { return CurrentSession; }
private: // 세션 단독 소유 — UNetworkManager가 수명 관리 TSharedPtr<FGameSession> CurrentSession;};#include "NetworkManager.h"
void UNetworkManager::Initialize(FSubsystemCollectionBase& Collection){ Super::Initialize(Collection);}
void UNetworkManager::Deinitialize(){ // TSharedPtr이 소유 — Reset()으로 명시적 해제 또는 소멸자에서 자동 해제 CurrentSession.Reset(); Super::Deinitialize();}
void UNetworkManager::CreateSession(const FString& SessionName, int32 MaxPlayers){ // 기존 세션 해제 if (CurrentSession.IsValid()) { DestroySession(); }
CurrentSession = MakeShared<FGameSession>(SessionName, MaxPlayers); UE_LOG(LogTemp, Log, TEXT("Session created: %s (Max: %d)"), *SessionName, MaxPlayers);}
void UNetworkManager::DestroySession(){ if (CurrentSession.IsValid()) { UE_LOG(LogTemp, Log, TEXT("Destroying session: %s"), *CurrentSession->Name); CurrentSession.Reset(); }}6.2 TUniquePtr — RAII 패턴으로 리소스 관리
Section titled “6.2 TUniquePtr — RAII 패턴으로 리소스 관리”// 파일 핸들, 외부 SDK 래퍼 등 단독 소유가 명확한 경우class FExternalSDKWrapper{public: FExternalSDKWrapper() { /* SDK 초기화 */ } ~FExternalSDKWrapper() { /* SDK 정리 */ }
void SendEvent(const FString& EventName) { /* ... */ }};
UCLASS()class MYGAME_API UAnalyticsSubsystem : public UGameInstanceSubsystem{ GENERATED_BODY()
public: virtual void Initialize(FSubsystemCollectionBase& Collection) override { Super::Initialize(Collection); // 소유권 명확 — 이 서브시스템만 SDK를 소유 SDKWrapper = MakeUnique<FExternalSDKWrapper>(); }
virtual void Deinitialize() override { // 소멸자에서 자동으로 delete — 명시적 호출 불필요 SDKWrapper.Reset(); Super::Deinitialize(); }
void TrackEvent(const FString& EventName) { if (SDKWrapper) { SDKWrapper->SendEvent(EventName); } }
private: TUniquePtr<FExternalSDKWrapper> SDKWrapper;};6.3 혼합 패턴 — UObject + 비 UObject 협력
Section titled “6.3 혼합 패턴 — UObject + 비 UObject 협력”// UCLASS 멤버에서 두 시스템을 올바르게 혼합하는 예시UCLASS()class MYGAME_API AMyGameCharacter : public ACharacter{ GENERATED_BODY()
// --- UObject 계열: UPROPERTY + TObjectPtr --- UPROPERTY(VisibleAnywhere, Category = "Components") TObjectPtr<UStaticMeshComponent> WeaponMesh; // 강한 참조
UPROPERTY(EditDefaultsOnly, Category = "Config") TObjectPtr<class UDataTable> StatsDataTable; // 에셋 참조
TWeakObjectPtr<APlayerController> CachedController; // 약한 UObject 참조
// --- 비 UObject 계열: TSharedPtr/TUniquePtr --- TUniquePtr<FExternalSDKWrapper> AnalyticsSDK; // 단독 소유 비 UObject TSharedPtr<FGameSession> ActiveSession; // 공유 소유 비 UObject};7. 스마트 포인터 성능 고려
Section titled “7. 스마트 포인터 성능 고려”| 포인터 타입 | 메모리 오버헤드 | 접근 비용 | 비고 |
|---|---|---|---|
날 포인터 (T*) | 없음 | 최소 | UObject에만 + UPROPERTY 필수 |
TObjectPtr<T> | 없음 (포인터 크기 동일) | 에디터 빌드에서 소폭 증가 | UE5 표준 |
TWeakObjectPtr<T> | 약간 (인덱스 저장) | IsValid() 체크 비용 | 안전한 약한 참조 |
TSharedPtr<T> | 레퍼런스 카운트 블록 | 카운트 증가/감소 (원자적) | 비 UObject용 |
TUniquePtr<T> | 없음 | 날 포인터와 동일 | 이동 전용 |
성능 팁:
TSharedPtr의 레퍼런스 카운트 조작은 원자적 연산(Atomic Operation)으로 멀티스레드 환경에서 안전하지만 날 포인터보다 비쌉니다. 성능이 중요한 내부 루프에서는TSharedRef를 변수에 저장해 반복 카운트 조작을 줄이거나TUniquePtr을 사용하세요.
| 도구 | 대상 | 소유권 | 핵심 규칙 |
|---|---|---|---|
UPROPERTY() TObjectPtr<T> | UObject | GC 관리 | 클래스 멤버 UObject 참조의 표준 |
TWeakObjectPtr<T> | UObject | 없음 (약한) | IsValid() 필수, GC 수집 허용 |
TUniquePtr<T> | 비 UObject | 단독 | 이동만 가능, 복사 불가 |
TSharedPtr<T> | 비 UObject | 공유 | 레퍼런스 카운팅, 순환 참조 주의 |
TSharedRef<T> | 비 UObject | 공유 | null 불가 보장 |
TWeakPtr<T> | 비 UObject | 없음 (약한) | Pin()으로 일시적 강한 참조 획득 |
절대 하지 말아야 할 것
Section titled “절대 하지 말아야 할 것”// 1. UObject에 TSharedPtr 사용 — 이중 해제 크래시TSharedPtr<UMyComponent> Comp = MakeShared<UMyComponent>(); // 금지
// 2. UPROPERTY 없는 UObject* 멤버 — 댕글링 포인터UMyComponent* Comp; // UPROPERTY 없으면 금지
// 3. TWeakObjectPtr IsValid() 없이 역참조WeakActor->DoSomething(); // IsValid() 체크 없이 금지
// 4. 백그라운드 스레드에서 UObject 생성AsyncTask(ENamedThreads::AnyBackgroundThread, [](){ UMyObject* Obj = NewObject<UMyObject>(); // 게임 스레드에서만 허용});