[UE4] 가비지 컬렉션에 관한 유의사항
언리얼 엔진은 보면 볼수록 참 대단하다는 생각이 든다. 그중에서도 가장 대단하다고 생각되는 것은 리플렉션 시스템과 가비지 컬렉터다. 오늘은 가비지 컬렉터에 대한 나의 삽질기를 얘기해볼까 한다.
https://api.unrealengine.com/KOR/Programming/UnrealArchitecture/Objects/Optimizations/index.html#%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4%EC%9E%90%EB%8F%99%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8
위 문서에는 굉장히 중요한 얘기가 있다.
AActor
또는UActorComponent
가 소멸되면 레퍼런스는 자동으로null
이 됩니다.- 이 기능은
UPROPERTY
로 마킹되어 있거나 언리얼 엔진 컨테이너 클래스에 저장된 레퍼런스에만 적용됩니다. raw 포인터에 저장된 오브젝트 레퍼런스는 언리얼 엔진이 알지 못하기 때문에, 자동으로null
되거나, 가비지 컬렉션이 방지되지 않습니다.
즉, UPROPERTY
로 마킹된 AActor
또는 UActorComponent
레퍼런스는 해당 오브젝트가 삭제되면 자동으로 null
이 된다는 것이다. 덕분에 우리는 해당 오브젝트를 참조하려고 할 때 레퍼런스가 null
인지만 확인하면 된다. 하지만 과연 정말 그럴까? 여기에는 몇가지 유의사항이 있다. 지금부터 내가 한 삽질들을 소개하겠다.
나는 최적화를 위해 어떤 종류의 액터든 관리할 수 있는 전역 액터 풀을 만들었다. 그리고 그 액터 풀은 대충 TMap<UClass*, TArray<AActor*>>
의 형태로 관리된다. A 클래스를 집어넣으면 사용 가능한 A 액터 목록을 확인할 수 있는 구조다.
그러나 가끔 알 수 없는 이상한 오류로 엔진이 크래시났다. 아무리 봐도 에러가 날 이유가 없는 곳에서 에러가 났다. 나는 1시간의 디버깅 끝에 그 원인을 찾아냈다. 액터 풀에 레퍼런싱 되어있는 액터가 삭제되어도 레퍼런스가 null
이 되지 않는 것이었다. UPROPERTY
지정을 하지 않은 것이 원인이었다. 그래서 뒤늦게 UPROPERTY
를 추가하고 컴파일을 시도했으나 에러를 뿜어냈다. 중첩 컨테이너는 지원하지 않는댄다.
어떻게 할지 고민하다가 TWeakObjectPtr
이라는 놈을 발견했다. 사실 위 문서에도 있었다.
UProperty 가 아닌 오브젝트 포인터가 필요한 경우, TWeakObjectPtr 사용을 고려해 보세요. 이는 약 포인터로, 가비지 컬렉션을 방지하지는 않지만, 접근 전 질의를 통해 유효성 검사가 가능하며, 거기서 가리키는 오브젝트가 소멸된 경우
null
설정도 가능합니다.
이제 형태는 TMap<UClass*, TArray<TWeakObjectPtr<AActor>>>
이 된다. 접근할떄 그저 .Get()
을 붙여주기만 하면 된다.
이렇게 문제는 해결했지만 궁금증이 남았다. TArray
에 담긴 레퍼런스는 과연 UPROPERTY
를 붙여주지 않아도 언리얼 리플렉션 시스템에 보일까? 결론부터 말하면 아니다. UPROPERTY
를 붙여줘야 한다. 이를 알아내기 위해 실험을 해보았으나 결과가 이상했고, 가비지 컬렉터 설정도 바꿔가며 새로운 사실도 알아냈다.
첫번째로 알아낸 사실은, 언리얼 엔진 문서는 생각보다 훨씬 친절하고 상세하다는 것이다. UE4 C++ 프로그래밍 입문 문서의 꽤 아래쪽에 몇가지 아주 중요한 사실이 적혀있다. (‘입문’이라고 무시하지 말자)
액터는 보통 가비지 컬렉팅되지 않습니다. 스폰 후에는 반드시 거기서
Destroy()
를 수동 호출해야 합니다. 즉시 삭제되는 것은 아니고, 다음 가비지 컬렉션 단계에서 지워질 것입니다.
이 부분이 중요한데, 앞서 말씀드렸듯이
Destroy()
를 호출한 액터는 다음 번 가비지 컬렉터가 실행되기 전까지는 제거되지 않기 때문입니다.UObject
가 삭제 대기중인지는IsPendingKill()
메서드를 사용해서 검사할 수 있습니다. 그 메서드가true
를 반환하는 경우, 오브젝트가 죽을 테니 사용하지 말아야겠다 생각하면 됩니다.
TArray
는 그 요소를 가비지 컬렉션 시킬 때 부가적인 혜택이 있습니다. 여기에는TArray
가UPROPERTY
마킹되어 있고,UObject
파생 포인터를 저장한다 가정합니다.
사실 TArray
에 UPROPERTY
를 붙여야 한다는 것은 조금만 생각해보면 당연한 것이다. 처음 문서에 “UPROPERTY
로 마킹되어 있거나 언리얼 엔진 컨테이너 클래스에 저장된 레퍼런스에만 적용됩니다.”라고 적혀있어서 헷갈렸다. 마치 둘중 하나만 만족하면 된다는 뜻으로 알아들었다.
중요한 것은 액터에 Destroy()
를 호출해도 다음 GC가 실행되기 전까지는 제거되지 않는다는 것이다. 여기서 다음 번 GC 실행이 무슨 의미인가 하면, 우리는 보통 쓰레기를 모아뒀다가 한번에 갖다 버린다. 여기서도 마찬가지로 일정 간격마다 GC를 수행한다. 그 간격은 프로젝트 세팅에서 설정할 수 있다.
기본값이 왜 저런 애매한 숫자인지는 나도 모르겠다. 하여튼 약 1분마다 GC가 이루어지기 때문에, 그 안에 액터가 삭제될 경우 다음 GC 전까지는 메모리에 남아있기 때문에 조심해야 한다. 위에 나와있듯이 그 여부는 IsPendingKill()
메서드로 알아낼 수 있다. 그러나 만약 나의 경우처럼 UPROPERTY
마킹이 불가능한 경우에는 애초에 포인터가 가리키는 메모리 자체가 유효하지 않을 수도 있기 때문에 TWeakObjectPtr
을 사용하여 유효성 검증을 하는 것이 좋다.
Comments