[UE4] 가비지 컬렉션에 관한 유의사항

2 minute read

언리얼 엔진은 보면 볼수록 참 대단하다는 생각이 든다. 그중에서도 가장 대단하다고 생각되는 것은 리플렉션 시스템과 가비지 컬렉터다. 오늘은 가비지 컬렉터에 대한 나의 삽질기를 얘기해볼까 한다.

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

위 문서에는 굉장히 중요한 얘기가 있다.

  1. AActor 또는 UActorComponent가 소멸되면 레퍼런스는 자동으로 null이 됩니다.
  2. 이 기능은 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 는 그 요소를 가비지 컬렉션 시킬 때 부가적인 혜택이 있습니다. 여기에는 TArrayUPROPERTY 마킹되어 있고, UObject 파생 포인터를 저장한다 가정합니다.

사실 TArrayUPROPERTY를 붙여야 한다는 것은 조금만 생각해보면 당연한 것이다. 처음 문서에 “UPROPERTY로 마킹되어 있거나 언리얼 엔진 컨테이너 클래스에 저장된 레퍼런스에만 적용됩니다.”라고 적혀있어서 헷갈렸다. 마치 둘중 하나만 만족하면 된다는 뜻으로 알아들었다.

중요한 것은 액터에 Destroy()를 호출해도 다음 GC가 실행되기 전까지는 제거되지 않는다는 것이다. 여기서 다음 번 GC 실행이 무슨 의미인가 하면, 우리는 보통 쓰레기를 모아뒀다가 한번에 갖다 버린다. 여기서도 마찬가지로 일정 간격마다 GC를 수행한다. 그 간격은 프로젝트 세팅에서 설정할 수 있다.

gc-settings

기본값이 왜 저런 애매한 숫자인지는 나도 모르겠다. 하여튼 약 1분마다 GC가 이루어지기 때문에, 그 안에 액터가 삭제될 경우 다음 GC 전까지는 메모리에 남아있기 때문에 조심해야 한다. 위에 나와있듯이 그 여부는 IsPendingKill() 메서드로 알아낼 수 있다. 그러나 만약 나의 경우처럼 UPROPERTY 마킹이 불가능한 경우에는 애초에 포인터가 가리키는 메모리 자체가 유효하지 않을 수도 있기 때문에 TWeakObjectPtr을 사용하여 유효성 검증을 하는 것이 좋다.

Categories:

Updated:

Comments