Unity에서 C#으로 게임을 개발하면 작성한 C# 코드가 그대로 모바일에서 실행되는 것은 아니다.
Unity는 C# 스크립트를 컴파일하고, 선택한 Scripting Backend에 따라 실제 플랫폼에서 실행할 수 있는 형태로 변환한다.
대표적인 방식이 Mono와 IL2CPP다.
C# Script -> C# Compiler -> IL -> Scripting Backend -> Native Code / Runtime -> Application
여기서 Scripting Backend가 무엇인지에 따라 빌드 결과와 실행 방식이 달라진다.
Scripting Backend -> Mono / IL2CPP
Mono
Mono는 Unity에서 오랫동안 사용되어 온 .NET 기반 실행 환경이다.
C# 코드를 컴파일하면 중간 언어인 IL(Intermediate Language)이 생성되고, Mono 런타임이 이를 실행한다.
개념적으로 보면 다음과 같다.
C# -> IL -> Mono Runtime -> 실행
개발 과정에서 빌드가 비교적 빠르다는 장점이 있다.
코드를 수정하고 테스트하는 작업이 반복되는 개발 단계에서는 빌드 시간이 짧다는 것이 상당히 중요하다.
반면 플랫폼에 따라 Mono 사용에 제한이 있을 수 있고, AOT 컴파일이 필요한 플랫폼에서는 IL2CPP가 필요하다.
IL2CPP
IL2CPP는 이름 그대로 **IL to C++**를 의미한다.
Unity에서 생성된 IL 코드를 C++ 코드로 변환한 뒤, 해당 플랫폼의 네이티브 컴파일러를 이용해 최종 바이너리를 생성한다.
C# -> IL -> IL2CPP -> C++ -> Native Compiler -> Native Binary
따라서 IL2CPP는 단순히 “C#을 C++로 바꿔주는 기능”이라고만 이해하면 부족하다.
실제로는 IL을 C++로 변환하고, 이후 플랫폼에 맞는 네이티브 코드로 컴파일하는 Unity의 Scripting Backend다.
Mono와 IL2CPP의 차이
가장 중요한 차이는 실행 코드가 만들어지는 방식이다.
Mono -> C# -> IL -> Mono Runtime -> 실행
IL2CPP -> C# -> IL -> C++ -> Native Compiler -> Native 실행 코드
Mono는 런타임을 이용해 IL을 실행하는 구조이고, IL2CPP는 IL을 C++로 변환하여 네이티브 바이너리로 만드는 구조다.
따라서 두 방식은 단순히 “빌드 옵션 하나가 다르다” 정도의 차이가 아니다.
게임 코드가 최종적으로 실행되는 방식 자체가 달라진다.
IL2CPP를 사용하는 이유
IL2CPP의 중요한 이유 중 하나는 AOT(Ahead-of-Time) 컴파일이다.
일부 플랫폼에서는 런타임에 코드를 동적으로 생성하거나 JIT(Just-in-Time) 방식으로 컴파일하는 것이 제한된다.
이 경우 프로그램 실행 전에 필요한 코드를 미리 네이티브 코드로 컴파일해야 한다.
JIT
실행 -> 필요한 코드 컴파일 -> 실행
AOT
빌드 -> 코드 컴파일 -> 실행
IL2CPP는 IL을 C++로 변환하고 이를 네이티브 컴파일하여 이러한 AOT 환경에 대응할 수 있다.
특히 iOS와 같은 플랫폼에서 IL2CPP가 중요한 이유가 여기에 있다.
IL2CPP의 빌드 과정
IL2CPP 빌드를 조금 더 구체적으로 보면 다음과 같다.
C# Source -> C# Compiler -> IL -> IL2CPP -> Generated C++ -> Native Compiler -> Native Binary
Unity 프로젝트의 C# 코드가 먼저 IL 형태로 컴파일된다.
그 다음 IL2CPP가 IL을 분석하여 C++ 코드를 생성한다.
생성된 C++ 코드는 플랫폼의 네이티브 컴파일러를 통해 최종 실행 파일 또는 라이브러리로 만들어진다.
C# -> IL -> C++ -> Native Code
이것이 IL2CPP의 핵심 구조다.
Mono의 장점
Mono의 가장 큰 장점은 개발 편의성이다.
일반적으로 IL2CPP보다 빌드 과정이 단순하고 빌드 시간이 짧기 때문에 개발 중 반복적인 테스트에 유리하다.
코드 수정 -> Mono Build -> 테스트 -> 수정 -> Build -> 테스트
이 사이클이 빠를수록 개발 생산성이 올라간다.
따라서 개발 단계에서는 빠른 빌드가 중요한 경우 Mono가 유리할 수 있다.
IL2CPP의 장점
IL2CPP는 네이티브 코드 기반으로 빌드할 수 있다는 점이 핵심이다.
또한 AOT 기반 플랫폼을 지원할 수 있고, 빌드 과정에서 코드 변환 및 스트리핑이 적용되기 때문에 최종 애플리케이션의 코드 구성에도 영향을 준다.
다만 IL2CPP가 항상 Mono보다 “빠르다”고 단순하게 결론 내리면 안 된다.
실행 성능은 실제 코드와 플랫폼, Unity 버전, 컴파일 옵션 등에 따라 달라진다.
따라서 다음과 같이 이해하는 것이 정확하다.
IL2CPP -> AOT + Native Code 기반 빌드
Mono -> Managed Runtime 기반 실행
IL2CPP의 단점
IL2CPP의 가장 체감되는 단점은 빌드 시간이 길어진다는 것이다.
C# 코드를 단순히 컴파일하는 것이 아니라
IL -> C++ 변환 -> C++ 컴파일 -> 링크
과정을 거쳐야 하기 때문이다.
프로젝트 규모가 커질수록 이 차이는 커질 수 있다.
또한 IL2CPP에서는 Reflection, Generic, Serialization 등을 사용하는 코드에서 추가적인 주의가 필요할 수 있다.
특히 런타임에 타입이나 메서드를 동적으로 찾는 코드는 AOT 환경의 특성을 고려해야 한다.
Reflection / Generic / Serialization
->
AOT 환경 고려
->
필요한 타입과 코드가 빌드에 포함되는지 확인
따라서 Mono에서 정상적으로 동작하는 코드가 IL2CPP에서도 항상 동일하게 동작한다고 가정해서는 안 된다.
모바일 빌드에 필요한 SDK와 NDK
Android 빌드를 할 때는 Unity만 설치되어 있다고 끝나는 것이 아니다.
Android 플랫폼에 맞는 개발 도구가 필요하다.
대표적으로 다음이 있다.
Android SDK -> Android 플랫폼 및 빌드 도구
JDK -> Java 기반 Android 개발 도구
NDK -> Native C/C++ 코드 빌드 도구
Mono와 IL2CPP의 요구사항도 차이가 있다.
개념적으로 보면 다음과 같다.
Mono Android Build -> Unity + Android SDK + JDK
IL2CPP Android Build -> Unity + Android SDK + JDK + NDK
실제 요구되는 구성은 Unity 버전과 플랫폼에 따라 달라질 수 있으므로 특정 버전의 JDK나 NDK를 무조건 설치하는 방식으로 접근하면 안 된다.
최근 Unity는 Unity Hub와 Unity 설치 모듈을 통해 필요한 Android 개발 환경을 함께 구성할 수 있다.
Android 빌드의 기본 설정
Unity에서 Android 빌드를 할 때 기본적인 흐름은 다음과 같다.
Build Settings -> Android 선택 -> Switch Platform -> Player Settings -> Android 설정 -> Build
먼저 Android 플랫폼을 선택한다.
그 다음 Player Settings에서 애플리케이션의 기본 정보를 설정한다.
대표적으로 다음과 같은 값이 있다.
Company Name
Product Name
Package Name
Minimum API Level
Target API Level
Scripting Backend
그중 Package Name은 Android 애플리케이션을 식별하는 중요한 값이다.
예를 들어
com.company.game
와 같은 형태로 지정한다.
Scripting Backend 설정
Android Player Settings의 Scripting Backend에서는 Mono 또는 IL2CPP를 선택할 수 있다.
Scripting Backend -> Mono
또는
Scripting Backend -> IL2CPP
IL2CPP를 선택하면 Unity가 빌드 과정에서 IL2CPP 변환 과정을 수행한다.
C# -> IL -> IL2CPP -> C++ -> Android Native Binary
Mono를 선택하면 Mono 기반으로 빌드한다.
C# -> IL -> Mono Runtime 기반 실행
Mono와 IL2CPP를 언제 사용하는가
개발 단계에서는 빌드 속도가 중요하다.
수십 번, 수백 번 빌드해야 하는 상황에서 빌드 시간이 길어지면 개발 생산성이 직접적으로 떨어진다.
개발 -> 빠른 반복 -> 빠른 빌드 -> Mono 고려
반면 실제 출시 빌드에서는 플랫폼 요구사항과 배포 환경을 우선적으로 고려해야 한다.
Release -> Platform Requirement -> AOT / Native Build -> IL2CPP 고려
따라서 “무조건 IL2CPP가 좋다” 또는 “Mono가 빠르니까 Mono를 써야 한다”처럼 단순하게 판단하면 안 된다.
개발 환경과 배포 환경의 요구사항이 다르기 때문이다.
IL2CPP와 보안
IL2CPP를 사용하면 C# 코드가 그대로 IL 형태로 배포되는 구조와 달리 네이티브 코드로 변환된다.
때문에 소스 코드 분석이나 단순한 managed assembly 분석을 어렵게 만드는 효과가 있다.
하지만 이것을 완전한 코드 보안이라고 생각하면 안 된다.
네이티브 바이너리도 역공학할 수 있다.
IL2CPP -> 네이티브 코드
-> 분석 난이도 증가
-> 역공학 불가능 X
따라서 IL2CPP를 보안 기능이라고 단정하기보다는 코드 형태를 네이티브 바이너리로 변환하는 빌드 방식이며, 결과적으로 역공학 난이도에 영향을 줄 수 있다고 이해하는 편이 정확하다.
정리
Unity의 모바일 빌드에서 중요한 것은 단순히 APK를 만드는 방법이 아니다.
C# 코드가 실제 플랫폼에서 어떤 방식으로 실행되는지를 이해해야 한다.
C# -> IL -> Scripting Backend -> Platform Native Code -> Application
Mono와 IL2CPP의 핵심 차이는 다음과 같이 정리할 수 있다.
Mono -> IL -> Mono Runtime -> 실행
IL2CPP -> IL -> C++ -> Native Compiler -> 실행
Mono는 개발 과정에서 빠른 반복 작업에 유리하고, IL2CPP는 AOT 및 네이티브 코드 기반 플랫폼에 대응하기 위한 Unity의 핵심 빌드 방식이다.
결국 선택 기준은 단순하다.
개발 생산성 -> Mono 고려
AOT / 플랫폼 요구사항 -> IL2CPP 고려
출시 빌드 -> 플랫폼 요구사항과 프로젝트 특성에 따라 결정
Unity에서 “C#으로 작성했는데 왜 C++과 NDK가 필요한가?”라는 의문이 생긴다면 IL2CPP의 빌드 파이프라인을 이해하면 된다.
Unity의 C# 코드는 최종 플랫폼에서 반드시 C# 그대로 실행되는 것이 아니다.
Scripting Backend가 그 사이의 변환과 실행 방식을 결정한다.