Search

유니티 빌드

class
구조
상태
완료
날짜
목차

유니티의 빌드

유니티에서의 빌드 과정은 매우 복잡합니다.
텍스처와 오디오의 압축 포맷 설정 외에도 Define 설정, 네이밍 설정 등 다양한 빌드 과정을 모두 설명하려면, 한 권의 책으로는 부족할 것 같습니다.
따라서, 이곳에서는 빌드의 기초인 유니티가 어떻게 컴파일을 진행하는지 알아볼 것입니다.

빌드과정

유니티에서 빌드가 진행되는 과정을 대략적으로 살펴보기에 앞서, 기본적인 빌드 과정을 설명하겠습니다.
일반적으로 MONO 빌드가 아닌 IL2CPP(AOT) 빌드를 사용하기 때문에, 이 과정에 대해 설명하겠습니다.
1.
C# 컴파일러 진행(로즐린) -> IL(Intermediate Language) 코드로 컴파일
a.
유니티에서 C# 소스 코드는 먼저 컴파일러에 의해 중간 언어(IL) 코드로 컴파일됩니다. 이 과정은 C# 코드를 IL 코드로 변환합니다.
2.
IL 코드를 C++ 코드로 변환 (AOT)
a.
IL2CPP 프로세스는 IL 코드를 C++ 코드로 변환합니다. 이 단계는 "컴파일"이라기보다는 "변환" 혹은 "트랜스파일" 과정에 더 가깝습니다. 이 과정에서 유니티는 모든 IL 코드를 C++ 코드로 변환하여, JIT 과정을 겪지 않게 합니다.
3.
C++ 코드 컴파일
a.
변환된 C++ 코드는 해당 플랫폼의 네이티브 컴파일러를 사용하여 컴파일됩니다. C++ 컴파일러가 C++ 소스 코드를 직접 네이티브 바이너리 코드로 컴파일한다고 이해하는 것이 더 정확합니다. 이 과정을 통해 실행 가능한 네이티브 코드(기계어 코드)가 생성됩니다.
4.
링킹
a.
컴파일된 코드는 필요한 모든 외부 라이브러리와 함께 링크됩니다. 이 단계에서는 실행 파일이나 라이브러리가 최종적으로 생성되며, 모든 의존성이 포함됩니다.
5.
포스트 프로세싱
a.
이 단계에서는 플랫폼별 설정, 최적화 및 기타 필요한 사후 처리 작업이 이루어집니다. 예를 들어, 앱의 아이콘 설정, 퍼미션 설정, 코드 사이닝 등이 포스트 프로세싱에 포함될 수 있습니다.
6.
빌드 파일 제작 완성
a.
모든 이전 단계를 거친 후, 최종 빌드 파일이 생성됩니다. 이 파일은 특정 플랫폼에서 실행될 준비가 완료된 상태입니다.

C# 컴파일러 진행(로즐린)

유니티에서는 Roslyn을 사용하여, C# 코드를 IL(Intermediate Language) 코드로 컴파일 합니다.
가끔 혼동하는 분들이 있지만, C#은 인터프리터 언어(Interpreter Language)가 아닙니다. 이는 JIT 특성으로 인해, 인터프리터 언어와 비슷한 특징을 갖을 수는 있지만 C#은 명확한 컴파일 과정을 거치는 컴파일 언어입니다. 하이브리드 언어라고도 불릴 수 있지만, 인터프리터 언어는 아닙니다.
Roslyn은 마이크로소프트에서 제공하는 컴파일러입니다. 유니티가 이를 사용하는 이유는 여러 가지가 있겠지만, 결국 이는 오픈소스이기 때문입니다.
그럼 왜 C#은 C++처럼 바로 네이티브 코드(기계어 코드)를 만들지 않고 IL코드로 컴파일할까요?
이는 플랫폼 독립성 때문입니다. 네이티브 코드로 직접 컴파일하면 해당 운영체제(플랫폼)에 종속되지만, IL 코드는 그렇지 않습니다.
이를 크로스 플랫폼을 지원한다고 합니다.
이것을 가능하게 하는 것은 JIT 컴파일러가 런타임에서 IL 코드를 네이티브 코드로 컴파일하여 실행하기 때문입니다.
JIT 컴파일러는 운영체제에 맞게 IL코드를 네이티브 코드로 컴파일합니다. 따라서 IL코드는 플랫폼에 독립적으로 컴파일될 수 있습니다.
이는 인터프리터 언어의 특성과 유사하지만, C#을 인터프리터 언어로 오해하면 안됩니다.

IL코드를 C++로 변환 AOT

유니티에서는 IL코드를 C++로 변환하는 과정이 있습니다.
이 과정은 JIT 컴파일러를 사용하지 않기 위한 것입니다.
JIT 컴파일러는 런타임 도중 IL코드를 네이티브 코드로 컴파일하는 과정을 포함하고 있습니다.
따라서, 기본적으로 네이티브 코드로 변환된 C++ 컴파일 네이티브 코드보다 속도가 느릴 수밖에 없습니다.
유니티는 IL코드를 C++ 코드로 변환한 다음 다시 컴파일하는 과정을 거칩니다.
IL코드를 C++로 변환하는 이 과정을 AOT(ahead-of-time)이라고 합니다. 이는 JIT(just-in-time)과 완전히 반대되는 개념입니다.
이 부분을 이해하면, 유니티 C#에서 리플렉션을 사용하지 못 하는 이유를 이해할 수 있습니다. 더 정확히 얘기하면, System.Reflection.Emit 기능을 사용할 수 없습니다. (메타파일을 읽어서 사용하는 기능인 리플렉션은 사용할 수 있습니다.) (ex: 클래스 인스턴스 동적 생성, 해당 클래스의 변수 이름에 해당하는 변수에 값 넣기 등등) System.Reflection.Emit은 런타임에 동적으로 새로운 타입, 메서드, 어셈블리 등을 생성할 수 있는 기능을 제공합니다. 이는 JIT 컴파일러가 런타임에 IL 코드를 네이티브 코드로 변환할 수 있을 때 유용합니다. 그러나 IL2CPP는 빌드 시 모든 코드를 네이티브 코드로 변환하므로, 런타임에 코드를 동적으로 생성하거나 수정하는 System.Reflection.Emit의 기능을 지원하지 않습니다.

C++ 코드 컴파일

AOT로 C++변환한 코드를 C++ 컴파일러가 C++ 소스 코드를 직접 네이티브 바이너리 코드로 컴파일하는 과정입니다.

링킹

생성된 오브젝트 파일들과 필요한 모든 외부 라이브러리는 최종 실행 파일을 만들기 위해 링킹 과정을 거칩니다.
유니티에서는 이 단계가 자동으로 처리됩니다.
정적 링킹 : 필요한 라이브러리가 최종 실행 파일 내에 직접 포함됩니다. 이 방식은 실행 파일의 크기를 증가시키지만, 외부 의존성 없이 실행 가능합니다.
동적 링킹 : 실행 시간에 필요한 라이브러리가 로드됩니다. 이 방식은 실행 파일의 크기를 줄이고, 라이브러리를 공유할 수 있게 하지만, 해당 라이브러리가 시스템에 설치되어 있어야 합니다.
동적 링킹 파일이란 dll 파일을 의미합니다. 여기서는 동적 링킹 파일에 대해 간략하게 설명했지만, 이를 활용하면 c++.dll 파일을 사용해 유니티를 c++ 언어로 실행시킬 수 있습니다. 다만, IOS에서는 기본적으로 동적 링킹을 허용하지 않습니다. 따라서 DLL 파일을 사용하기 위해서는 정적 링킹을 기본으로 하는 것이 좋습니다.

포스트 프로세싱

빌드 과정의 마지막 부분으로, 최종 빌드 파일에 대해 추가적인 처리를 수행하는 단계입니다.
포스트 프로세싱은 유니티가 처리하는 부분과 개발자가 직접 처리해야 하는 부분이 합쳐져 있습니다. 예를 들어, 최적화나 압축 같은 작업은 유니티에서 직접 수행할 수 있습니다.
그러나 특정 파일을 압축하거나 코드 서명, 플랫폼 별 설정 적용 등의 작업은 포스트 프로세싱 스크립트를 사용거나 개발자가 유니티 외부에서 직접 수행해야 합니다.

포스트 프로세싱에서 수행될 수 있는 작업들

최적화 : 빌드 파일의 성능 최적화를 수행할 수 있습니다. 이는 로딩 시간 감소, 메모리 사용 최적화, 파일 크기 축소 등을 포함할 수 있습니다.
압축 : 게임 또는 애플리케이션의 데이터 파일을 압축하여 배포 크기를 줄일 수 있습니다.
코드 서명 : 특정 플랫폼에서 요구하는 코드 서명 과정을 수행합니다. 예를 들어, iOS 앱은 Apple의 코드 서명 인증서를 사용하여 서명되어야 App Store에 배포될 수 있습니다.
사실 이 부분은 XCode에서 실행되므로 빌드 과정이 단순하지 않습니다. 유니티 빌드 파일을 iOS 플랫폼에 맞게 재빌드하는 과정이 필요합니다. 이는 포스트 프로세싱의 일부라고 볼 수 있습니다.
앱 스토어 제출 준비 : 플랫폼의 앱 스토어 제출 요구 사항에 맞추어 메타데이터 설정, 아이콘 및 스플래시 이미지 준비, 설명서 및 마케팅 자료 준비 등을 포함할 수 있습니다.
플랫폼별 설정 적용 : 특정 플랫폼에 맞는 설정을 적용합니다. 예를 들어, 안드로이드의 경우 매니페스트 파일 수정, iOS의 경우 Info.plist 설정 조정 등이 있습니다.
개발자는 이 부분을 외부에서 plist의 설정을 직접 조정해야 합니다.

빌드 파일 제작 완성

최종 빌드 파일이 생성됩니다.

C#과 C,C++ 컴파일 과정과의 비교

컴퓨터 공학과에서 배운 기존의 C 컴파일 과정과 유니티 C#의 컴파일 과정은 큰 차이가 있었습니다.
C,C++에서 기본적으로 이루어지는 전처리기, 컴파일, 어셈블러 이 과정이 C#에선 생략된게 많습니다.

1. 전처리기

역할 : C#에는 전통적인 C/C++ 스타일의 전처리기 지시문("#define", "#if" 등)을 사용하는 것과 직접적으로 동일한 전처리기가 존재하지 않습니다. 대신, 유니티에서는 컴파일러 지시문을 사용하여 특정 코드 블록이 컴파일 시간에 포함되거나 제외되도록 할 수 있습니다.
예시: #if UNITY_EDITOR, #endif 지시문을 사용하여 에디터 코드와 게임 코드를 구분할 수 있습니다.
전처리기 과정이 없기 때문에 c에서 i 파일로 변환되는 과정은 없습니다.

2. 컴파일러, 어셈블러

역할 : C# 코드를 중간 언어(IL, Intermediate Language) 코드로 변환합니다. 유니티에서는 Microsoft의 Roslyn 컴파일러 또는 Mono 컴파일러를 사용하여 이 작업을 수행합니다.
그러나 AOT 과정을 통해 C#에서 C++ 컴파일러로 변환하는 것도 가능합니다. 단, C++ 컴파일러 과정에는 컴파일 → 어셈블러 → 네이티브 언어가 포함됩니다.
과정 : 컴파일러는 소스 코드에서 문법적 오류를 검사하고, 타입 검사를 수행한 후, 실행 가능한 IL 코드로 변환합니다.