[PART1.C# 런타임과 .NET 플랫폼 기초(3/11)] 어셈블리 · 네임스페이스 · using — 코드가 담기는 세 개의 그릇
어셈블리는 물리적 덩어리, 네임스페이스는 논리적 이름 공간, using은 이 둘을 편하게 엮는 컴파일러 지시문입니다.
목차
1. [문제 제기] using만 추가했는데 왜 빌드가 안 되는가
Unity 프로젝트에서 TextMeshPro를 쓰려고 스크립트 상단에 다음 한 줄을 넣었다고 상상해 봅니다.
using TMPro;
하지만 빌드하면 CS0246: 'TMPro' 형식 또는 네임스페이스 이름을 찾을 수 없습니다 오류가 납니다. 분명 using을 썼는데 왜 안 될까요?
반대 상황도 있습니다. Newtonsoft.Json 패키지를 NuGet으로 설치하고 .csproj에 <PackageReference>까지 들어갔는데 JsonConvert 타입을 못 찾습니다. 이번엔 using Newtonsoft.Json;을 빼먹은 경우입니다.
using— 컴파일러에게 "이 네임스페이스의 타입을 짧은 이름으로 부르겠다"고 알려주는 지시문 네임스페이스를 가져오는 것이지, DLL 파일(어셈블리)을 로드하는 명령이 아닙니다.
예시:using System.Collections.Generic;이제System.Collections.Generic.List<int>대신List<int>로 쓸 수 있습니다.
이 두 오류가 모두 하나의 뿌리에서 나옵니다. 바로 어셈블리 · 네임스페이스 · using 이 각각 다른 층에 존재한다는 사실을 이해하지 못한 데서 생깁니다.
- 어셈블리는 디스크에 존재하는 물리적 바이너리 파일(DLL/EXE) 입니다. 프로젝트에 참조(Reference)가 없으면 어떤
using으로도 타입을 쓸 수 없습니다. - 네임스페이스는 그 어셈블리 안에 들어있는 타입들을 논리적으로 묶는 이름표입니다. 어셈블리와 네임스페이스는 1:1 관계가 아닙니다.
- using은 컴파일 타임에만 존재하는 문법적 단축입니다. 런타임에는 흔적도 남지 않습니다.
Unity 모바일 클라이언트 개발자는 매일 이 세 개념을 다룹니다. asmdef로 어셈블리를 쪼개고, 네임스페이스로 서비스·전투·UI 코드를 분리하고, using으로 짧게 참조합니다. 세 층이 어떻게 다른지 구분하지 못하면 CS0246 오류가 날 때 어디를 고쳐야 할지 판단할 수 없습니다.
이 글에서는 세 개념을 물리 → 논리 → 연결 순서로 쌓아가며, IL이 실제로 어떻게 생성되는지, Unity에서 asmdef가 왜 중요한지, 신입 개발자가 자주 밟는 함정까지 정리합니다.
2. [개념 정의] 세 개의 층을 도서관으로 이해하기
2.1. 한 장의 그림으로 보기
세 개념의 관계를 먼저 한 그림으로 머릿속에 심어놓고 시작합니다.

도서관으로 비유하면 이렇게 됩니다.
- 어셈블리 = 도서관 건물. 책이 물리적으로 담겨 있는 실체.
- 네임스페이스 = 도서관의 층/섹션 (예: "2층 과학 코너").
- 타입 = 실제 책 (클래스, 구조체, 인터페이스).
- using = "2층 과학 코너" 자주 가는 사람이 끊는 정기 출입증. 이 출입증이 있으면
2층 과학 코너의 양자역학 개론대신 그냥양자역학 개론이라고 불러도 사서가 알아듣습니다.
중요한 점은 한 건물(어셈블리)에 여러 층(네임스페이스)이 있을 수도 있고, 같은 이름의 층이 건물마다 따로 있을 수도 있다는 것입니다. 예를 들어 System.Text 네임스페이스의 타입들은 System.Runtime.dll, System.Text.Encoding.dll 등 여러 어셈블리에 퍼져 있습니다. 어셈블리와 네임스페이스는 독립적입니다.
2.2. 가장 단순한 예시
// MyGame.Combat 네임스페이스 안에 DamageCalc 클래스 정의
namespace MyGame.Combat
{
public class DamageCalc
{
public int Compute(int atk, int def) => atk - def;
}
}
// 다른 파일에서 사용
using MyGame.Combat; // "MyGame.Combat 층 출입증"
public class Program
{
public static void Main()
{
var calc = new DamageCalc(); // 짧은 이름으로 OK
Console.WriteLine(calc.Compute(10, 3));
}
}
using 지시문 한 줄로 MyGame.Combat.DamageCalc 를 DamageCalc 로 줄여 썼습니다. 이 코드를 컴파일하면 IL은 어떤 모습일까요?
.class public auto ansi beforefieldinit MyGame.Combat.DamageCalc
extends [System.Runtime]System.Object
{
.method public hidebysig
instance int32 Compute (int32 atk, int32 def) cil managed
{
IL_0000: ldarg.1
IL_0001: ldarg.2
IL_0002: sub
IL_0003: ret
}
}
.class public auto ansi beforefieldinit Program
{
.method public hidebysig static void Main () cil managed
{
// 타입 이름이 전체 이름(FQN) 으로 저장됨 — using 은 흔적 없음
IL_0000: newobj instance void MyGame.Combat.DamageCalc::.ctor()
IL_0005: callvirt instance int32 MyGame.Combat.DamageCalc::Compute(int32, int32)
...
}
}
핵심: IL에서 타입 이름은 항상 MyGame.Combat.DamageCalc 같은 전체 이름(FQN, Fully Qualified Name)으로 저장됩니다. using MyGame.Combat; 은 컴파일러가 소스를 읽을 때만 쓰는 단축 테이블일 뿐, 컴파일이 끝나면 사라집니다. 이것이 바로 "using은 런타임 비용 0" 의 진짜 의미입니다.
쉬운 설명: using은 편집기용 약자 사전 같은 것입니다. 컴파일러가 소스 코드를 읽다가 DamageCalc 를 만나면 사전을 뒤져 MyGame.Combat.DamageCalc 로 풀어 씁니다. 풀어쓰고 나면 사전은 버려집니다.
기술 정의: using 지시문은 어휘 범위(lexical scope)에 using 디렉티브 테이블을 추가하는 선언이며, 식별자 해석(name resolution) 단계에서만 사용됩니다. 컴파일러의 메타데이터 에미터는 TypeRef 와 MemberRef 테이블에 항상 정규화된 이름을 기록합니다.
3. [내부 동작] 어셈블리의 내부 구조와 이름 해석 순서
3.1. 어셈블리(DLL/EXE)의 내부 구조
어셈블리는 단순히 "코드가 들어있는 파일" 이 아닙니다. Windows PE(Portable Executable) 포맷 위에 .NET 전용 헤더와 여러 메타데이터 테이블이 얹혀진 자기 서술적(self-describing) 바이너리 입니다.

각 섹션이 하는 일:
- PE 헤더: OS가 이 파일을 실행 파일로 인식합니다.
- CLR 헤더: 런타임(CLR, Common Language Runtime — .NET의 실행 엔진)에게 "이건 관리되는 코드다" 라고 알려줍니다.
- 매니페스트: 이 어셈블리의 신분증과 의존성 목록. 버전, PublicKeyToken,
AssemblyRef로 기록된 다른 어셈블리 참조가 모두 여기에 있습니다. - 메타데이터 테이블: 타입·멤버 정보를 담은 구조화된 테이블. 리플렉션·IntelliSense·JIT이 모두 이것을 읽습니다.
- IL 코드: 메서드 본문. CPU 독립적인 중간 언어이며 실행 시점에 JIT이 기계어로 변환합니다.
다음 C# 코드를 빌드해 실제 매니페스트에 어셈블리 참조가 어떻게 박히는지 확인해 봅니다.
using System;
using System.Collections.Generic;
namespace MyGame.Combat
{
public class DamageCalc
{
public int Compute(int atk, int def)
{
var buffer = new List<int>();
buffer.Add(atk - def);
return buffer[0];
}
}
}
빌드 후 ilspycmd -il 로 IL을 보면 메서드 호출부가 이렇게 나옵니다.
.method public hidebysig
instance int32 Compute (int32 atk, int32 def) cil managed
{
// 외부 어셈블리 [System.Collections] 을 명시적으로 참조
IL_0000: newobj instance void class [System.Collections]System.Collections.Generic.List`1<int32>::.ctor()
IL_0005: dup
IL_0006: ldarg.1
IL_0007: ldarg.2
IL_0008: sub
IL_0009: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<int32>::Add(!0)
IL_000e: ldc.i4.0
IL_000f: callvirt instance !0 class [System.Collections]System.Collections.Generic.List`1<int32>::get_Item(int32)
IL_0014: ret
}
IL의 [System.Collections] 부분이 핵심입니다. 이것은 "어느 어셈블리에 있는 타입인지" 를 가리키는 AssemblyRef 참조입니다. 네임스페이스는 System.Collections.Generic, 어셈블리는 System.Collections. 둘이 다른 이름이라는 것도 이때 처음 보입니다. 컴파일러는 using System.Collections.Generic; 을 보고 List<T> 를 찾아낸 뒤, 그 타입이 실제로 담긴 어셈블리(System.Collections)를 IL에 직접 박아넣습니다.
이것이 바로 "참조와 using이 다른 일을 한다"는 말의 정체 입니다. <PackageReference> 는 컴파일러에게 "이 DLL을 뒤져봐도 좋다"고 알려주고, using 은 "그 DLL 안의 이 네임스페이스 타입들을 짧은 이름으로 쓰겠다"고 알려줍니다. 둘 다 있어야 합니다.
3.2. 이름 해석(Name Resolution) 순서
컴파일러가 소스에서 List<int> 같은 타입 이름을 만나면, 아래 순서로 타입을 찾습니다.

위에서부터 찾다가 처음으로 매칭되는 타입을 채택합니다. 그래서 "내부(현재 네임스페이스) → 외부(using)" 순서가 핵심입니다.
namespace MyGame.Combat
{
// 현재 네임스페이스에 Vector3 이 있음!
public struct Vector3 { public float X, Y, Z; }
using UnityEngine; // UnityEngine.Vector3 도 존재
public class Bullet
{
// 1단계에서 MyGame.Combat.Vector3 가 먼저 발견됨
// UnityEngine.Vector3 가 아니라 내가 만든 Vector3 를 쓰게 됨
Vector3 velocity;
}
}
이 예시는 신입이 가장 자주 당하는 버그 중 하나입니다. 같은 이름의 Vector3 가 내 네임스페이스에도, UnityEngine 에도 있으면 1단계 규칙에 따라 내 것이 이깁니다. 이때는 using alias 로 명시적으로 해결해야 합니다.
using UnityVector3 = UnityEngine.Vector3;
public class Bullet
{
UnityVector3 velocity; // 이제 확실히 UnityEngine 의 것
}
4. [실전 적용] using의 4가지 형태와 실전 선택
4.1. 4가지 using — 언제 무엇을 쓰는가
C# 10 기준으로 using 지시문은 4가지 형태가 있습니다.
| 형태 | 구문 | 용도 |
|---|---|---|
| 네임스페이스 | using System.Linq; |
특정 네임스페이스 타입을 짧은 이름으로 |
| 정적 | using static System.Math; |
정적 클래스 멤버를 이름 없이 |
| 별칭 | using Json = System.Text.Json; |
긴 이름 축약 · 이름 충돌 해결 |
| 전역 | global using System; |
프로젝트 전체 파일에 자동 적용 |
using static— 정적 멤버 가져오기 (C# 6+) 특정 클래스의static멤버(메서드·필드·상수)를 클래스 이름 없이 직접 호출할 수 있게 합니다. 수학 계산이나 LINQ 헬퍼처럼 짧게 쓰고 싶을 때 유용합니다.
예시:using static System.Math;뒤에PI * Pow(r, 2)(→Math.PI * Math.Pow(r, 2)와 동일)
global using— 전역 using (C# 10+) 한 파일(보통GlobalUsings.cs)에global using을 선언하면 프로젝트 내 모든 파일에 자동 적용됩니다. 반복적으로 쓰는using System;,using UnityEngine;같은 지시문을 제거하는 데 유용합니다.
예시:global using UnityEngine;→ 이제 프로젝트 모든.cs파일에서using UnityEngine;생략 가능
4.2. 성능 관점 — using은 정말 공짜인가
Before: 정적 using 사용
using static System.Math;
public class Program
{
public static double AreaWithUsingStatic(double r)
{
return PI * Pow(r, 2);
}
}
After: 전체 이름으로 호출
public class Program
{
public static double AreaWithFullName(double r)
{
return System.Math.PI * System.Math.Pow(r, 2);
}
}
둘 다 빌드해서 IL을 비교하면:
// AreaWithUsingStatic 의 IL
.method public hidebysig static
float64 AreaWithUsingStatic (float64 r) cil managed
{
IL_0000: ldc.r8 3.141592653589793
IL_0009: ldarg.0
IL_000a: ldc.r8 2
IL_0013: call float64 [System.Runtime]System.Math::Pow(float64, float64)
IL_0018: mul
IL_0019: ret
}
// AreaWithFullName 의 IL — 명령어까지 완전히 동일!
.method public hidebysig static
float64 AreaWithFullName (float64 r) cil managed
{
IL_0000: ldc.r8 3.141592653589793
IL_0009: ldarg.0
IL_000a: ldc.r8 2
IL_0013: call float64 [System.Runtime]System.Math::Pow(float64, float64)
IL_0018: mul
IL_0019: ret
}
두 메서드의 IL이 바이트 단위로 같습니다. 어느 쪽을 쓰든 런타임 비용은 정확히 같습니다. 어셈블리 경계 [System.Runtime] 이 두 경우 모두 IL에 동일하게 박힙니다. using static 을 쓴다고 메서드가 더 빨리 실행되지도 느려지지도 않습니다.
결론: using 선택은 성능이 아니라 가독성과 의도 로 판단합니다.
4.3. Unity 핫패스에서의 실전 예제 — using alias로 충돌 해결
Unity 게임에서 UnityEngine.Random 과 System.Random 을 함께 쓰면 이름이 충돌합니다. UnityEngine.Random 은 내부적으로 Unity 엔진의 전역 시드를 쓰고 스레드-안전하지 않습니다. 반면 System.Random 은 인스턴스별 시드를 갖고 서버/테스트 로직에 적합합니다.
Before: 매번 전체 이름으로 쓰기 (지저분함)
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
private readonly System.Random _serverRng = new System.Random(seed: 42);
void Update()
{
// Unity 시드 — 프레임마다 달라지는 비주얼 효과에 사용
float jitter = UnityEngine.Random.Range(-1f, 1f);
// 서버 결정적 로직에 사용
int damage = _serverRng.Next(10, 20);
}
}
After: using alias로 명확하게
using UnityEngine;
using UnityRandom = UnityEngine.Random;
using SysRandom = System.Random;
public class EnemySpawner : MonoBehaviour
{
private readonly SysRandom _serverRng = new SysRandom(seed: 42);
void Update()
{
float jitter = UnityRandom.Range(-1f, 1f); // 누가 봐도 Unity 쪽
int damage = _serverRng.Next(10, 20); // 누가 봐도 System 쪽
}
}
별칭이 있으면 코드 리뷰 때 "지금 이 Random이 어느 Random이지?" 라는 의문이 사라집니다. IL은 어느 쪽이든 UnityEngine.Random::Range / System.Random::Next 전체 이름으로 박혀서 동일하지만, 사람이 읽을 때 실수할 여지가 줄어듭니다.
Unity 핫패스 주의: System.Random 은 Next() 호출마다 내부 배열 연산이 있어 초당 수만 번 호출되는 물리 시뮬레이션 루프에서는 오버헤드가 감지됩니다. 그런 핫패스는 Unity.Mathematics.Random(Burst 호환) 또는 XorShift 같은 경량 PRNG로 교체합니다. 이름 충돌과 성능은 별개이지만, using alias 로 타입을 명시해두면 나중에 경량 PRNG로 교체할 때도 검색 대상이 명확합니다.
4.4. C# 10+의 암시적 using — .csproj 한 줄의 마법
.NET 6(C# 10) SDK 이상부터는 .csproj 에 한 줄 추가하면 자주 쓰는 네임스페이스들이 자동으로 global using 됩니다.
<!-- MyProject.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <!-- ← 여기 -->
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
활성화 시 SDK 유형에 따라 다음이 자동 적용됩니다(콘솔 기준).
// 보이지 않게 자동 추가되는 global using
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
그래서 .NET 6+ 콘솔 프로젝트에서는 using System; 없이도 바로 Console.WriteLine 을 쓸 수 있습니다. 신입이 "어? using 안 썼는데 왜 되지?" 하고 혼란스러워하는 지점입니다.
주의: Unity는 현재(2023 LTS 기준) ImplicitUsings 를 자동 지원하지 않습니다. Unity 프로젝트에서는 직접 GlobalUsings.cs 를 만들어야 합니다.
5. [함정과 주의사항] 신입이 가장 자주 밟는 지뢰
5.1. using 추가만으로는 아무것도 참조되지 않는다 (CS0246)
❌ 잘못된 패턴
// MyGame/Assembly-CSharp.csproj 에 Newtonsoft.Json 참조가 없는 상태
using Newtonsoft.Json;
public class SaveSystem
{
public string Serialize(object data)
{
return JsonConvert.SerializeObject(data);
// CS0246: 'JsonConvert' 형식 또는 네임스페이스 이름을 찾을 수 없습니다
}
}
이 코드는 using 만 있고 어셈블리 참조가 없습니다. .csproj 에 <PackageReference> 가 없으면 컴파일러는 Newtonsoft.Json 네임스페이스 자체를 모릅니다.
✅ 올바른 패턴
<!-- MyGame.csproj -->
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
using Newtonsoft.Json;
public class SaveSystem
{
public string Serialize(object data) => JsonConvert.SerializeObject(data);
}
빌드된 어셈블리의 매니페스트에는 이제 AssemblyRef 로 Newtonsoft.Json 이 등록됩니다.
.assembly extern Newtonsoft.Json
{
.publickeytoken = (30 AD 4F E6 B2 A6 AE ED)
.ver 13:0:0:0
}
비법: CS0246 이 뜨면 먼저 .csproj 의 <PackageReference> 와 <ProjectReference> 를 확인합니다. using 은 그다음 점검합니다.
5.2. internal 접근 제한자 오해 (asmdef 경계)
❌ 잘못된 패턴 — Unity asmdef 분리 후 internal 타입을 다른 asmdef에서 쓰려 함
// Assets/Scripts/Core/Services/GameSession.asmdef
namespace MyGame.Core
{
internal class GameSession // 기본 접근 제한자도 internal
{
public int PlayerId;
}
}
// Assets/Scripts/UI/HUD.asmdef (Core를 참조 중)
namespace MyGame.UI
{
using MyGame.Core;
public class Hud : MonoBehaviour
{
void Awake()
{
var session = new GameSession(); // CS0122: 보호 수준 때문에 접근할 수 없음
Debug.Log(session.PlayerId);
}
}
}
internal 은 같은 어셈블리 내부에서만 접근 가능합니다. asmdef를 쪼개는 순간 서로 다른 어셈블리가 되므로, UI asmdef에서는 Core asmdef의 internal 타입을 볼 수 없습니다.
internal— 같은 어셈블리 내부에서만 접근 허용 타입이나 멤버에internal을 붙이면 동일 어셈블리 코드에서만 접근 가능합니다. asmdef로 프로젝트를 쪼개면 각 asmdef가 별도 어셈블리가 되므로 경계가 분명해집니다. 참고로 클래스의 기본 접근 제한자는internal, 클래스 멤버의 기본은private입니다.
예시:internal class GameSession { ... }→ 같은 asmdef 안에서만 보임
✅ 올바른 패턴 1: public 으로 승격
namespace MyGame.Core
{
public class GameSession // 외부 asmdef에서도 접근 가능
{
public int PlayerId;
}
}
✅ 올바른 패턴 2: 테스트·Friend Assembly 만 허용하고 싶다면 InternalsVisibleTo
// Assets/Scripts/Core/AssemblyInfo.cs
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyGame.Core.Tests")]
이제 MyGame.Core.Tests asmdef만 특별히 internal 멤버를 볼 수 있습니다. 단위 테스트에서 내부 API를 검증할 때 자주 씁니다.
5.3. 네임스페이스와 클래스 이름이 같으면 참사
❌ 잘못된 패턴
namespace MyGame.Logging
{
public class Logging // 네임스페이스와 이름이 같음
{
public static void Warn(string msg) { /* ... */ }
}
}
// 사용하는 쪽
namespace MyGame.Combat
{
using MyGame.Logging;
public class Bullet
{
void OnHit()
{
Logging.Warn("hit!");
// CS0118: 'Logging' 은 네임스페이스이지만 형식처럼 사용됨
}
}
}
컴파일러는 Logging 이 네임스페이스인지 클래스인지 구분하지 못해 오류를 냅니다.
✅ 올바른 패턴
namespace MyGame.Logging
{
public class Logger // 이름을 다르게
{
public static void Warn(string msg) { /* ... */ }
}
}
실무 규칙: 네임스페이스 마지막 세그먼트와 클래스 이름을 같게 짓지 않습니다. 폴더명을 네임스페이스에 그대로 반영하되, 클래스명은 구체적으로 (Logger, LogWriter, LogFormatter 등).
5.4. Unity asmdef 순환 참조 함정
❌ 잘못된 패턴
// Combat.asmdef → UI.asmdef 참조
// UI.asmdef → Combat.asmdef 참조 (순환!)
// Unity Editor 에러: Assembly with name 'MyGame.Combat' already exists
asmdef끼리는 순환 참조 금지 입니다. 사이에 공통 인터페이스 asmdef를 만들거나, 이벤트 기반(UnityEvent/C# event)으로 풀어야 합니다.
✅ 올바른 패턴
Combat.asmdef → Shared.Events.asmdef (인터페이스만 정의)
UI.asmdef → Shared.Events.asmdef
↑ 둘 다 Shared 를 참조하고 서로는 모름
6. [C# 버전별 변화] 네임스페이스와 using의 진화
6.1. C# 1.0 — 기본 네임스페이스와 using
가장 원시적인 형태. 중괄호 네임스페이스와 네임스페이스 using만 있었습니다.
using System;
namespace MyGame.Combat
{
public class DamageCalc
{
public int Hit() => 42;
}
}
6.2. C# 6.0 (2015) — using static 도입
정적 멤버를 이름 없이 호출 가능해졌습니다.
Before (C# 5 이하)
public double Area(double r)
{
return System.Math.PI * System.Math.Pow(r, 2);
}
After (C# 6+)
using static System.Math;
public double Area(double r)
{
return PI * Pow(r, 2);
}
IL 관점에서는 앞에서 본 대로 완전히 동일 — 순전히 소스 가독성을 위한 문법입니다.
6.3. C# 10 (2021) — 파일 범위 네임스페이스 & global using
Before: 중괄호 네임스페이스 (C# 9 이하)
namespace MyGame.Combat
{
public class DamageCalc
{
public int Hit() => 42;
}
}
After: 파일 범위 네임스페이스 (C# 10+)
namespace MyGame.Combat; // 세미콜론, 중괄호 없음
public class DamageCalc
{
public int Hit() => 42;
}
두 코드의 IL은 완전히 동일합니다.
.class public auto ansi beforefieldinit MyGame.Combat.DamageCalc
extends [System.Runtime]System.Object
{
.method public hidebysig instance int32 Hit () cil managed
{
IL_0000: ldc.i4.s 42
IL_0002: ret
}
}
파일 전체가 한 네임스페이스이면 파일 범위 네임스페이스가 들여쓰기 한 단계를 통째로 절약합니다. .NET 6+ 프로젝트 템플릿의 기본이 이미 이 형식입니다.
global using 도 C# 10 도입
// GlobalUsings.cs (보통 프로젝트 루트에 둠)
global using System;
global using System.Linq;
global using UnityEngine; // Unity의 경우
한 번만 쓰면 프로젝트 모든 .cs 파일에 적용됩니다.
6.4. C# 12 (2023) — 별칭이 모든 타입으로 확장
기존 using alias 는 네임스페이스와 일반 타입에만 쓸 수 있었지만, C# 12부터는 튜플·배열·포인터 까지 별칭을 붙일 수 있습니다.
Before (C# 11 이하) — 불가능
// error: using alias cannot be applied to tuple
using Position = (int X, int Y);
After (C# 12+) — 가능
using Position = (int X, int Y);
using IntArray = int[];
public class Program
{
static Position Origin => (0, 0);
static IntArray Scores = { 100, 80, 75 };
}
게임 코드에서 (int X, int Y) 같은 튜플을 반복적으로 쓸 때 별칭 한 줄로 의미를 명확히 할 수 있습니다.
7. [정리] 이것만 기억하세요
세 개념을 한 표로 압축합니다.
| 개념 | 존재하는 층 | 컴파일 후 | 런타임 비용 |
|---|---|---|---|
| 어셈블리(DLL/EXE) | 디스크(물리) | 그대로 남음 | 참조된 어셈블리가 처음 쓰일 때 로드 |
| 네임스페이스 | 소스(논리) | IL에는 타입 전체 이름으로 병합 | 0 (이름에 점이 들어갈 뿐) |
| using | 소스(편의) | IL에 흔적 없음 | 0 (컴파일 타임 전용) |
신입이 기억해야 할 핵심 체크리스트:
- [ ]
CS0246(타입을 찾을 수 없음) →.csproj의<PackageReference>/<ProjectReference>부터 확인 - [ ]
using만 추가해서는 어셈블리가 로드되지 않는다. 반드시 참조가 선행되어야 한다 - [ ] 어셈블리와 네임스페이스는 1:1이 아니다.
System.Text네임스페이스는 여러 DLL에 나뉘어 있다 - [ ]
using지시문은 IL에 흔적을 남기지 않는다. 어떤 형태든 런타임 비용은 0 - [ ] 이름 충돌(CS0104)은
using alias로 해결한다 (using UnityRandom = UnityEngine.Random;) - [ ] Unity에서 asmdef로 분리하면
internal은 어셈블리 경계에서 막힌다. 테스트 asmdef만 허용하려면InternalsVisibleTo를 쓴다 - [ ] 파일 범위 네임스페이스(
namespace X;)와global using은 C# 10+의 기본. 새 코드는 이 스타일을 쓴다 - [ ] 네임스페이스 마지막 세그먼트와 클래스 이름을 같게 짓지 않는다 (CS0118 방지)
- [ ] asmdef끼리는 순환 참조 금지 — 공통 인터페이스 asmdef를 사이에 둔다
- [ ]
ImplicitUsings=enable은 .NET 6+ 콘솔/웹에서만 자동 지원. Unity는 수동으로GlobalUsings.cs를 만든다
다음 글에서는 C#의 다른 핵심 주제를 다룹니다. 코드가 "어디에 담기는가" 를 이해했으니, 이제 "어떻게 실행되는가" 로 넘어갈 준비가 되었습니다.