월세 500 호기심 천국
1주차: Unity 엔진은 왜 개발 언어로 C#을 선택했을까? 본문
0주차: 인트로 - 새롭고 신기한 것을 좋아하거나, 모르는 것을 알고 싶어 하는 마음
1주차: Unity 엔진은 왜 개발 언어로 C#을 선택했을까?
2주차: 의외로 세상 잡다한 Transform에 대한 모든 것
3주차: EulerAngle아, Quaternion아, 둘이 싸우지 말고 사이좋게 지내야 한다~ (1/2)
4주차: EulerAngle아, Quaternion아, 둘이 싸우지 말고 사이좋게 지내야 한다~ (2/2)
5주차: 헐크 아저씨도 깜짝 놀란 Unity Render Pipeline이 작동하는 과정
6주차: 매 프레임 나보다 갓생 사는 Unity의 Life Cycle
7주차: 남들보다 더 갓생 사는 Coroutine의 충격적인 정체
8주차: 야물(YAML)딱진 그 녀석의 은밀한 속마음 - Unity Scene 파일 (.unity)
9주차: 내 과자 포장지 누가 버렸어? - meta data 파일 (.meta) 파헤치기
10주차: 철가방 채로 배달온 Unity 빌드 파일. 난 단무지 시킨 적 없는데, 왜 있지?
11주차: 마무리 - 후기
Unity 심층분석 시리즈를 처음 구상했을 때
이게 첫 주제로 꽤 괜찮을 것 같다는 생각에 선정하였다.
모든 Unity 개발자는 C#을 접하고 이용하게 되는데,
적어도 내 주변에서 그 이유를 궁금해 하는 사람은 아무도 없었기 때문이다.
옆 동네 Unreal Engine에서는 C++를 자유자재로 구사하고, 저 아랫동네 Ren'Py도 Python 사투리를 조금 쓴다는데,
왜 우리는 당연하다는 듯이 C#을 쓰고있는 걸까?
문득 그 이유가 알고 싶어졌다.


그 이유를 설명하기에 앞서, 다소 모호한 것을 구분하고 넘어가야 할 것 같다.
사실 Unity는 C#과 C++을 모두 사용
Unity 엔진을 이용하는 게임 개발자들은 대부분 C#으로 스크립트를 작성하지만,
사실 엔진 자체는 C++로 짜여져 있다. (그래도 일부 UI, API는 C#이라고 한다)
이들이 엔진을 만드는 데에 C#이 아닌 C++을 사용한 이유는 비교적 명확할 것이다.
Unreal 엔진이 C++을 선택한 이유와 마찬가지로
속도와 성능, 유연성 면에서 C++이 C#에 비해 더 우세한 면모를 보이고,
저수준의 메모리 조절을 직접 할 수 있기 때문이다.
여기서 쓰인 저수준이라는 말은 무언가가 형편 없다거나 볼품 없다는걸 말하는게 아니라,
기계가 알아듣기 쉬운, 사람이 알아듣기 힘든 컴퓨터 친화적인 것들을 말한다.
다시 말해 사람이 이해하기도 쉽고 유지보수도 용이하게 할 수 있는 인간 친화적인 것들을 고수준,
컴퓨터가 이해하기 쉽고 CPU와 가까운 심연의 영역들까지 건드릴 수 있는 음흉한 것들을 저수준이라고 한다.
이는 도리어 우리의 호기심을 더욱 자극시킨다.
왜 엔진은 기껏 빠르고 성능 좋은 C++로 만들어두고,
게임 개발자들에게는 C#을 쓰게끔 환경을 조성해둔 걸까?
이를 알기 위해서는 먼저 C++과 C#의 언어적 특징, 그리고 그 차이를 알아봐야 한다.
C++과 C#의 차이
C++과 C#은 모두 C언어에서 출발한 언어이고,
그런 만큼 비슷한 구석도 많다.
하지만 다음과 같은 부분들에서 다소 차이를 보인다.
컴파일 환경
| C++ | C# | |
| 컴파일 결과 | 어셈블리어 | CIL |
| 플랫폼 | 중립 | .NET 플랫폼 |
C++은 비교적 고수준의 언어이다.
물론 여기서도 고수준의 언어는 위에 적어둔 설명처럼 인간 친화적인 언어를 말한다.
하지만 평소 다른 언어들을 공부해본 경험이 있는 사람이 이 글을 본다면
"C++은 저수준 언어로 유명한 C언어에 기반을 두고 있고, 얘도 저수준 언어로 알고 있는데... 뭐지?"
하고 의아해할 것이다.
하지만 그럼에도 내가 C++를 비교적 고수준의 언어로 소개한 이유는,
그 상대가 극강의 저수준 언어로 유명한 어셈블리어이기 때문이다...
아무튼 사람에게 쓰여진 C++ 코드들은 컴퓨터가 직접 읽기엔 비효율적인 부분들이 많아
저수준의 언어로 변환되는 컴파일이라는 과정을 거치게 되고, 이후 결과적으로 어셈블리어가 나온다.
저수준 언어 끝판왕인 어셈블리어의 특징은,
작동하는 운영체제나 CPU에 따라 규격이나 문법이 전부 달라진다는 것이다.
가령 Windows에서 돌아가는 어셈블리어가 Android OS, iOS 스마트폰에서는 돌아가지 않고,
Intel CPU에서 돌아가는 어셈블리어가 AMD CPU를 사용하는 스마트폰에서 돌아가지 않는다.
이를 해결하기 위해 C#은 컴파일의 결과물로 CIL(Common Intermidiate Language, 공통 중간 언어)을 내놓는다.
CIL은 .NET에서 실행되는 언어로,
.NET은 가장 아래 단계인 기계어와 어셈블리어의 한 층 위에서 작동하는 가상머신이다.
가상머신이 가상머신인 만큼 운영체제, CPU의 종류를 가리지 않고 여러 가지의 플랫폼에서 동일하게 실행될 수 있는,
크로스 플랫폼이라는 C++과는 차별화된 소소한 장점을 가지게 된다.
하지만 본질적으로 이걸 말하고 싶었던 건 아니고...
아무튼 이렇게 하여
C++은 고수준 언어(C++) → 저수준 언어(어셈블리어),
C#은 고수준 언어(C#) → 중간 언어(CIL) → 저수준 언어(어셈블리어)
의 과정을 거친다는 것을 알게 되었다.
그렇다면 Unity 엔진의 입장에서,
C#이 C++과 다르게 중간 언어를 거치며 얻게 되는 가장 큰 이점은 무엇일까?
바로 컴파일 속도다.
C++은 컴파일 때 고수준 언어에서 저수준 언어로 곧바로 전부 변환되어 상당한 컴파일 시간을 요구하는 반면,
C#은 컴파일 때 고수준 언어에서 중간 언어로 짧게 한 번 변환, 그리고 이후 런타임 때 중간 언어에서 저수준 언어로 실시간 변환되기 때문에,
C#은 C++에 비해 매우 빠른 컴파일 속도를 가지게 된다.
이런 식으로 런타임 때 .NET 가상머신에서 CIL을 저수준 언어로 실시간으로 컴파일하는 방식을
JIT(Just-In-Time) 컴파일이라고 한다.
가끔 Unity 개발을 할 때 VS로 C# 스크립트를 수정하고 Unity 에디터 화면으로 돌아오면
아주 약간의 로딩 시간 후 수정사항들이 곧바로 실시간으로 적용돼있는 모습을 자주 볼 수 있는데,
이것이 C#에서의 JIT 컴파일의 역할을 톡톡히 본 상황이라고 볼 수 있겠다.
그리고 이는 Unity 공식 홈페이지에서도 찾아볼 수 있는,
Unity가 C#을 채택한 이유 중 하나이다.
이야기는 CTO가 C#으로 Mono .NET 런타임을 활용하기 시작한 17년 전부터 시작됩니다. Unity는 C#을 상대적으로 효율적인 네이티브 코드로 변환하는 JIT(Just-In-Time) 컴파일러와 결합된 단순성 때문에 C#을 선호했습니다. Unity 엔진의 나머지 부분과 훨씬 더 큰 부분은 균형이 잘 잡혀 있고 제어된 성능을 제공하기 위해 C++를 사용하여 개발되었습니다.
출처: https://blog.unity.com/engine-platform/unity-and-net-whats-next

메모리 관리 방식
| C++ | C# | |
| 동적 할당 | 포인터 | 참조형 |
| 메모리 해제 | 수동 | 자동 (Garbage Collector) |
두 번째 이유를 알기 위해서는 C++과 C#의 메모리 관리 방식을 알아볼 필요가 있다.
앞에서는 상대가 어셈블리어였다보니 C++을 고수준 언어라 소개했었지만,
사실은 보통의 사람들이 말하듯 C++은 포인터를 이용해 메모리 관리에 직접 관여하는 저수준 언어가 맞다.
메모리를 동적을 할당하고 싶을 때는 포인터를 이용해 사용하고 싶은 만큼 메모리를 할당받고,
더 이상 해당 메모리가 필요 없어지면 명령어를 호출해 할당받은 부분을 수동으로 해제한다.
이는 능력 있는 개발자들로 하여금 할당과 해제를 적재적소에 배치하여 메모리 관리를 효율적으로 할 수 있게끔 했지만
수많은 C++ 개발자들을 흉악한 버그와 메모리 누수의 구렁텅이로 빠트렸으며
C++ 초심자들의 흔히 말하는 입구컷이 되기도 했다.

반면에 C#의 경우에는 메모리 관리 방식이 조금 다르다.
사용자는 메모리를 별도로 해제해줄 필요가 없으며,
메모리가 부족해질 때 쯤 GC(GarbageCollector)가 알아서 나타나 안쓰는 메모리들을 정리해준다.
물론 C# 내에서 포인터의 개념이 아예 사라진 것이 아니지만
이는 C# 라이브러리들이 알아서 잘 쓰는 용도일 뿐,
사용자가 직접 포인터를 만들고 사용할 일은 없다.
때문에 Unity 입문자의 허들이 매우 낮아지게 된다.
물론 GC도 단점이 아예 없는 것은 아니다.
가끔 Unity로 만들어진 게임을 하다보면 동일 조건, 동일 스테이지, 동일 상황임에도
오래 켜둔 게임과 방금 킨 게임의 성능 차이가 생기는 경우가 있다.
이는 오래 켜둔 동안 메모리가 잘 정리되지 않았고,
GC의 존재로 인해 개발자가 메모리를 해제하는 시점을 직접 지정할 수 없기 때문이다.
System.GC.Collect(); 명령어를 통해 GC를 강제로 실행시킬 수는 있지만,
이것 역시 개발자의 의도와 무관하게 .NET의 판단에 따라 지연될 수 있기 때문에 한계가 있다.
하지만 그럼에도 프로그래밍에 아직 미숙한 초보 개발자 입장에서 GC의 존재는 한없이 듬직할 것이고,
C++을 채택한 Unreal과 달리 Unity가 입문/인디 개발자 친화적인 이미지를 구축하는 데에
C#의 낮은 메모리 관리 난이도가 한 몫 했을 것이다.
편의성 개선: 모호성 완화
C++은 C언어에서 본따 만들어지는 과정에서 몇 가지 개선과 기능 추가가 이루어졌고,
C# 역시 마찬가지로 C++에서의 개선 사항과 기능들이 추가되어 만들어졌다.
그 중 C#이 특히 중요시 여겼던 부분은 편의성 개선이었다.
C#이 C++에 비해 개선된 것에는 다음 요소들이 있다.
if문 개선
본래 C++에서 if문 안의 조건식에는 int, float, char, bool 등 모든 타입의 변수들이 들어갈 수 있었다.
가령 아래와 같은 상황이 가능해진다.
#include <iostream>
int main() {
int a;
//a 입력 받음
std::cin >> a;
if (a) {
//a가 0이 아닐 경우 실행
std::cout << "a는 0이 아님" << std::endl;
}
else {
//a가 0일 경우 실행
std::cout << "a는 0" << std::endl;
}
return 0;
}
조건식에 bool값만이 아닌 다른 값들도 넣을 수 있는 범용성이 보장된 기능처럼 보일 수 있지만,
자칫하면 다음과 같은 실수를 낳을 수 있다.
#include <iostream>
int main() {
int a;
//a 입력 받음
std::cin >> a;
if (a = 0) { // <- 잘못 쓰여짐
//a가 0일 경우 실행
std::cout << "a는 0임" << std::endl;
}
else {
//a가 0이 아닐 경우 실행
std::cout << "a는 0이 아님" << std::endl;
}
return 0;
}
문제가 되는 부분은 if (a = 0) 부분이다.
프로그래머의 의도대로 실행시키려면 a = 0이 아닌 a == 0로 적어야 올바르다는 것 쯤은 모두가 알고 있지만,
실수는 우리 주변에서 언제 어떻게 일어날지 모르는 법이다.
특히 초심자의 경우에는 이런 실수가 잦아질 수도 있다.
C++은 해당 경우를 에러로 취급하지 않고 그대로 컴파일하는 반면,
C#은 if문 조건식으로 오로지 bool값만을 받기 때문에
이것을 에러로 취급하고 프로그래머로 하여금 다시 검토하게 한다.
기능은 다소 제한될지라도 프로그래밍 과정에서의 모호성을 줄여주는 것이다.
이런 사소한 부분이 Unity 초심자들의 C# 언어에 대한 체감 난이도를 낮춰준다.
switch 기능 강화/개선
사소하지만 C++의 기능이 더욱 강화되어 넘어온 것도 있다.
#include <iostream>
int main() {
std::string str;
//str 입력 받음
std::cin >> str;
switch (str)
{
case "MOVE": //이동
case "JUMP": //점프
case "ATTACK": //공격
default:
break;
}
return 0;
}
C++ 개발자가 해당 코드를 처음 봤을 때
프로그래머가 잘못 작성했다고 지적할 만한 부분들이 몇 군데 있을 것이다.
switch의 표현식에는 문자열이 들어갈 수 없다는 것과,
case가 끝나는 경우에는 모두 break가 있어야 한다는 것이다.
하지만 이것들은 C#에는 해당되지 않는다.
표현식 안에 문자열을 넣어 코드를 더 자유롭게 작성할 수 있고,
실수로 case문을 break로 끝내지 않더라도 알아서 case가 종료되면 switch문도 알아서 종료된다.
(단 해당 case가 비어있지 않다는 조건 하에. 비어있으면 C++처럼 다음 case로 넘어감)
이것들(if문, switch문) 역시 C++에서 흔히 쓰이는 기능들의 편의성을 보완한 결과물이라고 볼 수 있다.
그리고 메모리 관리 관련 내용에서도 언급했듯이,
이런 사소한 난이도 완화 부분들은 인디 게임 개발자의 Unity 개발 장벽을 낮추는데 크게 한몫한다.
이렇게 해서 Unity가 C++이 아닌 C#을 선택한 이유 세 가지를 알아봤다.
추가) C#의 단점을 숨겨라, IL2CPP
이렇게 C#이 C++에 비해 많은 장점을 가지고 있음에도, 단점 역시 존재한다.
바로 보안성이다.
C++은 컴파일 과정에서 바로 어셈블리어로 변환되는 반면,
C#은 중간 언어인 CIL로 한 번 변환되고
이후 .NET 가상머신을 반드시 거치기 때문에 보안상 취약한 점이 있다고 한다.
때문에 Unity 엔진은 완성된 게임을 빌드하기 전
개발자가 작성한 C# 코드들을 C++ 코드로 변환해 빌드하는 IL2CPP 기능을 지원하고 있다.
(다만 언어 변환에 걸리는 시간 때문에 빌드 시간이 좀 더 길어짐)
Unity 에디터에서 개발할 때는 C#의 쾌적한 컴파일 속도를 누리고,
빌드할 때는 C++로 변환해 보안성까지 챙기는 이런 발칙한 방법이 있나!
참으로 어쩌다 알게된 꿀기능이 아닐 수 없다.

Unity에서 C#을 선택한 이유 추측 세 줄 요약:
1. C#은 .NET의 JIT 컴파일러를 활용하기 때문에 컴파일 속도가 매우 빠름 (코드 수정된 부분 즉각 반영)
2. 게임 개발자 입장에서 메모리 관리가 매우 편함
3. C++과 비교하여 더욱 편해진 문법들
- 그리고 후에 나온 IL2CPP 기능을 활용해 C#의 보안성 문제까지도 보완 가능해짐

1주차 끝!!!
0주차: 인트로 - 새롭고 신기한 것을 좋아하거나, 모르는 것을 알고 싶어 하는 마음
1주차: Unity 엔진은 왜 개발 언어로 C#을 선택했을까?
2주차: 의외로 세상 잡다한 Transform에 대한 모든 것
3주차: EulerAngle아, Quaternion아, 둘이 싸우지 말고 사이좋게 지내야 한다~ (1/2)
4주차: EulerAngle아, Quaternion아, 둘이 싸우지 말고 사이좋게 지내야 한다~ (2/2)
5주차: 헐크 아저씨도 깜짝 놀란 Unity Render Pipeline이 작동하는 과정
6주차: 매 프레임 나보다 갓생 사는 Unity의 Life Cycle
7주차: 남들보다 더 갓생 사는 Coroutine의 충격적인 정체
8주차: 야물(YAML)딱진 그 녀석의 은밀한 속마음 - Unity Scene 파일 (.unity)
9주차: 내 과자 포장지 누가 버렸어? - meta data 파일 (.meta) 파헤치기
10주차: 철가방 채로 배달온 Unity 빌드 파일. 난 단무지 시킨 적 없는데, 왜 있지?
11주차: 마무리 - 후기
'내 맘대로 쓰는 Unity 심층분석 析深層分' 카테고리의 다른 글
| 5주차: 헐크 아저씨도 깜짝 놀란 Unity Render Pipeline이 작동하는 과정 (3) | 2024.01.27 |
|---|---|
| 4주차: EulerAngle아, Quaternion아, 둘이 싸우지 말고 사이좋게 지내야 한다~ (2/2) (1) | 2024.01.18 |
| 3주차: EulerAngle아, Quaternion아, 둘이 싸우지 말고 사이좋게 지내야 한다~ (1/2) (1) | 2024.01.13 |
| 2주차: 의외로 세상 잡다한 Transform에 대한 모든 것 (1) | 2024.01.07 |
| 0주차: 새롭고 신기한 것을 좋아하거나, 모르는 것을 알고 싶어 하는 마음 (1) | 2023.12.19 |