반응형

[PART1.C# 런타임과 .NET 플랫폼 기초(3/11)] 어셈블리 · 네임스페이스 · using — 코드가 담기는 세 개의 그릇

어셈블리는 물리적 덩어리, 네임스페이스는 논리적 이름 공간, using은 이 둘을 편하게 엮는 컴파일러 지시문입니다.


1. [문제 제기] using만 추가했는데 왜 빌드가 안 되는가

Unity 프로젝트에서 TextMeshPro를 쓰려고 스크립트 상단에 다음 한 줄을 넣었다고 상상해 봅니다.

C#
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. 한 장의 그림으로 보기

세 개념의 관계를 먼저 한 그림으로 머릿속에 심어놓고 시작합니다.

어셈블리 · 네임스페이스 · using — 3개의 층

도서관으로 비유하면 이렇게 됩니다.

  • 어셈블리 = 도서관 건물. 책이 물리적으로 담겨 있는 실체.
  • 네임스페이스 = 도서관의 층/섹션 (예: "2층 과학 코너").
  • 타입 = 실제 (클래스, 구조체, 인터페이스).
  • using = "2층 과학 코너" 자주 가는 사람이 끊는 정기 출입증. 이 출입증이 있으면 2층 과학 코너의 양자역학 개론 대신 그냥 양자역학 개론 이라고 불러도 사서가 알아듣습니다.

중요한 점은 한 건물(어셈블리)에 여러 층(네임스페이스)이 있을 수도 있고, 같은 이름의 층이 건물마다 따로 있을 수도 있다는 것입니다. 예를 들어 System.Text 네임스페이스의 타입들은 System.Runtime.dll, System.Text.Encoding.dll 등 여러 어셈블리에 퍼져 있습니다. 어셈블리와 네임스페이스는 독립적입니다.

2.2. 가장 단순한 예시

C#
// 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.DamageCalcDamageCalc 로 줄여 썼습니다. 이 코드를 컴파일하면 IL은 어떤 모습일까요?

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) 단계에서만 사용됩니다. 컴파일러의 메타데이터 에미터는 TypeRefMemberRef 테이블에 항상 정규화된 이름을 기록합니다.


3. [내부 동작] 어셈블리의 내부 구조와 이름 해석 순서

3.1. 어셈블리(DLL/EXE)의 내부 구조

어셈블리는 단순히 "코드가 들어있는 파일" 이 아닙니다. Windows PE(Portable Executable) 포맷 위에 .NET 전용 헤더와 여러 메타데이터 테이블이 얹혀진 자기 서술적(self-describing) 바이너리 입니다.

어셈블리(.dll/.exe)의 내부 구조

각 섹션이 하는 일:

  • PE 헤더: OS가 이 파일을 실행 파일로 인식합니다.
  • CLR 헤더: 런타임(CLR, Common Language Runtime — .NET의 실행 엔진)에게 "이건 관리되는 코드다" 라고 알려줍니다.
  • 매니페스트: 이 어셈블리의 신분증과 의존성 목록. 버전, PublicKeyToken, AssemblyRef 로 기록된 다른 어셈블리 참조가 모두 여기에 있습니다.
  • 메타데이터 테이블: 타입·멤버 정보를 담은 구조화된 테이블. 리플렉션·IntelliSense·JIT이 모두 이것을 읽습니다.
  • IL 코드: 메서드 본문. CPU 독립적인 중간 언어이며 실행 시점에 JIT이 기계어로 변환합니다.

다음 C# 코드를 빌드해 실제 매니페스트에 어셈블리 참조가 어떻게 박히는지 확인해 봅니다.

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을 보면 메서드 호출부가 이렇게 나옵니다.

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)" 순서가 핵심입니다.

C#
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 로 명시적으로 해결해야 합니다.

C#
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 사용

C#
using static System.Math;

public class Program
{
    public static double AreaWithUsingStatic(double r)
    {
        return PI * Pow(r, 2);
    }
}

After: 전체 이름으로 호출

C#
public class Program
{
    public static double AreaWithFullName(double r)
    {
        return System.Math.PI * System.Math.Pow(r, 2);
    }
}

둘 다 빌드해서 IL을 비교하면:

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.RandomSystem.Random 을 함께 쓰면 이름이 충돌합니다. UnityEngine.Random 은 내부적으로 Unity 엔진의 전역 시드를 쓰고 스레드-안전하지 않습니다. 반면 System.Random 은 인스턴스별 시드를 갖고 서버/테스트 로직에 적합합니다.

Before: 매번 전체 이름으로 쓰기 (지저분함)

C#
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로 명확하게

C#
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.RandomNext() 호출마다 내부 배열 연산이 있어 초당 수만 번 호출되는 물리 시뮬레이션 루프에서는 오버헤드가 감지됩니다. 그런 핫패스는 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 유형에 따라 다음이 자동 적용됩니다(콘솔 기준).

C#
// 보이지 않게 자동 추가되는 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)

❌ 잘못된 패턴

C#
// 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>
C#
using Newtonsoft.Json;

public class SaveSystem
{
    public string Serialize(object data) => JsonConvert.SerializeObject(data);
}

빌드된 어셈블리의 매니페스트에는 이제 AssemblyRefNewtonsoft.Json 이 등록됩니다.

IL
.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에서 쓰려 함

C#
// 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 으로 승격

C#
namespace MyGame.Core
{
    public class GameSession  // 외부 asmdef에서도 접근 가능
    {
        public int PlayerId;
    }
}

✅ 올바른 패턴 2: 테스트·Friend Assembly 만 허용하고 싶다면 InternalsVisibleTo

C#
// Assets/Scripts/Core/AssemblyInfo.cs
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("MyGame.Core.Tests")]

이제 MyGame.Core.Tests asmdef만 특별히 internal 멤버를 볼 수 있습니다. 단위 테스트에서 내부 API를 검증할 때 자주 씁니다.

5.3. 네임스페이스와 클래스 이름이 같으면 참사

❌ 잘못된 패턴

C#
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 이 네임스페이스인지 클래스인지 구분하지 못해 오류를 냅니다.

✅ 올바른 패턴

C#
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만 있었습니다.

C#
using System;

namespace MyGame.Combat
{
    public class DamageCalc
    {
        public int Hit() => 42;
    }
}

6.2. C# 6.0 (2015) — using static 도입

정적 멤버를 이름 없이 호출 가능해졌습니다.

Before (C# 5 이하)

C#
public double Area(double r)
{
    return System.Math.PI * System.Math.Pow(r, 2);
}

After (C# 6+)

C#
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 이하)

C#
namespace MyGame.Combat
{
    public class DamageCalc
    {
        public int Hit() => 42;
    }
}

After: 파일 범위 네임스페이스 (C# 10+)

C#
namespace MyGame.Combat;   // 세미콜론, 중괄호 없음

public class DamageCalc
{
    public int Hit() => 42;
}

두 코드의 IL은 완전히 동일합니다.

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 도입

C#
// 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 이하) — 불가능

C#
// error: using alias cannot be applied to tuple
using Position = (int X, int Y);

After (C# 12+) — 가능

C#
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#의 다른 핵심 주제를 다룹니다. 코드가 "어디에 담기는가" 를 이해했으니, 이제 "어떻게 실행되는가" 로 넘어갈 준비가 되었습니다.

반응형

+ Recent posts