Version: 2021.3
언어: 한국어
Unity 아키텍처
.NET 프로파일 지원

Unity의 .NET 개요

Unity는 오픈 소스 .NET 플랫폼을 사용하여 Unity로 만드는 애플리케이션이 다양한 하드웨어 설정에서 실행될 수 있도록 합니다. .NET 플랫폼은 여러 언어와 API 라이브러리를 지원합니다.

스크립팅 백엔드

Unity에는 Mono와 IL2CPP(C++로 변환하는 중간 언어)라는 두 가지 스크립팅 백엔드가 있으며 이 둘은 각각 다른 컴파일 기술을 사용합니다.

  • Mono는 JIT(Just-In-Time) 컴파일을 사용하고 런타임 시점에 요청 시 코드를 컴파일합니다.
  • IL2CPP는 AOT(ahead-of-time)를 사용하며 실행되기 전에 전체 애플리케이션을 컴파일합니다.

JIT 기반 스크립팅 백엔드를 사용하는 이점은 컴파일 시간이 AOT보다 훨씬 더 빠르다는 점입니다.

기본적으로 Unity는 Mono를 지원하는 플랫폼에서 Mono 백엔드를 사용합니다. 애플리케이션용 플레이어를 빌드할 때 사용할 스크립팅 백엔드를 선택할 수 있습니다. 에디터를 통해 이를 수행하려면 Edit > Project Settings > Player로 이동하여 Other Settings 패널을 연 다음 Scripting Backend 드롭다운을 클릭하여 사용하고자 하는 백엔드를 선택합니다. 자세한 내용은 스크립팅 백엔드를 참조하십시오.

관리되는 코드 스트리핑

애플리케이션을 빌드하면 Unity가 컴파일한 다음 프로젝트에서 어셈블리(.DLLs)를 검색하여 사용하지 않는 코드를 감지하고 제거합니다. 스트립핑 코드의 이러한 프로세스는 빌드에 대한 최종 바이너리 크기를 줄여주지만 빌드 시간은 늘어납니다.

코드 스트리핑은 Mono를 사용할 때 기본적으로 비활성화되어 있지만 IL2CPP에 대해서는 비활성화될 수 없습니다. Unity에서 관리되는 스트리핑 레벨 속성을 사용하여 스트리핑하는 코드의 양을 제어할 수 있습니다.

이 속성을 변경하려면 Edit > Project Settings > Player로 이동하여 Other Settings 패널을 연 다음 Managed Stripping Level 드롭다운을 클릭하여 스트리핑 레벨을 선택합니다.

관리되는 스트리핑 레벨을 증가시키면 Unity는 더 많은 코드를 제거합니다. 이로 인해 특히 리플렉션을 사용하거나 런타임 시 코드를 생성하는 경우 애플리케이션이 의존하는 코드를 Unity가 제거할 수도 있는 위험이 증가합니다.

코드의 특정 요소에 대한 주석을 사용하여 Unity의 코드 스트리핑을 방지할 수 있습니다. 자세한 내용은 관리되는 코드 스트리핑을 참조하십시오.

가비지 컬렉션

Unity는 Mono 백엔드와 IL2CPP 백엔드 모두에 대해 Boehm 가비지 컬렉터를 사용합니다. Unity는 기본적으로 증분 모드를 사용합니다. Unity에서는 증분 모드를 권장하지만 증분 모드를 비활성화하여 stop-the-world 방식의 가비지 컬렉션을 사용할 수 있습니다.

증분 모드와 stop-the-world 방식 간에 토글하려면 Edit > Project Settings > Player로 이동하여 Other Settings 패널을 열고 Use incremental GC 체크박스를 클릭합니다. 증분 모드에서는 Unity의 가비지 컬렉터가 제한된 시간 동안만 실행되며 반드시 한 번에 모든 오브젝트를 수집하지는 않습니다. 이는 여러 프레임에 걸쳐 오브젝트를 수집하는 데 걸리는 시간을 분산시키며 불안정성과 CPU 스파이크를 줄여줍니다. 자세한 내용은 관리되는 메모리를 참조하십시오.

애플리케이션에서 할당량과 가능한 CPU 스파이크 수를 확인하려면 Unity 프로파일러를 사용해야 합니다. 또한 GarbageCollector API를 사용하여 플레이어에서 가비지 컬렉션을 완전히 비활성화할 수 있습니다. 컬렉터가 비활성화되면 메모리를 초과하여 할당하지 않도록 유의합니다.

.NET 시스템 라이브러리

Unity는 많은 플랫폼을 지원하며 플랫폼에 따라 여러 스크립팅 백엔드를 사용할 수도 있습니다. .NET 시스템 라이브러리는 몇몇 경우에 올바르게 작동할 수 있도록 플랫폼별 구현을 해야 합니다. Unity에서는 가능한 한 많은 .NET 생태계를 지원하려고 최선을 다하고 있지만 예외적으로 Unity에서 확실히 지원하지 않는 .NET 시스템 라이브러리가 일부 있습니다.

Unity는 모든 Unity 버전에서 .NET 시스템 라이브러리에 대한 성능이나 할당을 보장하지 않습니다. 일반적으로 Unity는 .NET 시스템 라이브러리에서 어떠한 성능 회귀를 수정하지 않습니다.

Unity는 System.Drawing 라이브러리를 지원하지 않으며 이 라이브러리가 모든 플랫폼에서 작동한다는 보장을 하지 않습니다.

Mono 스크립팅 백엔드가 사용하는 JIT 컴파일은 애플리케이션 런타임 동안 동적 C#/.NET 중간 언어(IL) 코드 생성을 방출할 수 있게 해줍니다. IL2CPP 스크립팅 백엔드가 사용하는 AOT 컴파일은 동적 코드 생성을 지원하지 않습니다.

이를 고려해야 하는 중요한 이유는 타사 라이브러리를 사용할 때 JIT와 AOT에 대한 코드 경로가 다르거나 동적으로 발생된 코드에 의존하는 코드 경로를 사용할 수도 있기 때문입니다. 런타임 시 코드를 생성하는 방법에 대한 자세한 내용은 Microsoft의 ModuleBuilder 문서를 참조하십시오.

Unity에서 다수의 .NET API 프로파일을 지원하지만 다음의 사유로 인해 모든 새로운 프로젝트에 대해서는 .NET 표준 API 호환성 레벨을 사용해야 합니다.

  • .NET 표준은 더 작은 API 표면이므로 구현이 더 작습니다. 이렇게 하면 최종 실행 가능한 파일의 크기가 줄어듭니다.
  • .NET 표준은 더 나은 크로스 플랫폼을 지원하므로 코드가 모든 플랫폼에서 작동할 가능성이 더 높습니다.
  • 모든 .NET 런타임은 .NET 표준을 지원하므로 .NET 표준을 사용할 때 더 많은 VM/런타임 환경(예: .NET 프레임워크, .NET Core, Xamarin, Unity)에서 코드가 작동합니다.
  • .NET 표준은 컴파일 시간으로 더 많은 오류를 이동시킵니다. .NET 프레임워크에 있는 많은 API는 컴파일 시점에 사용할 수 있지만 일부 플랫폼에는 런타임 시점에 예외가 발생하는 구현이 있습니다.

예를 들어 오래된 기존 애플리케이션에 대한 지원을 제공해야 하는 경우 다른 프로파일을 사용하면 유용할 수 있습니다. Api 호환성 레벨 설정을 변경하려면 Edit > Project Settings > Player로 이동합니다. Other Settings 헤딩 아래에서 Api Compatibility Level을 원하는 설정으로 변경합니다.

자세한 내용은 .NET 프로파일 지원을 참조하십시오.

타사 .NET 라이브러리 사용

다양한 Unity 설정 및 플랫폼에서 광범위하게 테스트된 타사 .NET 라이브러리만 사용해야 합니다.

타사 라이브러리에서는 JIT와 AOT 코드 경로에 대한 성능 특성이 크게 다를 수도 있습니다. AOT는 일반적으로 시작 시간을 단축하므로 더 큰 애플리케이션에 적합하지만 컴파일된 코드를 수용하기 위해 바이너리 파일 크기를 늘립니다. 또한 AOT는 개발 과정에서 빌드하는 데 시간이 더 오래 걸립니다.

JIT는 실행 중인 플랫폼을 기반으로 런타임 시 조정되므로 잠재적으로 애플리케이션 시작 시간을 더 길게 투자하여 실행 성능을 향상시킬 수 있습니다. 그런 면에서 에디터와 타겟 플랫폼 모두에서 애플리케이션을 프로파일링해야 합니다. 자세한 내용은 프로파일러 개요를 참조하십시오.

사용하는 스크립팅 백엔드, .NET 버전, 프로파일에 따라 성능 특성이 달라질 수도 있으므로 모든 타겟 플랫폼에서 .NET 시스템 라이브러리의 사용을 프로파일링해야 합니다.

타사 라이브러리를 검토할 때는 다음 부분을 고려하십시오.

  • 호환성: 타사 라이브러리는 일부 Unity 플랫폼 및 스크립팅 백엔드와 호환되지 않을 수 있습니다.
  • 성능 : 타사 라이브러리는 다른 .NET 런타임 대비 Unity의 성능 특성이 크게 다를 수 있습니다.
  • AOT 바이너리 크기 : 타사 라이브러리는 라이브러리가 사용하는 종속성 개수로 인해 AOT 바이너리 크기가 크게 늘어날 수 있습니다.

C# 리플렉션 오버헤드

Mono와 IL2CPP는 모든 C# 리플렉션(System.Reflection) 오브젝트를 내부적으로 캐싱하며 설계상 Unity는 이러한 오브젝트에 대해 가비지 컬렉션을 수행하지 않습니다. 이러한 동작을 수행하면 애플리케이션의 생애 주기 동안 가비지 컬렉터가 캐싱된 C# 리플렉션 오브젝트를 지속적으로 스캔하며 이로 인해 불필요하고 잠재적으로 중대한 가비지 컬렉터 오버헤드가 발생합니다.

가비지 컬렉터 오버헤드를 최소화하려면 애플리케이션에서 Assembly.GetTypesType.GetMethods() 등과 같은 메서드를 피해야 합니다. 이러한 메서드는 런타임 시점에 많은 C# 리플렉션 오브젝트를 생성합니다. 대신에 에디터에서 어셈블리를 스캔하여 필요한 데이터를 찾고 런타임 시점에 사용할 수 있도록 직렬화하거나 코드를 생성해야 합니다.

UnityEngine.Object 특수 동작

UnityEngine.Object는 상응하는 네이티브 C++ 오브젝트에 연결되어 있으므로 Unity 내에서 특수 타입의 C# 오브젝트입니다. 예를 들어 카메라 컴포넌트를 사용하면 Unity는 오브젝트 상태를 C# 오브젝트 자체가 아닌 해당 오브젝트의 네이티브 C++ 컴포넌트에 저장합니다.

Unity에서는 현재 UnityEngine.Object의 인스턴스로 C# WeakReference 클래스를 사용하는 것을 지원하지 않습니다. 따라서 로드된 에셋을 레퍼런스하기 위해 WeakReference를 사용해서는 안 됩니다. WeakReference 클래스에 대한 자세한 내용은 Microsoft의 WeakReference 클래스 문서를 참조하십시오.

Unity C# 및 Unity C++가 공유하는 UnityEngine 오브젝트

Object.Destroy 또는 Object.DestroyImmediate와 같은 메서드를 사용하여 UnityEngine.Object 파생 오브젝트를 파괴하면 Unity는 네이티브 카운터 오브젝트를 파괴(언로드)합니다. 가비지 컬렉터가 메모리를 관리하므로 명시적 호출로 C# 오브젝트를 파괴할 수는 없습니다. 관리되는 오브젝트에 더 이상 레퍼런스가 없으면 가비지 컬렉터가 C# 오브젝트를 수집 후 파괴합니다.

애플리케이션이 파괴된 UnityEngine.Object에 다시 액세스하려고 하는 경우 Unity는 대부분의 타입에 대한 네이티브 오브젝트를 다시 생성합니다. 이러한 재생성 동작에는 MonoBehaviourScriptableObject라는 두 가지 예외가 있으며 이 두 오브젝트가 한번 파괴되면 Unity는 이를 다시 로드하지 않습니다.

MonoBehaviour와 ScriptableObject는 같음(==)과 같지 않음(!=) 연산자를 오버라이드합니다. 파괴된 MonoBehaviour 또는 ScriptableObject를 null와 비교하는 경우 관리되는 오브젝트가 여전히 존재하고 아직 가비지 컬렉션이 수행되지 않았을 때 해당 연산자는 true를 반환합니다.

??와 ?. 연산자를 오버로드할 수 없으므로 이러한 연산자는 UnityEngine.Object에서 파생되는 오브젝트와 호환될 수 없습니다. 관리되는 오브젝트가 여전히 존재하는 동안 파괴된 MonoBehaviour 또는 ScriptableObject에서 해당 연산자가 사용되는 경우 같음과 같지 않음 연산자와 동일한 결과를 반환하지 않습니다.

async 작업 및 await 작업의 제약 사항

Unity API는 스레드에 안전하지 않으므로 UnitySynchronizationContext 안에서만 async 작업과 await 작업을 사용해야 합니다. Async 작업은 호출되면 오브젝트를 할당하기도 하므로 과도하게 사용할 경우 성능상의 문제가 발생할 수도 있습니다.

Unity는 기본 SynchronizationContext와 커스텀 UnitySynchronizationContext를 덮어쓰고 기본적으로 편집 모드와 플레이 모드로 메인 스레드에서 모든 작업을 실행합니다. Async 작업을 사용하려면 반드시 Task.Run API로 자체 스레드를 수동으로 생성하고 처리하며 해당 Unity 버전 대신 기본 SynchronizationContext를 사용해야 합니다.

플레이 모드를 종료하면 Unity는 관리되는 스레드에서 실행한 async 작업을 자동으로 멈추지 않습니다. 플레이 모드 진입과 종료 이벤트를 수신하여 수동으로 작업을 멈추려면 EditorApplication.playModeStateChanged를 사용해야 합니다. 이러한 방식을 채택할 경우 컨텍스트를 다시 UnitySynchronizationContext로 마이그레이션하지 않으면 Unity의 스크립팅 API 대부분을 사용할 수 없습니다.

개발 빌드에서 멀티 스레드 코드로 Unity API를 사용하려고 하는 경우 Unity는 다음과 같은 오류 메시지를 표시합니다.


UnityException: Internal_CreateGameObject는 메인 스레드에서만 호출될 수 있습니다. \

생성자 및 필드 이니셜라이저는 씬을 로드할 때 로딩 스레드에서 실행됩니다. \

생성자나 필드 이니셜라이저에서 이 함수를 사용하지 말고, 그 대신 초기화 코드를 Awake 또는 Start 함수로 이동하십시오.

성능상의 이유로 Unity는 비 개발 빌드에서 멀티 스레드 동작에 대해 확인하지 않으며 이러한 오류를 라이브 빌드에 표시하지 않습니다. 즉 Unity는 라이브 빌드상에서 멀티 스레드 코드 실행을 방지하지 않으며 다수의 스레드를 사용하는 경우 랜덤 크래시와 그 외 예측 불가능한 오류가 발생할 수 있습니다. 이러한 이유로 Unity는 멀티스레딩을 사용하지 않는 편을 권장합니다.

멀티스레딩의 이점을 안전하게 이용하려면 C# 잡 시스템을 사용해야 합니다. 잡 시스템은 다수의 스레드를 안전하게 사용하여 병렬로 잡을 실행하며 멀티스레딩의 성능에 대한 이점을 얻습니다. 자세한 내용은 멀티스레딩이란?을 참조하십시오.

Unity 아키텍처
.NET 프로파일 지원