[Unreal 5] 스마트 포인터 유효성 판단
1. nullptr verification vs IsValid() vs IsValidLowLevel()
CPP 원시 포인터의 경우
Object != nullptr 과 같은 방식으로 포인터가 유효한지에 대해서 검사한다.
그냥 정말 단순하게 포인터에 주소 값이 들어있는지 아닌지를 체크하는 것이다.
Unreal 5 스마트 포인터의 경우
Object->IsValid() 는 원시 포인터와 비슷한 방식으로 스마트 포인터가 유효한지에 대해서 검사한다.
다만, 이는 단순히 포인터의 주소가 유효한지 뿐만이 아니라
추가적으로 '안전하게 함수 호출이 가능한 상태인지', '오브젝트 참조가 가능한 상태인지' 를 검사한다.
실제로 IsValid 로 들어가 보면, 스마트 포인터를 Raw 포인터로 변환한 다음
아래와 같이 주소의 유효성 뿐만 아니라 CheckObjectValidBasedOnItsFlags 라는 검사를 동시에 하고 있다.
주석을 읽어 보면, pending kill or garbage 상태가 아니어야 valid 하다고 판단한다는 것을 알 수 있는데, 이를 위한 함수로 보여진다.
(Return true if the object is usable: non-null and not pending kill or garbage)
이를 이해하기 위해서는 'Garbage Collection' 을 먼저 이해하고 있어야 하는데,
이 글은 가비지 컬렉션을 이해하기 위한 글은 아니므로 간단하게 말하자면
'어느 객체에도 참조되고 있지 않은 메모리를 청소(해제)한다' 라고 생각하면 된다.
그렇다면, GC는 특정한 객체가 어떻게 다른 객체에 의해 참조되고 있는지 알 수 있을까?
사실 매우 간단하다.
그냥 Root 부터 시작해서 참조되는 모든 오브젝트를 찾아 다니면서
특정 오브젝트를 찾을 수 있다면, 참조되고 있는 오브젝트니까 찾을 수 있는 것이고
그렇지 않다면 참조되고 있지 않은 것이라고 판단하여
모든 조회를 마친 후, 참조되지 않는 객체들에 대해 메모리를 해제한다.
[UE5] Garbage Collection
[UE5] Garbage Collection 최초 작성 : 2023-07-05 마지막 수정 : 2023-07-05 최재호 목차 1. 환경 2. 목표 3. 내용 3.1. 언리얼 GC 방식인 Mark and Sweep 3.2. GC 가 GameThread 히칭을 발생시키는 주요 지점 중 하나 3.3. UE5 G
scahp.tistory.com
이 글에 아주 상세히 설명되어 있기 때문에 확인하면 아주 좋다.
어찌 됐건, GC에게 관리되어지기 위해서 오브젝트는 '리플렉션' 되고 있어야 한다.
언리얼 리플렉션은 위처럼 CG가 Root 부터 시작해서 참조되는 오브젝트를 찾으러 다닐 때
언리얼 엔진에게 자기 자신이 존재함을 알려주는 방법이다.
언리얼 엔진이 이 객체를 알고 있어야 (참조하고 있어야) 탐색을 통해 유효성 검사를 할 수 있으니 당연한 일이다.
즉, 자신을 언리얼 엔진에게 참조되게 함으로써 언리얼 에디터 등에 자신의 존재를 확인할 수 있게 되는 것이다.
스마트 포인터는 이러한 부분을 이용하여 보다 정밀하게 검사를 수행하는데,
아까 IsValid 를 타고 들어갔던 부분에서 조금 더 들어가 보면
이렇게 EInternalObjectFlags 를 통해 유효성을 검사한다는것을 알 수 있고
이 플래그가 뭐하는 놈인지 쪼끔 더 들어가 보면, 실제로 언리얼 엔진에서 이 객체를 관리하기 위한 여러 개의 플래그가
32비트짜리 비트마스킹으로 정의되어 있다는 것을 알 수 있다.
여기서 Garbage와 PendingKill의 주석을 읽어 보면, 가비지 컬렉션에 사용되는 플래그임을 알 수 있다.
Garbage = 객체를 참조하는 객체가 없기 때문에 Garbage 라고 판단하고 더 이상 참조될 수 없음을 의미한다.
PandingKill = 객체가 소멸되기를 기다리는 상태를 의미한다.
(5.0부터는 Deprecated 되어 사용하지 않고, 성능 상의 이유로 RF_PendingKill 플래그로 대체됨.)
어쨌든 둘 다 GC에 의해 청소될 놈들이라는 표시다.
따라서 스마트 포인터에서 IsValid 는, 주소가 유효함과 동시에 그 객체 또한 유효해야만 그 스마트 포인터가 유효함을 인정하는 것이다.
예를 들어, 오브젝트를 앞서 참조는 했지만, 오브젝트의 소멸 이후에 이를 재참조 하려고 한다면 이를 원시 포인터로써는 알 수 있는 방법이 없다. 소멸 이후에도 주소는 존재하지만, 다음 Tick 에서 오브젝트는 없을 수도 있다.
결국 게임이 Crash 나게 되는 것이다.
이것이 UE5에서 스마트 포인터를 표준으로 사용하려는 이유이다.
그러면 IsValidLowLevel() 은 뭘까?
오브젝트를 특정 시점에 참조를 하긴 했지만 해당 오브젝트가 생성 중이거나, 소멸 중의 시점에 참조되어서
아직까지 포인터 자체는 유효하지만 오브젝트 자체는 유효하지 않은 시점일 경우에, 다시 포인터 자체만으로는 그 유효성을 확신할 수 없다.
이런 상황에서는 오브젝트 자체의 유효성을 판단하는 IsValidLowLevel() 을 사용하여 검사할 수 있다.
일단 하나 확실히 해야 할 것은, IsValid() 와 IsValidLowLevel() 은 다르다는 것이다.
소스를 확인해 보면, IsValidLowLevel() 은 UObjectBase 에 선언되어있다.
때문에 우선은 Object를 참조할 수 있는 상태여야만 호출할 수 있을 것이다.
즉, 포인터를 사용하여 참조한다면 IsValid() 로 객체 참조가 유효한지 확인 한 후에
ValidLowLevel() 로 해당 오브젝트가 유효한지 확인해야 한다.
이러한 검사는 GUObjectArray, 즉 FUObjectArray 라는 전역 UObject 배열 인스턴스에 의해 관리되는데
여기로 다시 들어가 보면
IsValid 가 이렇게 정의되어 있다. 즉, 모든 UObject 는 이 FUObjectArray 라는 객체에 의해서 관리되어지며
여기서 Object의 Index 를 통해 배열 원소에 접근하여 해당 오브젝트의 유효성을 판단하는 것이다.
결론적으로, 만약 포인터 참조에 의한 런타임 문제가 생기는 것 같다면 이러한 부분들을 이해하고 해결법을 찾아 보길 바란다.