반응형

[PART1.C# 런타임과 .NET 플랫폼 기초(4/11)] 전역 using — 프로젝트 전체에 한 번에 적용 (C# 10)

global using 의 동작 원리 / <ImplicitUsings> 와의 관계 / Unity .asmdef 에서의 실전 함정


1. [문제 제기] — 파일마다 반복되는 네 줄짜리 주문

Unity 프로젝트를 새로 열어 스크립트 파일 10개를 만들어 보신 적이 있으실 겁니다. 열 개 파일 모두 상단은 거의 똑같은 모양입니다.

C#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
// (여기부터 진짜 코드)

새 스크립트를 만들 때마다 상단 네 줄을 복붙하거나, IDE 가 자동으로 채워 주기를 기다립니다. 10개 파일이면 40줄, 프로젝트에 스크립트가 200개라면 800줄이 모든 파일 최상단을 차지하는 잡음입니다. 한 파일 안에서도 스크롤을 내리기 전까진 클래스 선언이 보이지 않습니다.

문제는 단순한 잡음만이 아닙니다. 새 네임스페이스(예: DG.Tweening)를 프로젝트에 도입하면 그걸 쓰는 모든 파일에 같은 using 을 추가해야 합니다. 반대로 패키지를 제거할 땐 모든 파일에서 한 줄씩 지워야 합니다. "공통적으로 쓰는 네임스페이스를 한 곳에서 관리할 수는 없을까?" 가 바로 이번 주제가 답하는 질문입니다.

C# 10(.NET 6, 2021년 11월)이 도입한 global using 은 이 질문에 "예, 한 곳에서 선언하면 프로젝트 전체에 자동 적용됩니다" 라고 답합니다. 단, 흔히 놓치는 함정이 있습니다. Unity 에서는 .asmdef 때문에 "프로젝트 전체" 의 의미가 달라집니다.


2. [개념 정의] — "한 번만 선언하면 모두가 쓴다"

2.1 비유 — 회사 공용 출입증 vs 개별 출입증

일반 using파일 단위 출입증입니다. 방(파일)에 들어갈 때마다 어떤 네임스페이스를 "열람 허용" 했는지 그 방 안에서만 유효합니다. 옆 방에 가면 같은 출입증을 다시 발급받아야 합니다.

global using건물 전체 공용 출입증입니다. 프로젝트 입구(GlobalUsings.cs 같은 파일 한 곳)에서 한 번 등록해 두면, 그 건물(어셈블리) 안 모든 방이 자유롭게 드나들 수 있습니다.

2.2 구조 시각화

using 스코프 — 파일 범위 vs 프로젝트 범위

2.3 기본 문법

C#
// GlobalUsings.cs — 파일명은 관례. 아무 .cs 파일이든 가능
global using System;
global using System.Collections.Generic;
global using System.Linq;
C#
// Player.cs — 상단에 using 하나도 없이 바로 코드
namespace MyGame;

public class Player
{
    private List<int> scores = new();    // System.Collections.Generic 자동 적용
    public int Sum() => scores.Sum();    // System.Linq 자동 적용
}
global — 전역 사용 지시어 using 앞에 global 을 붙이면 해당 using 이 파일 범위가 아닌 어셈블리(컴파일 단위) 전체로 확장됩니다. C# 10 부터 지원.
예시: global using System.Text; 프로젝트 모든 파일에서 StringBuilder 를 네임스페이스 없이 사용 가능

2.4 세 가지 형태

C#
// 1) 일반 전역 using — 네임스페이스 가져오기
global using System;

// 2) 전역 static using — 정적 멤버를 클래스 접두사 없이
global using static System.Math;
// 이후 코드: var r = Sqrt(x);  // Math.Sqrt 아니라 Sqrt

// 3) 전역 using 별칭 — 타입에 짧은 이름 부여
global using UnityRandom = UnityEngine.Random;
// 이후 코드: UnityRandom.Range(0, 10);
  • global 키워드는 반드시 using 에 옵니다.
  • 네임스페이스 선언부 바깥, 파일 최상단에 위치해야 합니다.
  • 파일명은 관례적으로 GlobalUsings.cs 지만 규칙은 아닙니다. 여러 파일에 나눠 써도 동작은 같습니다.
쉬운 설명: 모든 .cs 파일 상단에 보이지 않는 using System; 이 자동으로 붙는다고 생각하시면 됩니다.
기술 정의: global using 은 C# 10 언어 기능으로, 하나의 컴파일 단위(대개 어셈블리) 안의 모든 구문 트리(syntax tree)에 해당 using 지시문을 주입하여 이름 해석(name resolution) 단계에서 참고되도록 합니다.

3. [내부 동작] — 컴파일러만 쓰고 버린다

3.1 컴파일러가 하는 일

global using 의 처리 시점 — 컴파일 타임 only

3.2 증거 — Before/After IL 완전 동일

다음 두 코드가 IL 레벨에서 동일한지 직접 확인합니다.

Before — 일반 using

C#
using System;
using System.Collections.Generic;

namespace GlobalUsingDemo;

public static class Program
{
    public static void Run()
    {
        var list = new List<int> { 1, 2, 3 };
        Console.WriteLine(list.Count);
    }
}

After — global using

C#
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;

// Program.cs — 상단 using 없음
namespace GlobalUsingDemo;

public static class Program
{
    public static void Run()
    {
        var list = new List<int> { 1, 2, 3 };
        Console.WriteLine(list.Count);
    }
}

두 버전을 각각 컴파일하여 ilspycmd 로 디컴파일한 IL 입니다(두 결과는 한 글자도 다르지 않습니다).

IL
.method public hidebysig static 
    void Run () cil managed 
{
    .maxstack 3
    .locals init (
        [0] class [System.Collections]System.Collections.Generic.List`1<int32>  // FQN 만 기록
    )

    IL_0000: nop
    IL_0001: newobj instance void class [System.Collections]System.Collections.Generic.List`1<int32>::.ctor()  // List<int> 힙 할당
    IL_0006: dup
    IL_0007: ldc.i4.1                                                         // 상수 1 적재
    IL_0008: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<int32>::Add(!0)  // 컬렉션 초기화 → Add 호출로 변환
    IL_000d: nop
    IL_000e: dup
    IL_000f: ldc.i4.2
    IL_0010: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<int32>::Add(!0)
    IL_0015: nop
    IL_0016: dup
    IL_0017: ldc.i4.3
    IL_0018: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<int32>::Add(!0)
    IL_001d: nop
    IL_001e: stloc.0
    IL_001f: ldloc.0
    IL_0020: callvirt instance int32 class [System.Collections]System.Collections.Generic.List`1<int32>::get_Count()  // List.Count 호출
    IL_0025: call void [System.Console]System.Console::WriteLine(int32)       // Console.WriteLine — [System.Console] 어셈블리 + FQN 으로만 기록
    IL_002a: nop
    IL_002b: ret
}

IL 분석 포인트

  1. using 개념이 IL 문법에 존재하지 않습니다. 모든 타입 참조는 [어셈블리]네임스페이스.타입 형태의 FQN 으로만 기록됩니다. 소스에서 짧게 썼든 길게 썼든 IL 단계에선 구분이 사라집니다.
  2. Before/After 가 바이트 단위로 동일합니다. diff 비교 결과 차이 0줄. 즉 global using런타임 성능·어셈블리 크기·GC 특성 어느 것에도 영향을 주지 않는 순수 컴파일 타임 편의 기능입니다.
  3. Unity 핫패스에서도 안전합니다. IL 이 동일하므로 IL2CPP 가 변환하는 C++ 코드도 동일, 생성되는 네이티브 바이너리도 동일합니다. Update 루프에서 쓰이든 어디에서 쓰이든 global using 때문에 프레임이 떨어질 가능성은 0입니다.

3.3 <ImplicitUsings> 의 정체

.NET 6 SDK 부터 .csproj 에 기본으로 들어가는 옵션입니다.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

이 옵션이 켜지면 빌드 시 SDK 가 obj/{Configuration}/{TargetFramework}/{ProjectName}.GlobalUsings.g.cs 파일을 자동 생성합니다. 실제 프로젝트에서 생성된 파일을 열어 보면 다음과 같습니다.

C#
// <auto-generated/>
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;
쉬운 설명: <ImplicitUsings> 는 "SDK 가 GlobalUsings.g.cs 를 대신 써 주는 MSBuild 기능" 일 뿐입니다. C# 언어 기능이 아닙니다.
기술 정의: <ImplicitUsings>enable</ImplicitUsings> 는 MSBuild 프로퍼티로, SDK 종류(Microsoft.NET.Sdk, Microsoft.NET.Sdk.Web, Microsoft.NET.Sdk.Worker)별로 미리 정의된 네임스페이스 목록을 global using 형태로 자동 생성되는 C# 파일에 기록하게 합니다.

SDK 종류별로 포함되는 네임스페이스가 다릅니다.

SDK 자동 포함 네임스페이스 (요약)
Microsoft.NET.Sdk (콘솔·라이브러리) System, System.Collections.Generic, System.IO, System.Linq, System.Net.Http, System.Threading, System.Threading.Tasks
Microsoft.NET.Sdk.Web 위 + Microsoft.AspNetCore.*, Microsoft.Extensions.* 일부
Microsoft.NET.Sdk.Worker 위 + Microsoft.Extensions.Configuration/DependencyInjection/Hosting/Logging

세밀하게 조정하고 싶으면 .csproj 에 직접 추가·제거할 수 있습니다.

<ItemGroup>
  <Using Include="System.Text" />           <!-- 추가 -->
  <Using Remove="System.Net.Http" />        <!-- 제거 -->
  <Using Include="MyGame.Helpers" Static="true" />  <!-- static 형태 -->
</ItemGroup>

4. [실전 적용] — 언제 쓰고 어떻게 정리하는가

4.1 Before — 파일마다 반복되는 using

C#
// Player.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Player : MonoBehaviour { /* ... */ }

// Enemy.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Enemy : MonoBehaviour { /* ... */ }

// Item.cs (이하 200 파일 동일)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Item : MonoBehaviour { /* ... */ }

한 파일당 5줄 × 200 파일 = 1000줄의 잡음이 모든 파일 상단을 차지합니다.

4.2 After — GlobalUsings.cs 한 곳에 집중

C#
// Assets/Scripts/Runtime/GlobalUsings.cs  ← 이 파일 하나만 작성
global using System;
global using System.Collections;
global using System.Collections.Generic;
global using UnityEngine;
global using UnityEngine.UI;
C#
// Player.cs — using 없이 바로 시작
public class Player : MonoBehaviour { /* ... */ }

// Enemy.cs — using 없이 바로 시작
public class Enemy : MonoBehaviour { /* ... */ }

// Item.cs — using 없이 바로 시작
public class Item : MonoBehaviour { /* ... */ }

각 파일 상단 5줄이 사라지고, 공통 using 관리 지점이 한 곳으로 집중됩니다. 새 네임스페이스를 도입할 때도 GlobalUsings.cs 한 줄 추가로 끝납니다.

IL 비교 결과: §3.2 에서 확인한 대로 두 버전의 IL 은 한 글자도 다르지 않습니다. 성능·크기·GC 어느 축에서도 변화 없습니다. 순수 "코드 가독성과 관리 편의성" 향상만 얻습니다.

4.3 마이그레이션 체크리스트

1. 프로젝트에서 가장 자주 쓰이는 using 목록 추출
   → IDE 통계 기능 (Rider: "Optimize Usings", VS: Roslyn 분석)
2. `GlobalUsings.cs` 파일 생성 후 공통 using 이전
   → 앞에 `global` 키워드 추가
3. IDE 일괄 정리 실행
   → VS: Ctrl+K, Ctrl+E / Rider: "Optimize Imports"
4. git diff 로 결과 리뷰
   → 파일당 상단 수 줄 감소가 기대값
5. 빌드 통과 확인

4.4 별칭 활용 — 긴 타입에 짧은 이름

C#
// GlobalUsings.cs
global using PlayerCache = System.Collections.Generic.Dictionary<int, MyGame.Player>;
global using WeakObjRef = System.WeakReference<UnityEngine.Object>;
C#
// Manager.cs — 짧고 의미가 명확한 이름으로 사용
public class Manager
{
    private PlayerCache _cache = new();        // Dictionary<int, Player> 대신
    private WeakObjRef? _tracked;               // WeakReference<UnityEngine.Object> 대신
}

Unity 핫패스에서 Dictionary<,> 제네릭을 여러 번 쓰는 코드가 가독성을 해친다면, 별칭으로 정리하면 의도가 분명해집니다. IL 관점에서는 여전히 같은 제네릭 인스턴스이므로 성능 차이는 없습니다.


5. [함정과 주의사항] — Unity 실전에서 가장 많이 밟는 지뢰

5.1 함정 ① — .asmdef 경계를 넘지 못한다

Unity 에서는 Assembly Definition (.asmdef) 하나가 별개의 C# 프로젝트(컴파일 단위) 와 같습니다. global using 의 유효 범위는 이 컴파일 단위 내부입니다.

Unity .asmdef 와 global using 의 스코프

❌ 잘못된 기대

C#
// Assets/GlobalUsings.cs (기본 어셈블리 Assembly-CSharp 에 위치)
global using UnityEngine;

// Assets/Scripts/Editor/GlobalUsings.cs — 이 파일 없음
// Assets/Scripts/Editor/EditorTool.cs
public class EditorTool : EditorWindow   // ❌ CS0246: 'EditorWindow' 를 찾을 수 없습니다
{
    // Editor.asmdef 쪽에는 global using 이 전파되지 않았음
}

✅ 올바른 방법

C#
// Assets/Scripts/Editor/GlobalUsings.cs — Editor 어셈블리 전용으로 별도 작성
global using UnityEngine;
global using UnityEditor;

.asmdef 별로 별도 GlobalUsings.cs 를 둬야 합니다. Runtime·Editor·Tests 어셈블리가 분리된 프로젝트라면 세 개 필요합니다.

5.2 함정 ② — System.Random vs UnityEngine.Random 충돌

Unity 에서 가장 자주 발생하는 이름 충돌입니다.

❌ 모호성 오류 발생

C#
// GlobalUsings.cs
global using System;          // System.Random 포함
global using UnityEngine;     // UnityEngine.Random 포함
C#
// Spawner.cs
public class Spawner : MonoBehaviour
{
    void Spawn()
    {
        var r = Random.Range(0f, 1f);  // ❌ CS0104: 'Random' 이(가) 'System.Random' 과 'UnityEngine.Random' 사이에서 모호함
    }
}

IL 관점: 컴파일 자체가 실패하므로 IL 은 생성되지 않습니다. 오류는 이름 해석(name resolution) 단계에서 발생합니다.

✅ 별칭으로 선제 해결

C#
// GlobalUsings.cs
global using System;
global using UnityEngine;
global using Random = UnityEngine.Random;     // 기본값: Unity 쪽
global using SysRandom = System.Random;       // System 쪽이 필요하면 이 이름 사용
C#
// Spawner.cs
public class Spawner : MonoBehaviour
{
    void Spawn()
    {
        var r = Random.Range(0f, 1f);   // ✅ UnityEngine.Random.Range
    }
}

// CryptoHelper.cs
public class CryptoHelper
{
    public int SecureSeed() => new SysRandom().Next();  // ✅ System.Random
}

5.3 함정 ③ — global using static 남용

global using static 은 정적 메서드를 클래스 접두사 없이 전역에서 쓸 수 있게 합니다. 편리해 보이지만 원점 추적을 어렵게 합니다.

❌ 혼란스러운 전역 static

C#
// GlobalUsings.cs
global using static UnityEngine.Debug;   // Log, LogWarning, LogError 전역
global using static UnityEngine.Mathf;   // Abs, Sin, Cos, Clamp 전역
global using static System.Math;         // Abs, Sin, Cos 전역 — 겹침!
C#
public class Player
{
    public void Take(int damage)
    {
        var clamped = Abs(damage);   // ❌ CS0121: 'Mathf.Abs(int)' 와 'Math.Abs(int)' 사이에서 모호
        Log("Hit!");                  // UnityEngine.Debug.Log 인지 로컬 Log 메서드인지 한눈에 안 보임
    }

    private void Log(string msg) { /* 같은 클래스에 Log 가 있으면? */ }
}

✅ 명시적 호출 유지

C#
// GlobalUsings.cs
global using UnityEngine;   // Debug, Mathf 는 네임스페이스 레벨로만 열고
// (global using static 은 쓰지 않음)
C#
public class Player
{
    public void Take(int damage)
    {
        var clamped = Mathf.Abs(damage);   // ✅ 어디서 온 Abs 인지 분명
        Debug.Log("Hit!");                  // ✅ 로그 호출 출처 명확
    }
}

IL 관점: 모호성 오류는 컴파일 실패로 이어져 IL 이 만들어지지 않습니다. 성공적으로 해석되더라도 IL 은 call [UnityEngine]UnityEngine.Mathf::Abs(int32) 처럼 FQN 을 쓰므로 런타임에는 문제가 없지만, 코드를 읽는 사람이 고생합니다.

5.4 함정 ④ — 라이브러리에 global using 을 넣고 기대

C#
// MyLib.dll 프로젝트의 GlobalUsings.cs
global using MyLib.Helpers;     // ← 내 라이브러리 내부에서만 유효
C#
// 이 DLL 을 참조하는 앱 프로젝트에서
// MyLib.Helpers 가 자동으로 열리지 않음 — 앱은 별도 using 필요
public class App
{
    public void Run()
    {
        Helper.DoIt();   // ❌ CS0246: 'Helper' 를 찾을 수 없음
    }
}

✅ 올바른 이해

C#
// 앱 프로젝트 쪽에서도 별도 선언
using MyLib.Helpers;           // 또는
global using MyLib.Helpers;    // 앱 프로젝트 GlobalUsings.cs 에서

global using 은 해당 어셈블리 내부 에만 영향을 줍니다. DLL 을 참조하는 외부 어셈블리에는 전파되지 않습니다. 라이브러리 사용자가 우리 타입을 편하게 쓸 수 있도록 하려면 문서로 안내해야지, global using 으로 해결되지 않습니다.


6. [C# 버전별 변화] — 파일 상단 정리의 역사

C# 버전 변화 비고
C# 1.0 2002 using 도입 (파일 범위) 모든 파일에 반복
C# 6.0 2015 using static 도입 (파일 범위) 정적 메서드 접두사 생략 가능
C# 10.0 2021 global using 도입 (프로젝트 범위) .NET 6 과 함께
C# 10.0 2021 <ImplicitUsings> MSBuild 옵션 도입 .NET 6 SDK 기본값
C# 12.0 2023 using 별칭이 튜플·제네릭·포인터까지 확장 using Point = (int X, int Y);

6.1 C# 6 이전 vs C# 10 이후

Before (C# 6 이전 — 모든 파일 반복)

C#
// FileA.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class FileA { /* ... */ }

After (C# 10 이후 — 한 곳 집중 + SDK 자동 주입)

C#
// GlobalUsings.cs (직접 작성하거나 <ImplicitUsings> 가 자동 생성)
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;

// FileA.cs — 상단 using 없이 바로 시작
public class FileA { /* ... */ }

IL 비교: §3.2 의 결론과 동일 — 모든 버전에서 IL 은 동일합니다. 달라진 것은 오직 소스 상단의 모양일 뿐입니다.

6.2 C# 12 — 별칭 확장

C# 10 에서는 using Alias = 단순타입명 만 가능했으나 C# 12 부터 튜플·제네릭 전체가 별칭 가능해졌습니다.

C#
// C# 12 이후
global using Position = (int X, int Y);                                  // 튜플 별칭
global using PlayerMap = System.Collections.Generic.Dictionary<int, string>;  // 제네릭 별칭
C#
public class Board
{
    public Position StartAt = (0, 0);
    public PlayerMap Names = new();
}

자세한 내용은 다음 토픽([5] using 별칭으로 튜플·제네릭에 이름 붙이기)에서 다룹니다.


7. [정리] — 이것만 기억하세요

  • [ ] global using 은 C# 10 기능으로, 해당 키워드가 붙은 using어셈블리(컴파일 단위) 전체 로 확장합니다.
  • [ ] 컴파일러가 짧은 타입명을 FQN 으로 해석할 때만 쓰이고, IL 에는 using 의 흔적이 전혀 남지 않습니다. Before/After IL 은 바이트 단위로 동일합니다.
  • [ ] <ImplicitUsings>enable</ImplicitUsings> 는 MSBuild 옵션으로, obj/.../GlobalUsings.g.cs 를 SDK 가 자동 생성하는 기능입니다. C# 언어 기능과는 별개입니다.
  • [ ] 스코프는 어셈블리 단위. Unity 에서는 .asmdef 하나 = 어셈블리 하나 이므로, Editor·Runtime·Tests 각각에 별도 GlobalUsings.cs 가 필요합니다.
  • [ ] System.RandomUnityEngine.Random 처럼 이름이 겹치는 타입은 전역 별칭(global using Random = UnityEngine.Random;)으로 선제 해결합니다.
  • [ ] global using static 은 원점 추적을 어렵게 하므로 신중히 사용합니다. 특히 Debug, Mathf, Math 같이 겹치는 정적 클래스를 함께 여는 것은 피합니다.
  • [ ] 라이브러리 내부의 global using소비자에게 전파되지 않습니다. 자기 어셈블리 안에서만 유효합니다.
  • [ ] 성능·메모리·어셈블리 크기 걱정은 불필요합니다. 순수 코드 스타일·관리 편의성 향상 기능입니다.
반응형

+ Recent posts