i_jemin

Archive

← 아카이브로

UniRx, UniTask, 제네릭 메타 데이터와 관련된 최적화 경험

유니티 IL2CPP의 제네릭 메타 데이터가 너무 커서 최적화를 시도한 경험이다.

프로젝트의 어셈블리는 다량의 일회성 스크립트와 중복되거나 불필요하게 복잡한 구현으로 비대해져 있었다. 일회성 스크립트에서도 UniRx를 사용하면서 가능한 제네릭 조합이 크게 늘었고, UniTask로 메서드를 비동기로 선언하면서 같은 함수의 시그니처도 늘어났다.

그 결과 어셈블리뿐 아니라 메모리에 로드되는 제네릭 타입의 메타 데이터가 커졌다. 런타임 코드와 메타 데이터가 커지면서 메모리는 파편화된 상태로 남았고, 타입 정보를 조회하는 시간도 길어졌다.

다음과 같은 해결 방안을 검토했다.

- 적극적인 코드 스트립핑
- 빠른 빌드 사용
- 명확한 목적으로만 리플렉션 사용
- 불필요하게 복잡한 코드 제거

먼저 코드 스트립핑으로 불필요한 코드와 제네릭 타입을 배제해야 한다. 이때 명확한 규율 없이 사용하던 리플렉션에서 예외가 발생하면 해당 사용처를 찾아 수정할 수 있다.

느린 빌드는 가능한 제네릭 조합의 글로벌 메타 데이터&머신코드를 미리 빌드한다. 메타 데이터와 빌드 크기는 커지지만, 필요할 때 지연 생성하는 방법에 비해 게임 시작 시간과 런타임 오버헤드를 줄인다.

반대로 빠른 빌드는 모든 제네릭 조합의 메타 데이터&머신코드를 미리 생성하지 않아 첫 로드의 메타 데이터 크기를 줄일 수 있다. 우리 프로젝트는 메타 데이터가 워낙 커서 빠른 빌드 설정이 필요했다.

UniTask는 비동기 메서드를 적극적으로 사용할 수 있다는 장점이 시그니처 증가의 단점보다 컸다. 또한 UniRx가 생성하는 제네릭과 메타 데이터의 비중이 훨씬 커서, UniTask를 정리하기보다 UniRx에 집중하는 편이 이득이었다.

그러나 프로젝트에서 이 방안들을 충분히 시행하지는 못했다. 기능 구현과 최적화·안정화의 책임이 나뉘어 있었고, 기능을 구현하는 단계에서 최적화와 코드 정리에 어느 정도 시간을 쓸지 공감대를 만들지 못했다. 빠른 구현을 위해 사용하던 리플렉션을 수정하는 데에도 이견이 컸다.

특히 리플렉션으로 어셈블리와 전체 타입을 통째로 로드하는 코드가 있었다. 빠른 빌드 상태에서 이 코드가 동작하면 순간적으로 매우 큰 메타 데이터를 생성하려 해 심각한 프리즈나 앱 크래시가 발생했다.

빠른 빌드를 유지하면서 예외가 발생하는 사용처를 수정해야 했지만, 변경에 합의하지 못해 전체 메타 데이터를 미리 빌드하는 설정을 유지했다.

이 경험에서 얻은 교훈은 프로젝트 초기에 제약을 분명하고 빈틈없이 설정해야 한다는 것이다. 설계자는 빠른 구현을 우선하는 과정에서 제약을 우회하고 코드가 복잡해질 가능성까지 고려해야 한다. 이 사례에서는 초기부터 높은 수준의 코드 스트립핑과 빠른 빌드를 활성화했어야 한다고 생각한다.

또한 기능 구현자도 최적화와 안정화의 책임을 함께 가져야 한다. 이를 별도 팀의 의무로만 두면, 그 팀에 충분한 지원과 협업 기반이 없는 한 개선을 추진하기 어렵다.