Skip to content

UE5 C++ 스마트 포인터 완전 정복

개요 — 언리얼 엔진의 두 가지 메모리 관리 세계

Section titled “개요 — 언리얼 엔진의 두 가지 메모리 관리 세계”

언리얼 엔진 C++에는 메모리를 관리하는 방식이 두 가지가 공존합니다.

세계대상관리 주체사용 도구
UObject 세계UObject 파생 클래스 (AActor, UComponent 등)언리얼 GC (가비지 컬렉터)UPROPERTY, TObjectPtr, TWeakObjectPtr
일반 C++ 세계일반 C++ 클래스/구조체 (F 접두사)개발자 또는 스마트 포인터TSharedPtr, TUniquePtr, TWeakPtr

이 두 세계를 혼용하면 심각한 버그가 발생할 수 있습니다. 이 가이드는 두 시스템을 명확히 구분하고 올바르게 사용하는 기준을 제시합니다.


언리얼 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 + TObjectPtr
UPROPERTY()
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)됩니다.

// 소유하지 않는 참조 — 대상이 먼저 소멸될 수 있는 경우
// 예: 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();
}
}
};
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, TUniquePtrUObject가 아닌 일반 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());
// TUniquePtr — 복사 불가, 이동만 가능. 소유자가 하나임을 보장
TUniquePtr<FNetworkSession> UniqueSession = MakeUnique<FNetworkSession>(TEXT("10.0.0.1"));
// 복사 불가 — 컴파일 에러
// TUniquePtr<FNetworkSession> Other = UniqueSession;
// 이동은 가능 — 소유권 이전
TUniquePtr<FNetworkSession> NewOwner = MoveTemp(UniqueSession);
// 이제 UniqueSession은 null, NewOwner가 소유
// 범위 종료 시 자동 delete

5. 스마트 포인터 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.1 네트워크 세션 관리 — TSharedPtr

Section titled “6.1 네트워크 세션 관리 — TSharedPtr”
NetworkManager.h
#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;
};
NetworkManager.cpp
#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
};

포인터 타입메모리 오버헤드접근 비용비고
날 포인터 (T*)없음최소UObject에만 + UPROPERTY 필수
TObjectPtr<T>없음 (포인터 크기 동일)에디터 빌드에서 소폭 증가UE5 표준
TWeakObjectPtr<T>약간 (인덱스 저장)IsValid() 체크 비용안전한 약한 참조
TSharedPtr<T>레퍼런스 카운트 블록카운트 증가/감소 (원자적)비 UObject용
TUniquePtr<T>없음날 포인터와 동일이동 전용

성능 팁: TSharedPtr의 레퍼런스 카운트 조작은 원자적 연산(Atomic Operation)으로 멀티스레드 환경에서 안전하지만 날 포인터보다 비쌉니다. 성능이 중요한 내부 루프에서는 TSharedRef를 변수에 저장해 반복 카운트 조작을 줄이거나 TUniquePtr을 사용하세요.


도구대상소유권핵심 규칙
UPROPERTY() TObjectPtr<T>UObjectGC 관리클래스 멤버 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>(); // 게임 스레드에서만 허용
});