본문 바로가기

Study/Unity Engine

[Unity] 유니티 프레임 워크, .NET과 GC

우선 .NET에 대해 알아야한다.

.NET은 소프트웨어 개발을 위한 도구, 라이브러리 및 언어를 포함하는 플랫폼으로, 이를 통해 다양한 디바이스와 운영체제에서 실행 가능한 애플리케이션 제작이 가능하다.

 

기존 .NET Framework의 주요 한계는 플랫폼 간 코드 공유가 제한적이었다는 점이다. 특히 .NET Framework는 Windows 운영체제를 중점적으로 지원했기 때문에, 리눅스 및 macOS와 같은 다른 운영체제로 애플리케이션을 포팅하려면 코드 수정과 테스트 과정이 필수적이었다.

이러한 제약을 극복하기 위해, 마이크로소프트는 .NET Core를 출시하였다. .NET Core는 오픈소스이며 크로스 플랫폼을 지원해 개발자들이 Windows, Linux, macOS 등 다양한 환경에서 동일한 코드를 실행할 수 있게 되었다.

 

현재 .NET 플랫폼은 .NET 5, .NET 6와 같이 통합된 단일 코드 기반의 오픈소스 개발 플랫폼으로 발전하였다. 이 플랫폼은 과거 .NET Framework와 .NET Core를 통합하여 더 간결하고 강력한 생태계를 제공하며, 클라우드, 웹, 데스크톱, 모바일, IoT 등 다양한 애플리케이션 개발을 지원한다.

https://dotnet.microsoft.com

 

https://dotnet.microsoft.com/ko-kr/learn/dotnet/what-is-dotnet-framework


.NET을 사용함으로써 얻을 수 있는 장점들은 다음과 같다

1) 생산성 및 개발 효율성

  • 풍부한 API: .NET 표준 라이브러리와 Unity API의 조합으로, 복잡한 기능을 간단하게 구현할 수 있다.
  • IDE 통합: Visual Studio와 Rider 같은 강력한 개발 도구를 활용해 디버깅, 코드 자동 완성, 리팩토링 등의 기능을 지원받을 수 있다.
  • 가독성 높은 코드: C# 언어는 간결하고 직관적인 문법을 제공하여 유지보수와 협업에 유리하다.

2) 멀티플랫폼 지원

  • .NET의 크로스 플랫폼 특성과 Unity의 자체 기술(예: IL2CPP)은 Windows, macOS, Android, iOS, 콘솔 등 다양한 플랫폼에서 동일한 코드를 재사용할 수 있도록 해준다.

3) 성능 및 최적화

  • .NET의 가비지 컬렉터(GC)는 메모리 관리의 복잡성을 줄여주며, Unity는 이를 기반으로 최적화된 메모리 관리 전략을 제공한다.
  • AOT 컴파일러를 통해 배포 빌드의 성능을 극대화하며, 게임의 성능을 향상시킬 수 있다.

4) 커뮤니티 및 생태계

  • .NET과 Unity는 모두 대규모 커뮤니티와 풍부한 문서를 보유하고 있어 학습과 문제 해결이 용이하다.
  • NuGet 패키지와 Unity Asset Store 같은 풍부한 외부 리소스를 통해 개발에 필요한 기능을 쉽게 확장할 수 있다.

 

유니티는 접근성, 효율성, 확장성을 중심으로 게임 개발 환경을 제공하고자 한다.

  • 접근성: Unity는 초보 개발자부터 숙련된 전문가까지 누구나 쉽게 사용할 수 있는 직관적인 인터페이스와 워크플로를 제공
  • 효율성: 생산성을 극대화하기 위해 빠른 프로토타이핑과 빌드 프로세스를 지원하며, 다양한 플랫폼으로의 빠른 배포를 지향
  • 확장성: Unity는 모듈화된 설계와 다양한 커스터마이징 옵션으로, 게임뿐만 아니라 VR/AR, 시뮬레이션, 영화 제작 등의 다양한 산업군에서 활용될 수 있도록 확장성을 제공

 

반면 유니티는 다음과 같은 내용들을 지양한다.

  • 과도한 복잡성: Unity는 간단한 워크플로와 도구를 통해 개발 복잡성을 줄이는 것을 목표로 합니다. 이를 위해 복잡한 설정이나 코딩 과정을 최소화하려 한다.
  • 플랫폼 의존성: 특정 플랫폼에 종속되지 않도록 멀티플랫폼 환경을 지원하며, 개발자가 특정 환경에 구애받지 않고 유연하게 작업할 수 있도록 한다.
  • 불필요한 성능 저하: Unity는 성능 최적화를 중요시하며, 불필요한 성능 병목 현상이 발생하지 않도록 메모리 관리와 컴파일 방식을 개선하고 있다.
  • 폐쇄적 생태계: Unity는 커뮤니티 중심의 생태계를 중요하게 여기며, 외부 라이브러리나 플러그인을 자유롭게 통합할 수 있는 개방형 구조를 지향한다.

유니티는 게임 개발에 있어 빠르고 효율적인 개발 환경을 제공하기 위해 .NET 프레임워크를 사용한다. 이는 .NET이 지향하는 바와 유니티가 지향하는 바가 일치한다는 것을 보여준다.

  • 강력한 언어 지원: C#과 같은 고급 언어는 객체 지향 프로그래밍(OOP)을 지원하며, 안정성과 유연성을 제공해 복잡한 게임 로직을 구현하기 용이하다.
  • 생산성 향상: .NET이 제공하는 표준 라이브러리와 API는 일반적인 작업(파일 입출력, 네트워크 통신 등)을 단순화해 개발 시간을 줄여준다.
  • 멀티플랫폼 지원: .NET은 플랫폼 간 호환성을 염두에 두고 설계되었으며, Unity의 다양한 플랫폼 지원 전략과 잘 맞아떨어진다.
  • 성능 최적화: .NET은 JIT(Just-In-Time) 컴파일과 AOT(Ahead-Of-Time) 컴파일을 조합하여 런타임 성능과 배포 효율성을 동시에 제공한다.

즉, 유니티에서 .NET을 사용하는 이유는 생산성, 멀티플랫폼 지원, 성능 최적화, 커뮤니티 생태계 등과 같은 이점을 극대화하기 위함이다. 유니티는 이러한 기반 위에서 개발의 접근성, 효율성, 확장성을 강화하는 방향으로 발전하며, 복잡성과 비효율성, 플랫폼 종속성 등을 지양한다. 이를 통해 개발자들이 창의적인 작업에 집중할 수 있는 환경을 제공하고자 한다.


이제 내부를 살펴보자.

유니티에는 두 가지 스크립팅 백엔드가 존재한다. 바로 Mono와 IL2CPP다.

 

Mono

  • JIT(Just In Time) 컴파일을 사용하는 Mono는 런타임 시점에 요청받은 코드를 컴파일하는 오픈소스 .NET이다.
  • JIT 컴파일을 사용하므로 상대적으로 빠른 빌드 속도와 개발 중 빠른 반복작업 그리고 스크립트 변경사항을 즉시 테스트할 수 있다는 장점이 있다. 또한 런타임에 디버깅이 용이하고, 개발 중 빠른 수정과 테스트에 적합하다.
  • 다만, iOS와 같은 일부 플랫폼에서는 Mono대신 IL2CPP를 사용해야 하고, JIT 컴파일로 인해 성능이 정적으로 컴파일된 코드보다 낮을 수 있다.

IL2CPP

  • AOT(Ahead Of Time)컴파일을 사용하는 IL2CPP는 Unity에서 자체적으로 개발한 스크립팅 백엔드로, C# 코드를 C++ 코드로 변환한 후 컴파일한다.
  • 즉, 빌드 시점에 모든 코드를 컴파일하며, 런타임 성능이 뛰어나고 최적화된 네이티브 코드로 실행된다. 또한 JIT이 금지된 플랫폼(iOS)에서 필수적으로 사용되며, Unity에서 지원하는 거의 모든 플랫폼에서 사용이 가능하다. 이러한 IL2CPP를 사용하면 코드가 C++로 변환되기 때문에 디컴파일이 어렵고, 보안이 강화된다.
  • 다만, 디버깅이 Mono보다 어렵고, 빌드 프로세스가 복잡하고 특히 Mono에 비해 컴파일 시간이 오래 걸린다.

이 두가지 백엔드 모두, Boehm-Demers-Weiser GC를 사용한다.

Boehm-Demers-Weiser GC는 C++의 대표적인 GC이다.

 

이 Boehm-Demers-Weiser GC는 대표적인 Conservative GC(보수적 가비지 컬렉터)이다.

  • 보수적 가비지 컬렉터는 힙(Heap) 메모리에서 어떤 값이 참조(reference)인지 아닌지를 명시적으로 알 수 없는 경우에도 안전하게 작동하는 GC를 말한다.
  • 즉, 힙, 스택, 전역 변수 영역을 검색하여 포인터로 추정되는 값을 보존하여 실제로 이 추정 값이 포인터가 아닐 가능성이 있지만, 포인터 데이터 손실을 방지하기 위해 포인터일 가능성이 있는 값까지 포함하여 관리한다. 이는 명시적 포인터 타입이 없는 C/C++ 언어의 특성과 호환되도록 설계되었음을 확인할 수 있다.

Boehm-Demers-Weiser GC의 주요 기능은 다음과 같다.

  • 자동 메모리 관리: 프로그래머가 malloc으로 할당한 메모리를 명시적으로 free하지 않아도, 더 이상 사용되지 않는 객체를 자동으로 해제한다.
  • 투명성: 기존의 코드(특히 C/C++)에 거의 변경을 가하지 않고 통합할 수 있다.
  • 호환성: 표준 C/C++ 프로그램에서 동작하며, 프로그래머가 원하는 경우 명시적으로 GC를 트리거할 수도 있다.
  • 병렬 및 멀티스레딩 지원: 병렬화된 환경에서도 작동하며, 멀티스레드 환경에서 성능 최적화 기능을 제공한다.

 

Boehm-Demers-Weiser GC의 동작 원리

  1. 루트 집합(Root Set) 탐색
    프로그램에서 참조 가능한 모든 객체(루트)를 찾아낸다.
    이때, 스택, 전역 변수, CPU 레지스터 등을 검색하여 루트 집합을 생성한다.
  2. 포인터 추정(Conservative Pointer Guessing)
    힙, 스택 등에서 포인터로 보이는 모든 값을 추정한다.
    정확한 타입 정보가 없으므로, 포인터로 보이는 값을 "보수적"으로 처리한다.
    만일, 가비지 데이터가 포인터처럼 보이면 메모리가 해제되지 않을 가능성이 생긴다(이는 보수적 GC의 단점 중 하나다).
  3. 도달 가능한 객체(Reachable Object) 추적
    루트 집합에서 시작해 도달 가능한 객체를 추적(mark)한다.
    도달하지 않는 객체는 가비지로 간주되어 회수(sweep)된다.
  4. 할당 및 해제
    메모리를 동적으로 할당하고, 더 이상 필요하지 않은 메모리를 해제한다.
    사용자가 malloc 또는 유사한 함수로 메모리를 할당한 경우에도 이를 관리한다.

이를 통해, 기존 C/C++와 같이 수동 메모리 관리를 사용하는 기존 코드베이스와 쉽게 통합할 수 있으며, 개발 생산성을 향상시켜주고, 표준 라이브러리와 호환성이 높다는 장점이 있지만, 보수적 GC의 단점인, 부정확성 즉, 보수적 접근 방식으로 인해 포인터가 아닌 값을 잘못된 포인터로 인식할 수 있어, 이로 인해 일부 가비지가 회수되지 않을 수 있다.

또한 수동 메모리 관리에 비해 메모리 관리 오버헤드가 증가할 수 있으며, GC 작업 중 애플리케이션의 실행이 잠시 중단될 수 있으므로, 실시간 시스템에는 적합하지 않을 수 있다.

 

즉, 유니티는 .NET 플랫폼을 기반으로 게임 개발을 지원하며, Mono와 IL2CPP 두 가지 스크립팅 백엔드를 활용해 다양한 플랫폼과 개발 요구를 충족시킨다. .NET은 C#과 같은 강력한 언어와 표준 라이브러리를 통해 생산성과 개발 효율성을 제공하며, 멀티플랫폼 지원과 성능 최적화의 장점을 극대화한다. Mono는 JIT 컴파일을 통해 빠른 반복 작업과 디버깅이 용이한 반면, IL2CPP는 AOT 컴파일을 사용해 뛰어난 런타임 성능과 보안을 제공한다. 유니티는 Boehm-Demers-Weiser GC를 사용해 자동 메모리 관리를 구현하며, 이를 통해 개발 생산성을 높이지만 보수적 GC의 단점으로 인해 일부 메모리 관리 오버헤드와 부정확성이 존재할 수 있다. 이러한 구조는 개발자들에게 접근성, 효율성, 확장성을 제공하는 동시에 복잡성과 플랫폼 종속성을 최소화해 창의적이고 유연한 개발 환경을 제공한다.