UE5 C++ 스마트 포인터
개요 — 언리얼 엔진의 두 가지 메모리 관리 세계
섹션 제목: “개요 — 언리얼 엔진의 두 가지 메모리 관리 세계”언리얼 엔진 C++에는 메모리를 관리하는 방식이 두 가지가 공존합니다.
| 세계 | 대상 | 관리 주체 | 사용 도구 |
|---|---|---|---|
| UObject 세계 | UObject 파생 클래스 (AActor, UComponent 등) | 언리얼 GC (가비지 컬렉터) | UPROPERTY, TObjectPtr, TWeakObjectPtr |
| 일반 C++ 세계 | 일반 C++ 클래스/구조체 (F 접두사) | 개발자 또는 스마트 포인터 | TSharedPtr, TUniquePtr, TWeakPtr |
이 두 세계를 혼용하면 심각한 버그가 발생할 수 있습니다. 이 가이드는 두 시스템을 명확히 구분하고 올바르게 사용하는 기준을 제시합니다.
1. UObject GC 시스템 복습
섹션 제목: “1. UObject GC 시스템 복습”1.1 GC가 UObject를 추적하는 방법
섹션 제목: “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를 쓰면 안 되는 이유
섹션 제목: “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 멤버 포인터
섹션 제목: “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 참조
섹션 제목: “3. TWeakObjectPtr — 약한 UObject 참조”TWeakObjectPtr<T>는 GC 수집을 막지 않는 **약한 참조(Weak Reference)**입니다. 참조 대상이 소멸되면 자동으로 무효화(null)됩니다.
3.1 사용 상황
섹션 제목: “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 체크
섹션 제목: “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 관리
섹션 제목: “4. UE 스마트 포인터 — 비 UObject 관리”TSharedPtr, TSharedRef, TWeakPtr, TUniquePtr는 UObject가 아닌 일반 C++ 클래스/구조체를 힙에서 관리할 때 사용합니다.
4.1 TSharedPtr — 공유 소유권 (레퍼런스 카운팅)
섹션 제목: “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
섹션 제목: “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 — 공유 객체의 약한 참조
섹션 제목: “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 — 단독 소유권
섹션 제목: “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 판단 기준
섹션 제목: “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 관리 예시
섹션 제목: “6. 실전 — Non-UObject 관리 예시”6.1 네트워크 세션 관리 — TSharedPtr
섹션 제목: “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 패턴으로 리소스 관리
섹션 제목: “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 협력
섹션 제목: “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. 스마트 포인터 성능 고려
섹션 제목: “7. 스마트 포인터 성능 고려”| 포인터 타입 | 메모리 오버헤드 | 접근 비용 | 비고 |
|---|---|---|---|
날 포인터 (T*) | 없음 | 최소 | UObject에만 + UPROPERTY 필수 |
TObjectPtr<T> | 없음 (포인터 크기 동일) | 에디터 빌드에서 소폭 증가 | UE5 표준 |
TWeakObjectPtr<T> | 약간 (인덱스 저장) | IsValid() 체크 비용 | 안전한 약한 참조 |
TSharedPtr<T> | 레퍼런스 카운트 블록 | 카운트 증가/감소 (원자적) | 비 UObject용 |
TUniquePtr<T> | 없음 | 날 포인터와 동일 | 이동 전용 |
성능 팁:
TSharedPtr의 레퍼런스 카운트 조작은 원자적 연산(Atomic Operation)으로 멀티스레드 환경에서 안전하지만 날 포인터보다 비쌉니다. 성능이 중요한 내부 루프에서는TSharedRef를 변수에 저장해 반복 카운트 조작을 줄이거나TUniquePtr을 사용하세요.
8. 정리
섹션 제목: “8. 정리”| 도구 | 대상 | 소유권 | 핵심 규칙 |
|---|---|---|---|
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()으로 일시적 강한 참조 획득 |
절대 하지 말아야 할 것
섹션 제목: “절대 하지 말아야 할 것”// 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>(); // 게임 스레드에서만 허용});9. 스레드 안전 TSharedPtr — ESPMode::ThreadSafe
섹션 제목: “9. 스레드 안전 TSharedPtr — ESPMode::ThreadSafe”기본 TSharedPtr은 ESPMode::Fast(비원자적 카운팅)입니다. 백그라운드 스레드에서 동시에 접근하는 비 UObject 데이터는 ESPMode::ThreadSafe를 명시해야 합니다.
// ESPMode::ThreadSafe — 원자적 레퍼런스 카운팅 (멀티스레드 안전)TSharedPtr<FNetworkSession, ESPMode::ThreadSafe> SharedSession = MakeShared<FNetworkSession, ESPMode::ThreadSafe>(TEXT("10.0.0.1"));
// 백그라운드 스레드에서 안전하게 캡처AsyncTask(ENamedThreads::AnyBackgroundThread, [SharedSession](){ // 람다 캡처 시 카운트 원자적 증가 → 안전 if (SharedSession.IsValid()) { SharedSession->ProcessPackets(); } // 람다 소멸 시 카운트 원자적 감소});
// 게임 스레드에서도 동시에 접근 가능if (SharedSession.IsValid()){ UE_LOG(LogTemp, Log, TEXT("Session: %s"), *SharedSession->Name);}| 모드 | 레퍼런스 카운팅 | 성능 | 사용 상황 |
|---|---|---|---|
ESPMode::Fast (기본) | 비원자적 | 빠름 | 단일 스레드 |
ESPMode::ThreadSafe | 원자적 | 소폭 느림 | 멀티스레드 공유 |
주의:
ESPMode::Fast와ESPMode::ThreadSafe포인터는 서로 다른 타입이므로 직접 대입할 수 없습니다. 처음부터 올바른 모드로 생성해야 합니다.