2026. 3. 10. 13:25ㆍ운영체제(OS)
프로그래밍 언어 분류와 링커, 로더 — Unity 개발자 관점
프로그래밍 언어가 어떻게 분류되는지, 그리고 작성한 코드가 실제로 실행되기까지 링커와 로더가 어떤 역할을 하는지 정리한다. 교재에서 배우는 이 흐름은 Unity가 C# 코드를 빌드하고 실행하는 방식과 직결된다.
1. 프로그래밍 언어 분류
프로그래밍 언어는 크게 저급 언어와 고급 언어로 나뉜다. 저급 언어는 기계어와 어셈블리어1가 해당되고, 고급 언어는 C, Java, C# 처럼 사람이 읽기 쉬운 문법을 가진 언어들이다. 저급일수록 하드웨어에 가깝고, 고급일수록 인간의 언어에 가깝다.
고급 언어로 작성된 코드는 CPU가 직접 이해할 수 없기 때문에 반드시 번역 과정이 필요하다. 그 번역 방식에 따라 언어를 세 가지로 구분한다. 컴파일 언어, 인터프리터2 언어, 그리고 바이트코드3 기반 언어다.
컴파일 언어
C, C++ 같은 컴파일 언어는 컴파일러가 소스 전체를 미리 기계어로 변환한다. 실행 시점에 별도 번역이 없으니 속도가 가장 빠르다. 단점은 플랫폼마다 다시 컴파일해야 한다는 것이다. Windows용 exe는 macOS에서 그대로 실행되지 않는다.
인터프리터 언어
Python 같은 인터프리터 언어는 소스 코드를 한 줄씩 읽어서 즉시 실행한다. 별도의 빌드 단계 없이 바로 실행할 수 있어 개발 속도가 빠르지만, 매 실행마다 번역하므로 컴파일 언어보다 느리다. 런타임에 인터프리터가 항상 필요하다는 점도 특징이다.
바이트코드 + JIT 언어
C#, Java는 두 방식의 중간을 택한다. 컴파일러가 소스를 바이트코드(중간 언어)로 변환하고, 실행 시점에 JIT4(Just-In-Time) 컴파일러가 바이트코드를 현재 플랫폼의 기계어로 변환해 실행한다. 플랫폼 이식성과 실행 속도를 동시에 챙기는 방식이다.
Unity에서 C#을 쓰면 기본적으로 Mono CLR5이 이 JIT 방식으로 동작한다. 반면 IL2CPP 빌드를 선택하면 IL을 C++ 코드로 변환한 뒤 완전히 기계어로 컴파일하는 AOT6(Ahead-Of-Time) 방식이 된다. iOS처럼 JIT 실행이 제한된 플랫폼에서는 IL2CPP가 필수다.
| 분류 | 번역 시점 | 장점 | 단점 | Unity 연관 |
|---|---|---|---|---|
| 컴파일 | 빌드 타임 | 빠른 실행 | 플랫폼 종속 | IL2CPP 최종 단계 |
| 인터프리터 | 런타임 (줄 단위) | 즉시 실행 | 느린 속도 | — |
| 바이트코드+JIT | 런타임 (블록 단위) | 이식성 + 속도 | JIT 워밍업 | Mono 런타임 |
| 바이트코드+AOT | 빌드 타임 (IL→기계어) | 이식성 + 최대 성능 | 빌드 시간 증가 | IL2CPP 전체 |
2. 링커(Linker)
소스 파일 하나가 컴파일되면 목적 파일(.obj, .o)이 만들어진다. 그런데 실제 프로그램은 여러 파일과 외부 라이브러리로 구성되는 경우가 많다. 링커는 이 분리된 목적 파일들을 하나로 합쳐 실행 파일을 만드는 도구다. 빌드 타임에 디스크 위에서 작업하며, 결과물은 실행 가능한 단일 파일이다.
링커가 하는 핵심 작업은 심볼 해결(Symbol Resolution)이다. 파일 A에서 파일 B의 함수를 호출할 때, 컴파일 단계에서는 그 주소를 모른다. 링커가 모든 목적 파일을 훑어보고 "이 이름은 저 파일의 저 위치다"를 연결해준다. 라이브러리도 이 시점에 함께 묶인다.
정적 링크 vs 동적 링크
링크 방식은 두 가지다. 정적 링크는 라이브러리 코드를 실행 파일 안에 전부 복사해 넣는다. 실행 파일이 커지지만 외부 의존성이 없어 어디서든 그대로 실행된다. 동적 링크는 라이브러리를 파일 안에 넣지 않고, 실행 시점에 외부 .dll 파일을 찾아 연결한다. 같은 DLL을 여러 프로세스가 공유할 수 있어 메모리 효율이 높다.
→ exe 크기 ↑
→ 외부 의존성 없음
→ 배포 단순
→ exe 크기 ↓
→ .dll 파일 필요
→ 메모리 공유 가능
Unity 빌드 결과물을 보면 이 차이가 바로 드러난다. Assembly-CSharp.dll은 내가 작성한 C# 코드가 담긴 동적 라이브러리다. UnityPlayer.dll, mono.dll 같은 파일들도 런타임에 동적으로 로드된다. IL2CPP 빌드에서는 이 .dll들이 하나의 기계어 바이너리로 정적 링크된 형태로 변환된다.
3. 로더(Loader)
링커가 실행 파일을 만들었다면, 그걸 실제로 실행하는 건 로더7의 몫이다. 로더는 디스크의 실행 파일을 읽어서 RAM에 올리고, CPU가 실행할 수 있는 상태로 준비하는 OS 구성 요소다. 프로그램을 더블클릭하거나 셸에서 실행 명령을 내리면 OS가 로더를 호출한다.
로더가 하는 작업은 크게 네 단계다. 필요한 메모리 공간을 할당(Allocation)하고, 심볼을 연결(Linking)한 뒤, 실제 메모리 주소에 맞게 주소를 재배치(Relocation)하고, 마지막으로 코드와 데이터를 RAM에 적재(Loading)한다.
로더의 종류
교재에서 소개하는 로더는 네 가지인데, 현대 시스템 관점에서 보면 두 가지가 실질적으로 사용된다.
| 로더 종류 | 동작 방식 | 현대 사용 |
|---|---|---|
| Compile and GO | 컴파일과 실행을 동시에 처리 | ❌ 거의 사용 안 함 |
| Absolute Loader | 미리 정해진 고정 주소에 적재 | ❌ 임베디드 일부만 |
| Direct Linking Loader | 링크와 적재를 함께 처리 | ✅ 일반 실행 파일 기본값 |
| Dynamic Loading Loader | 필요한 시점에만 코드 적재 (Load-On-Call) | ✅ DLL, Addressables 핵심 |
현대 OS는 Direct Linking Loader를 기본으로 쓰면서, DLL 같은 동적 라이브러리에는 Dynamic Loading을 함께 활용한다. 프로그램 실행 중에도 로더가 작동하는 셈이다.
동적 적재와 Unity Addressables
동적 적재8는 프로그램 시작 시점에 모든 코드를 올리지 않고, 해당 기능이 필요해지는 순간에만 메모리에 올리는 방식이다. 초기 로딩이 빠르고 메모리 사용량이 줄어드는 장점이 있다. Load-On-Call이라고도 부른다.
Unity의 Addressables 시스템은 이 동적 적재 개념을 게임 에셋에 적용한 것이다. Addressables.LoadAssetAsync()를 호출하면 OS 로더가 에셋 번들 파일을 디스크에서 RAM으로 올리고, Mono CLR이 필요한 코드를 JIT 컴파일하며, Unity 엔진이 에셋을 내부 캐시에 등록하는 순서로 동작한다. 씬에 들어갈 때만 에셋을 올리고, 나올 때 해제하는 패턴이 바로 이 원리다.
① OS 로더 — 에셋 번들(.bundle)을 디스크 → RAM으로 복사, 메모리 주소 재배치
② JIT 컴파일 — 번들 안의 IL 코드를 현재 플랫폼 기계어로 변환
③ GC 관리 — 에셋 객체 참조 카운팅 시작
④ Unity 엔진 — 에셋을 내부 캐시에 등록, 완료 콜백 호출
📎 용어 설명
- 어셈블리어 — 기계어와 1:1로 대응하는 저급 언어. CPU 명령어를 사람이 읽을 수 있는 니모닉(mov, add, jmp 등)으로 표현한다.
- 인터프리터 — 소스 코드를 한 줄씩 읽어 즉시 실행하는 번역 방식. 별도의 컴파일 단계 없이 실행되며 Python, 초기 JavaScript 등이 해당된다.
- 바이트코드 — 특정 CPU가 아닌 가상 머신이 이해하는 중간 명령어 집합. C#의 경우 IL(Intermediate Language), Java의 경우 Java bytecode라 부른다.
- JIT(Just-In-Time) — 프로그램 실행 중에 바이트코드를 현재 플랫폼의 기계어로 변환하는 컴파일 방식. 자주 실행되는 코드를 캐싱해 반복 변환 비용을 줄인다.
- Mono CLR — .NET CLR의 오픈소스 크로스플랫폼 구현체. Unity가 C# 런타임으로 채택했으며 JIT 방식으로 IL을 실행한다.
- AOT(Ahead-Of-Time) — 실행 전에 미리 기계어로 컴파일해두는 방식. JIT 워밍업 없이 즉시 최대 성능으로 실행되며, iOS처럼 런타임 코드 생성이 제한된 환경에서 필수다.
- 로더 — 디스크의 실행 파일을 RAM에 올리고 CPU가 실행 가능한 상태로 만드는 OS 구성 요소. 메모리 할당, 심볼 연결, 주소 재배치, 적재 네 단계를 처리한다.
- 동적 적재(Dynamic Loading) — 프로그램 실행 중 필요한 시점에만 코드나 데이터를 메모리에 올리는 방식. Load-On-Call이라고도 하며, DLL 로딩과 Unity Addressables가 이 원리를 사용한다.
'운영체제(OS)' 카테고리의 다른 글
| [OS] 스레드(Thread)와 멀티 스레딩 - 6 (0) | 2026.03.12 |
|---|---|
| [OS] 프로세스 개요와 PCB, 상태 전이 - 5 (0) | 2026.03.11 |
| [OS] CPU 구조와 파이프라인 - 4 (0) | 2026.03.11 |
| [OS]운영체제 발전 흐름 - 3 (0) | 2026.03.11 |
| [OS] 시스템 소프트웨어 구성과 OS의 역할 - 1 (0) | 2026.03.10 |