스토리텔링 개발자

[UE4] 스마트 포인터(Smart Pointer) 본문

개발/언리얼 엔진

[UE4] 스마트 포인터(Smart Pointer)

김디트 2021. 8. 9. 22:40
728x90

개요

메모리 관리는 C++의 난점 중 하나입니다.

 

스택에 할당된 메모리는 범위만 잘 지켜주면 대부분은 괜찮지만,

힙에 할당한 메모리는 물가에 내놓은 갓난 아이처럼 끝까지 잘 지켜봐 줘야 합니다.

자칫 잘못하면 그대로 소중한 메모리의 누수로까지 이어집니다.

 

좀 더 편하게 메모리 관리를 할 수 있는 방법은 없을까요?

 

그런 고민에서 생겨난 것이 이름처럼 스마트한 스마트 포인터입니다.

 

이 스마트 포인터는, 이젠 준 필수 기능이 되었습니다.

이전까진 시험적으로 사용해오다가, 모던 C++부터는 정식 라이브러리에 완전히 편입되기까지 했죠.

 

또한 언리얼 역시 이 C++ 라이브러리가 제공하는 스마트 포인터와 거의 동일한 것을 제공하고 있습니다.

 

오늘 이야기해 볼 것이 이 언리얼의 스마트 포인터 입니다.

 

 

 

스마트 포인터의 종류

언리얼에는 두 가지 종류의 인스턴스가 있습니다.

 

1. C++ 구조체를 기반으로 하는 구조체

  • C++ 메모리 관리 체계를 사용합니다.

2. UObject를 상속받는 게임 오브젝트 클래스

  • 언리얼 가비지 컬렉션의 관리를 받습니다.

 

이에 대해 우선 간략하게 알아보도록 하겠습니다.

 

구조체 기반 스마트 포인터(C++)

 

기존 C++ 메모리 관리 체계를 사용합니다.

이 경우, raw C++ 기반의 테크닉을 사용할 수밖에 없습니다.

 

그래서 언리얼은 모던 C++의 것을 본뜬 세 가지의 스마트 포인터를 제공합니다.

 

종류는 아래와 같습니다.

 

1. TUniquePtr (유니크 포인터)

2. TSharedPtr (공유 포인터)

3. TWeakPtr (약한 포인터)

 

각각에 대한 상세 설명은 아래에서 이어가도록 하겠습니다.

 

그리고 여기서 잠깐 짚고 넘어가고 싶은 점이 있습니다.

 

언리얼에는 C++의 것을 본뜬 세 가지 스마트 포인터가 있는데,

모던 C++의 것을 직접 사용하지 않고 굳이 본뜬 새로운 스마트 포인터를 마련한 이유가 뭘까요?

 

이는 스마트 포인터 성능의 일관성을 유지하기 위함입니다.

 

모던 C++에 포함된, 표준 라이브러리의 것은 컴파일러의 구현에 따라 성능과 구현이 달라지게 됩니다.

하지만 엔진에서 구현하게 되면 어떤 환경에서도 동일한 성능과 구현을 표현할 수 있게 됩니다.

 

오브젝트 기반 스마트 포인터(UE4)

 

언리얼의 UObject 인스턴스는 기본적으로 가비지 컬렉션의 관리를 받습니다.

하지만 구조적인 문제로 순환 참조 가 발생할 여지가 있습니다.

 

이를 해결하기 위해 두 가지 스마트 포인터를 제공합니다.

 

1. TWeakObjectPtr (약한 포인터)

2. TStrongObjectPtr (강한 포인터)

 

 

 

구조체 기반 스마트 포인터

각 스마트 포인터의 특성을 좀 더 자세히 알아보도록 하겠습니다.

 

유니크 포인터 (TUniquePtr)

 

유일한 소유권을 가지는 스마트 포인터입니다.

 

유일한 소유권이라는 의미는,

복사가 불가능하며, 이동으로만 포인터를 옮길 수 있다는 뜻입니다.

 

아래의 예제를 보시죠.

 

Temp = new FTempClass();
TUniquePtr<FTempClass> UniqueTemp = TUniquePtr<FTempClass>(Temp);

// 카피는 컴파일 에러가 납니다.
TUniquePtr<FTempClass> CopiedTemp = UniqueTemp;

// rvalue 이동 시멘틱을 사용하여 이동시킵니다.
// UniqueTemp는 nullptr를 가지고, MovedTemp가 Temp를 가지게 됩니다.
TUniquePtr<FTempClass> MovedTemp = MoveTemp(UniqueTemp);

 

소유권이 유일하다는 것의 장점은, 역시 관리가 편리하다는 점이겠죠.

단 하나 뿐이므로 언제 메모리가 해제될 지 명확히 파악할 수 있습니다.

 

 

공유 포인터 (TSharedPtr)

 

유니크 포인터와는 다르게, 소유권을 복사할 수 있는 스마트 포인터입니다.

 

레퍼런스 카운트를 통해 관리됩니다.

즉, 복사, 해제마다 레퍼런스 카운트를 증가, 감소시켜 해제 시점을 결정합니다.

레퍼런스 카운트가 0 이 될 때, 관리하고 있는 인스턴스를 해제시킵니다.

 

아래는 예제입니다.

 

FTempClass* Temp = new FTempClass();
{
	// MakeShareable()을 통해 공유 포인터를 생성할 수 있습니다.
    TSharedPtr<FTempClass> SharedTemp = MakeShareable(Temp);

	// 공유 포인터는 유효성 체크가 가능하므로 안전합니다.
    if(SharedTemp.IsValid())
        UE_LOG(Log, TEXT("포인터가 살아있다.");
}

// 지역 범위가 종료되면서 공유 포인터, 그리고 관리되는 Temp의 메모리가 해제됩니다.
// warning : 이 경우 변수 Temp가 댕글링 포인터를 가리키게 되므로 위험합니다.

 

기존의 포인터와 동일한 방식으로 사용할 수 있습니다.

다만, 이 경우 순환 참조가 생길 수 있다는 문제가 있습니다.

 

 

약한 포인터 (TWealPtr)

 

공유 포인터의 순환 참조 문제를 해결하기 위해 사용합니다.

 

여기서 잠깐, 순환 참조에 대해 짚어보자면

두 포인터가 서로를 참조하여 레퍼런스 카운트가 절대 0이 되지 않는 현상을 의미합니다.

 

예제로 확인해 봅시다.

class YourClass
{
public:
    TSharedPtr<YourClass> OtherObject;
};

{
	TSharedPtr<YourClass> Object1 = MakeShared<YourClass>();
	TSharedPtr<YourClass> Object2 = MakeShared<YourClass>();

	// 서로를 가리키는 상황
	Object1->OtherObject = Object2;
	Object2->OtherObject = Object1;

	// 레퍼런스 카운트가 존재하므로 블럭이 끝나도 Object1, Object2는 모두 해제되지 않습니다.
}

 

 

약한 포인터는 공유 포인터, 약한 포인터로만 생성할 수 있습니다.

이말인 즉, 약한 포인터 생성을 위해선 템플릿 스마트 포인터가 필요합니다.

 

반대로 약한 포인터에서 Pin() 함수를 사용하여 새로운 공유 포인터 생성도 가능합니다.

 

Temp = new FTempClass();
TSharedPtr<FTempClass> SharedTemp = MakeSharable(Temp);

{

	// 약한 포인터 생성
    TWeakPtr<FTempClass> WeakTemp = TWeakPtr<FTempClass>(SharedTemp);

	// 새로운 공유 포인터를 만들어낼 수도 있습니다.
    TSharedPtr<FTempClass> NewSharedTemp = WeakTemp.Pin(); 
    
} // 여기서 WeakTemp, NewSharedTemp가 소멸됩니다.(지역 범위 종료)

// 하지만 SharedTemp가 아직 살아있으므로 레퍼런스 카운터는 1.
// Temp 포인터는 여전히 살아 있습니다.

 

 

 

오브젝트 기반 스마트 포인터

오브젝트 기반 인스턴스는 언리얼 가비지 컬렉션의 관리를 받습니다.

 

이 가비지 컬렉션에 대해 먼저 이야기 해보겠습니다.

 

가비지 컬렉션은 오브젝트 메모리를 알아서 관리, 정리해주는 시스템 전반을 의미합니다.

메모리 관리에 신경을 덜 써도 된다는 이점이 있습니다.

그에 반해 언제 수집하고 제거할지 특정할 수 없다는 단점도 있습니다.

 

언리얼의 가비지 컬렉션은 기본적으로 공유 포인터(SharedPtr)처럼 레퍼런스 카운트를 활용합니다.
UPROPERTY() 태깅이 된 인스턴스에 카운트를 증가시킵니다.

 

즉, 구조적으로 공유 포인터와 동일하다는 의미이므로

공유 포인터의 순환 참조 문제가 언리얼 오브젝트에서도 발생합니다.

 

그래서 필요한 것이 오브젝트 기반의 스마트 포인터입니다.

 

 

약한 포인터 (TWeakObjectPtr)

 

TWeakPtr와 동일한 목적의 약한 포인터입니다.

 

원본 UObject 인스턴스의 레퍼런스 카운트를 증가시키지 않으므로 유효성 검사가 필요합니다.

원본 인스턴스가 더 이상 유효하지 않을 수 있기 때문입니다.

 

UTempClass* Temp = NewObject<UTempClass>();

// 이처럼 할당만으로 TWeakObjectPtr 생성 가능합니다.
TWeakObjectPtr<UTempClass> WeakTemp = Temp;

// 기존 인스턴스가 살아있다면 출력됩니다.
if(WeakTemp.IsValid())
    UE_LOG(Log, TEXT("포인터가 살아있다.");

 

강한 포인터 (TStrongObjectPtr)

 

UObject 인스턴스의 레퍼런스 카운트를 올리기 위해서는 UPROPERTY() 마킹이 필요합니다.

 

그렇다면 지역 변수의 가비지 컬렉션 수집을 막고 싶은 경우를 생각해 봅시다.

사용 중에 수집되어 오브젝트가 제거되기라도 하면 곤란한 상황이 될테니까요.

 

이 경우엔 UPROPERTY() 마킹을 어떻게 해야 할까요?

 

이 경우 TStrongObjectPtr를 사용하게 됩니다.

사용법은 약한 포인터와 거의 동일합니다.

UTempClass* Temp = NewObject<UTempClass>();

// 강한 포인터를 생성합니다.
TStrongObjectPtr<UTempClass> StrongTemp = TStrongObjectPtr<UTempClass>(Temp);

// 기존 인스턴스가 살아있다면 출력합니다.
if(StrongTemp.IsValid()) 
    UE_LOG(Log, TEXT("포인터가 살아있다.");

 

 

 

같이 읽어볼만한 글

 

[Effective C++] 13. 자원 관리 객체(RAII)

항목 13 : 자원 관리에는 객체가 그만!   자원 삭제 실패 케이스객체 삭제는 실패할 수 있는 경우가 다양하다.삭제 전에 return문이 들어있는 경우삭제 전 continue 혹은 goto로 루프를 갑작스래 빠져

delightlane.tistory.com

 

728x90
Comments