반응형

[PART1.C# 런타임과 .NET 플랫폼 기초(7/11)] .NET Framework · .NET Core · .NET 5+ · Mono — 이름이 왜 이렇게 많은가

왜 분기되었고 왜 통합되었는가 / CLR·CoreCLR·Mono의 구조 차이 / Unity는 왜 아직 Mono를 쓰는가


1. [문제 제기] 왜 이 개념이 필요한가

입문자가 C#을 시작하면서 가장 먼저 벽에 부딪히는 지점은 문법이 아닙니다. 문서와 강의마다 이름이 다르다는 사실입니다.

  • "이 라이브러리는 .NET Framework 4.8 전용입니다."
  • "NuGet 패키지를 받는데 net8.0, netstandard2.0, net48, monoandroid가 모두 보입니다."
  • "Unity 프로젝트를 열었더니 Player Settings에 Mono와 IL2CPP 중 하나를 고르라고 합니다."
  • "Visual Studio에서 프로젝트를 새로 만들 때 '.NET Framework'와 '.NET'이 따로 있습니다. 뭐가 다른가요?"

이름이 이렇게 많은 이유는 단순합니다. C#이라는 언어 하나 뒤에, 서로 다른 시대에 서로 다른 목적으로 만들어진 런타임이 여럿 존재하기 때문입니다. 그리고 Unity 모바일 개발자에게 이 구분은 특히 중요합니다. Unity가 쓰는 런타임은 표준 .NET이 아니라 Mono를 커스텀한 것이고, iOS 빌드 시 쓰는 IL2CPP는 아예 C#을 C++로 바꿔버리는 별도 경로입니다. 이 배경을 모른 채 "최신 C# 14 기능을 Unity에서 썼더니 런타임 에러가 난다"는 상황을 만나면 원인을 찾을 수 없습니다.

이 글은 .NET Framework · .NET Core · .NET 5+ · Mono · Unity 런타임이 어떤 순서로 등장했고, 왜 분기되었으며, 왜 통합되었는지, 그리고 오늘 내가 쓰는 C# 코드가 어떤 런타임 위에서 도는지를 입문자 관점에서 정리합니다.


2. [개념 정의] 다섯 개의 런타임, 하나의 언어

비유: 같은 설계도, 다른 공장

C#은 설계도입니다. 이 설계도대로 물건을 찍어내는 공장이 런타임입니다. 공장은 여러 개 있습니다.

  • Microsoft 본사 공장 (.NET Framework) — 2002년부터 Windows 땅에만 세워진 최초의 공장입니다.
  • Microsoft 신형 모듈 공장 (.NET Core → .NET 5+) — 2016년에 Linux·macOS 어디서든 조립할 수 있게 재설계된 공장입니다.
  • 커뮤니티 개조 공장 (Mono) — 2004년에 다른 회사가 원본 설계도를 보고 복제한 공장입니다. 나중에 Microsoft가 그 회사를 인수했습니다.
  • Unity 전용 개조 공장 (Unity Mono) — Unity가 Mono를 게임용으로 뜯어고친 공장입니다.
  • Unity의 특수 변환소 (IL2CPP) — 공장이 아니라, C# 설계도를 C++ 설계도로 번역한 뒤 각 플랫폼 네이티브 컴파일러에게 넘기는 변환소입니다.

언어(C#)는 하나지만, 어느 공장에서 찍어내느냐에 따라 최종 제품의 동작과 성능이 달라집니다.

전체 계보도

2002

이 그림 한 장에 모든 이야기가 담겨 있습니다. 아래부터는 각 상자의 내부로 들어가 왜 그 공장이 필요했는지, 지금은 어떻게 쓰이는지를 하나씩 살펴봅니다.

가장 먼저 기억해야 할 한 줄

오늘 신규 프로젝트를 시작한다면 대답은 하나입니다. 현재 지원되는 LTS 버전(.NET 8 또는 .NET 10)입니다. 나머지 이름은 "왜 다른 이름이 존재하는지"를 이해하기 위한 맥락일 뿐입니다.

3. [내부 동작] 각 런타임의 아키텍처 차이

런타임을 구성하는 핵심 세 요소는 CLR(Common Language Runtime, 코드를 실행하는 가상 머신), JIT(Just-In-Time 컴파일러, IL을 기계어로 변환), BCL(Base Class Library, System.* 같은 표준 라이브러리)입니다. 이 셋이 런타임마다 조금씩 다릅니다.

IL(Intermediate Language) — C# 컴파일러는 소스코드를 바로 기계어로 바꾸지 않고, 중간 형태인 IL(중간 언어)로 번역합니다. IL은 실행 시점에 JIT가 현재 CPU에 맞는 기계어로 변환하거나(JIT), 빌드 시점에 미리 변환합니다(AOT).

CLR vs CoreCLR vs Mono

.NET Framework · CLR

3-1. .NET Framework의 CLR — Windows에 묶인 원조

.NET Framework는 Windows 운영체제 안에 설치된 공용 컴포넌트입니다. 즉, 개발자가 앱을 배포할 때 런타임을 함께 넣지 않았습니다. 사용자 PC에 이미 .NET Framework 4.7.2 같은 버전이 설치되어 있다고 가정하고 앱만 배포했습니다.

  • 한 PC에는 .NET Framework 버전이 하나만 활성화됩니다(in-place update). 4.5가 4.0을 덮어씁니다. 이게 나중에 업데이트 충돌의 큰 원인이 됩니다.
  • BCL에 Windows 의존 기능이 대거 포함되어 있습니다. System.Windows.Forms, System.Drawing(GDI+), 레지스트리 접근, WCF, Workflow Foundation 등. 이를 Linux로 포팅하는 것은 사실상 불가능했습니다.
  • JIT은 초기에는 JIT64를 쓰다가 .NET 4.6부터 RyuJIT로 교체됐습니다. RyuJIT는 시작 시간을 최대 30% 줄이고 SIMD(한 번의 명령으로 여러 데이터를 동시에 처리) 같은 현대 CPU 기능을 지원합니다.

3-2. .NET Core의 CoreCLR — 처음부터 다시 쓴 엔진

.NET Core는 기존 CLR의 유지보수 버전이 아니라 처음부터 재설계된 런타임입니다. 핵심 설계 변경은 세 가지입니다.

  1. OS 바인딩 제거 — Windows 커널 호출에 의존하던 코드를 크로스플랫폼 추상화로 바꿨습니다. 그래서 Linux·macOS에서 동일한 바이너리가 돕니다.
  2. 앱 로컬 런타임 — 런타임이 OS에 설치되는 게 아니라 앱 폴더에 함께 배포됩니다. 버전 A와 버전 B가 한 PC에서 동시에 다른 폴더에서 실행됩니다(side-by-side).
  3. 모듈화된 BCL — 모든 라이브러리를 하나의 거대한 mscorlib.dll에 몰아넣지 않고 NuGet 단위로 쪼갰습니다. 필요 없는 기능은 배포에 포함되지 않습니다.

3-3. Mono — 복제에서 시작해 Unity의 기반이 된 런타임

Mono는 2004년, Microsoft가 .NET을 Windows 전용으로 가둬두던 시절에 Miguel de Icaza가 시작한 오픈소스 프로젝트입니다. 목표는 "Linux에서도 C# 앱을 돌리자"였고, 이를 위해 CLR을 밖에서 관찰한 명세(ECMA-335)만 보고 독자 구현했습니다.

  • 독자적인 JIT 컴파일러와 GC(초창기 Boehm GC — 보수적 수집기)를 가집니다.
  • iOS에서는 JIT이 금지되어 있어서(보안 모델), Full AOT라는 "모든 IL을 빌드 시점에 네이티브로 변환해두는" 모드를 먼저 개척했습니다. 이 기술이 나중에 IL2CPP의 뿌리가 됩니다.
  • 2011년 Xamarin이 Mono를 상업화해 iOS·Android 앱 개발 도구로 발전시켰고, 2016년 Microsoft가 Xamarin을 인수하면서 Mono는 Microsoft 소유가 되었습니다.

3-4. Unity 런타임 — Mono 포크 + IL2CPP

Unity가 오늘 쓰는 C# 런타임은 엄밀히 말해 세 가지가 섞여 있습니다.

  1. 에디터 안의 Unity Mono — 에디터에서 Play 버튼을 눌렀을 때 스크립트를 실행하는 런타임. Mono 2.x 기반을 Unity가 독자적으로 포크해 오래 유지한 버전이며, 수년간 .NET Framework 3.5 수준의 BCL에 묶여 있었다가 최근 Unity 2021 LTS에서 .NET Standard 2.1 기반으로 올라왔습니다.
  2. IL2CPP — 빌드 옵션. C# → IL → C++ → 네이티브 코드 순으로 변환합니다. iOS는 IL2CPP가 필수이고 다른 플랫폼에서도 성능·리버스엔지니어링 방지 목적으로 선택됩니다.
  3. Boehm GC — Unity Mono와 IL2CPP가 공통으로 쓰는 GC입니다. 세대별이 아니라 전역 스톱더월드 방식이라, 큰 할당이 쌓이면 프레임 단위 스파이크가 크게 발생합니다. 이게 "Unity에서는 GC.Alloc을 줄여라"라는 격언의 근원입니다.

즉, Unity의 C#은 문법은 최신이어도 런타임 본체는 .NET 10과 다른 계보입니다. 이 사실이 Unity 개발자에게는 결정적입니다.

3-5. 이름 하나에 대응되는 것들 — 한눈 요약

이름 런타임 엔진 JIT GC BCL 주 용도(현재)
.NET Framework 4.8.1 CLR RyuJIT 세대별 Windows 포함 BCL Windows 레거시(WPF·WinForms·ASP.NET)
.NET Core 1.0~3.1 CoreCLR RyuJIT 세대별 단일 BCL (.NET 5에 흡수)
.NET 5/6/7/8/9/10 CoreCLR + Native AOT RyuJIT 세대별 + 동적 PGO 단일 BCL 신규 서버·CLI·웹·크로스플랫폼 표준
Mono (Microsoft) Mono VM Mono JIT SGen 또는 Boehm 독자 BCL(호환) Xamarin → .NET MAUI로 흡수
Unity Mono Unity-custom Mono Mono JIT Boehm 축소 BCL Unity 에디터 / 일부 모바일 빌드
Unity IL2CPP 런타임 없음 (AOT) 없음 (C++ 컴파일) Boehm 축소 BCL iOS(필수) · Android · 콘솔

4. [실전 적용] 코드가 어느 런타임에서 도는지 확인하는 법

4-1. TFM(Target Framework Moniker)을 읽는 법

.csproj 파일을 열면 <TargetFramework> 가 보입니다. 이 문자열 하나로 "내 코드가 어떤 런타임용으로 빌드되는지"가 결정됩니다.

C#
// 예시 1: 신규 프로젝트 — .NET 8 LTS
// Example.csproj
// <TargetFramework>net8.0</TargetFramework>

// 예시 2: Unity 패키지로 배포할 공용 라이브러리
// <TargetFramework>netstandard2.1</TargetFramework>

// 예시 3: Windows 전용 레거시 유지보수
// <TargetFramework>net48</TargetFramework>

// 예시 4: 여러 런타임을 동시에 지원(멀티 타겟)
// <TargetFrameworks>net8.0;net48;netstandard2.1</TargetFrameworks>
TFM 접두사 의미 예시
net8.0, net10.0 통합 .NET 5+ 계열 신규 서버·웹·CLI
net48, net472 .NET Framework Windows 전용 레거시 유지보수
netstandard2.0, netstandard2.1 여러 런타임 공통 API 규격 라이브러리
net8.0-windows, net8.0-android 플랫폼 확장 TFM MAUI 등
monoandroid12.0 Xamarin 시대 유산 MAUI 이전 안드로이드
.NET Standard란 런타임이 아니라 '라이브러리용 API 규격'입니다. Unity 2021 LTS는 netstandard2.1 호환이므로, netstandard2.1로 빌드한 DLL은 Unity에도, .NET 8 서버에도 그대로 사용할 수 있습니다. "이 라이브러리는 어디에서나 돈다"는 약속이 .NET Standard의 본질입니다.

4-2. 실행 중 런타임 식별하기

코드가 지금 어떤 런타임 위에서 돌고 있는지RuntimeInformation으로 확인합니다. 이 예시는 Unity에서도, 서버에서도 모두 실행 가능합니다.

C#
using System;
using System.Runtime.InteropServices;

public static class RuntimeProbe
{
    public static void Print()
    {
        // FrameworkDescription: ".NET Framework 4.8.1" / ".NET 8.0.4" / "Mono 6.12.0.182" 등
        Console.WriteLine($"Runtime     : {RuntimeInformation.FrameworkDescription}");
        Console.WriteLine($"OS          : {RuntimeInformation.OSDescription}");
        Console.WriteLine($"Process Arch: {RuntimeInformation.ProcessArchitecture}");

        // Mono 여부는 타입 존재로 판별 (Unity 권장 방식과 동일)
        bool isMono = Type.GetType("Mono.Runtime") != null;
        Console.WriteLine($"Is Mono?    : {isMono}");
    }
}
  • .NET 8 콘솔 앱에서 실행 시 FrameworkDescription.NET 8.0.x로 나옵니다.
  • Unity 에디터에서 실행 시에는 Mono 5.x 또는 Mono 6.x 문자열이 나옵니다(Unity 버전에 따라 다름).
  • iOS IL2CPP 빌드에서는 FrameworkDescription이 여전히 Mono 계열로 표기되지만 Mono.Runtime 타입은 존재하지 않는 경우가 있어(IL2CPP 변환 과정에서 제거) 완벽한 판별은 아닙니다. 확실한 구분은 전처리 심볼을 씁니다(다음 절).

4-3. 전처리 심볼로 컴파일 시점에 분기하기

#if컴파일 시점에 런타임별 코드 경로를 나누는 것은 라이브러리에서 흔한 패턴입니다.

C#
public static class PlatformPath
{
    public static string Home()
    {
#if NET8_0_OR_GREATER
        // .NET 8 이상 전용 API — Environment.GetFolderPath에 새 오버로드가 있음
        return Environment.GetFolderPath(
            Environment.SpecialFolder.UserProfile,
            Environment.SpecialFolderOption.Create);
#elif NETSTANDARD2_1 || NETSTANDARD2_0
        // Unity 및 범용 — 두 오버로드 모두에서 동작하는 안전한 버전
        return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
#elif NET48
        // .NET Framework 레거시 — Windows 전용 API를 덧붙일 수 있음
        return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
#else
        return Environment.CurrentDirectory;
#endif
    }
}
#if — 조건부 컴파일 지시문 #if 블록 안의 코드는 지정한 심볼이 정의돼 있을 때만 컴파일에 포함됩니다. 포함되지 않은 분기는 어셈블리에 IL이 아예 생성되지 않습니다.
예시: #if UNITY_IOS ... #endif — Unity iOS 빌드에서만 포함됩니다.
  • NET8_0_OR_GREATER, NETSTANDARD2_1, NET48 같은 심볼은 SDK가 자동으로 정의합니다.
  • Unity 환경에서는 추가로 UNITY_2021_3_OR_NEWER, UNITY_EDITOR, UNITY_IOS, UNITY_ANDROID, ENABLE_IL2CPP, ENABLE_MONO 같은 심볼을 Unity가 주입합니다.

4-4. IL 레벨에서 #if가 어떻게 동작하는지 확인

조건부 컴파일은 "지워진 분기는 IL에서 흔적이 없다"는 것을 직접 확인해볼 가치가 있습니다. 아주 단순화한 예시로 살펴봅니다.

C#
public static class PathResolver
{
    public static string Home()
    {
#if NET8_0_OR_GREATER
        return "NET8";
#else
        return "LEGACY";
#endif
    }
}

이 코드를 net8.0으로 컴파일한 결과 IL은 아래와 같습니다(ilspycmd 출력에서 핵심만 발췌).

IL
.method public hidebysig static string Home() cil managed
{
    // 본문: "NET8" 문자열만 남음 — "LEGACY" 분기는 IL에 없다
    .maxstack 8
    IL_0000: ldstr      "NET8"    // 문자열 리터럴을 스택에 푸시
    IL_0005: ret                   // 스택 값을 반환
}

핵심 포인트는 LEGACY 문자열이 어셈블리에 아예 포함되지 않는다는 것입니다. 런타임 분기(if 문)와 달리 #if컴파일 단계에서 코드를 삭제합니다. 다른 플랫폼용 코드가 탈락한 분기로 실수로 실행될 위험이 0이라는 뜻입니다. Unity의 UNITY_IOS 같은 심볼도 정확히 같은 방식으로 동작합니다.

4-5. Unity 핫패스에서 BCL 차이가 드러나는 순간

서버 .NET 8과 Unity Mono의 BCL은 같은 이름의 API라도 구현이 다릅니다. 대표 사례가 string 포매팅입니다.

C#
using UnityEngine;

public class HpBar : MonoBehaviour
{
    private int _hp;
    private int _maxHp = 100;

    // ❌ 나쁜 예 — 매 프레임 string.Format으로 GC 할당 발생
    void Update()
    {
        GetComponent<UnityEngine.UI.Text>().text =
            string.Format("{0} / {1}", _hp, _maxHp);
    }
}
C#
using System.Text;
using UnityEngine;

public class HpBar : MonoBehaviour
{
    private int _hp;
    private int _maxHp = 100;
    private readonly StringBuilder _sb = new StringBuilder(16);

    // ✅ 좋은 예 — StringBuilder 재사용으로 할당 회피
    void Update()
    {
        _sb.Clear();
        _sb.Append(_hp).Append(" / ").Append(_maxHp);
        GetComponent<UnityEngine.UI.Text>().text = _sb.ToString();
    }
}

양쪽 버전을 컴파일한 IL 핵심을 보면 차이가 명확합니다.

IL
// 나쁜 예의 string.Format 호출 경로
IL_0012: ldstr     "{0} / {1}"
IL_0017: ldarg.0
IL_0018: ldfld     int32 HpBar::_hp
IL_001d: box       [System.Runtime]System.Int32   // int -> object 박싱 발생
IL_0022: ldarg.0
IL_0023: ldfld     int32 HpBar::_maxHp
IL_0028: box       [System.Runtime]System.Int32   // 두 번째 박싱
IL_002d: call      string [System.Runtime]System.String::Format(
                     string, object, object)       // object 배열 내부 할당
IL
// 좋은 예의 StringBuilder 경로
IL_0007: ldfld     class [System.Runtime]System.Text.StringBuilder HpBar::_sb
IL_000c: callvirt  instance void [System.Runtime]System.Text.StringBuilder::Clear()
IL_0011: ldfld     class [System.Runtime]System.Text.StringBuilder HpBar::_sb
IL_0016: ldarg.0
IL_0017: ldfld     int32 HpBar::_hp
IL_001c: callvirt  instance class [System.Runtime]System.Text.StringBuilder
                     System.Text.StringBuilder::Append(int32)     // 박싱 없음

string.Formatobject 매개변수 때문에 intbox(값 타입을 힙에 래핑하는 IL 명령)됩니다. 매 프레임 HP UI가 한 번 호출되면 초당 60회 × 두 번의 박싱 + 문자열 객체 생성이 쌓입니다. Unity의 Boehm GC는 세대별이 아니라 전역 스톱더월드이기 때문에, 이런 누적 할당이 수 MB를 넘어서면 수십 ms짜리 프레임 드랍으로 돌아옵니다. 같은 코드가 .NET 8 서버에서는 세대별 GC 덕분에 Gen0에서 빠르게 회수되어 티가 덜 납니다. "같은 코드인데 왜 모바일에서만 느려지나"의 가장 흔한 원인입니다.


5. [함정과 주의사항]

5-1. "최신 C# 문법 = 어디서나 사용 가능"이 아니다

C# 언어 버전런타임 버전은 별개입니다. Unity에서 C# 12/13 문법을 쓰려고 시도하면 컴파일은 돼도 런타임 API가 없어서 실행 시점에 터지는 경우가 많습니다.

C#
// ❌ Unity 2021 LTS(Mono 기반)에서 런타임 에러 가능
// C# 11의 static abstract interface member는 .NET 7+ 런타임 필수
public interface IHasDefault<T>
{
    static abstract T Default { get; }   // Unity Mono에서 미지원
}
C#
// ✅ 같은 의도를 런타임 중립적으로 표현
public interface IHasDefault<T>
{
    T GetDefault();                       // 인스턴스 메서드로 대체
}

// 또는 팩토리 델리게이트
public static class Defaults
{
    public static T Get<T>(Func<T> factory) => factory();
}

판단 기준: Unity를 타겟팅한다면 Unity가 지원한다고 명시한 C# 언어 버전(Unity 공식 문서의 Scripting > C# Compiler 페이지 참조)을 넘지 않습니다. 단순히 Rider가 빨간 줄을 긋지 않는다고 안전한 것이 아닙니다.

5-2. .NET Standard 2.0만 쓰면 어디서든 돈다고 착각하지 않기

많은 블로그에 "모두에서 돌리려면 netstandard2.0으로 빌드하라"고 쓰여 있습니다. 틀린 말은 아니지만 현실은 더 복잡합니다.

  • netstandard2.0API 호환만 보장합니다. 성능 특성은 런타임마다 다릅니다. Span<T>, Memory<T> 같은 고성능 API는 netstandard2.1 이상에서만 제공됩니다.
  • 같은 API라도 Unity Mono에서는 구현이 더 느립니다(예: Regex, Linq, Thread).
  • netstandard2.0은 .NET Framework 4.6.1+ 호환이지만, 실제 배포 시 System.Memory.dll 같은 의존성이 따로 필요해서 설정이 복잡해집니다.

판단 기준: 오늘 기준 라이브러리 호환성 판단은 이 한 줄입니다.

Unity를 포함해 배포한다면 netstandard2.1, .NET 플랫폼에만 배포한다면 net8.0(또는 상위), 두 가지를 멀티 타겟팅합니다.

5-3. Unity의 "Mono 백엔드"는 Microsoft Mono의 최신과 다르다

Unity가 쓰는 Mono는 2016~2017년경 포크된 Unity 전용 Mono입니다. Microsoft가 소유한 "Microsoft Mono" 본류는 계속 업데이트됐지만 Unity는 장기간 자체 포크를 유지했습니다. 그래서 아래와 같은 혼동이 발생합니다.

C#
// Microsoft Mono 최신에서는 잘 도는 API가 Unity Mono에서는 없을 수 있음
using System.Net.Http;
using System.Net.Http.Json;   // ❌ Unity Mono에는 System.Net.Http.Json이 없음

public async Task<MyDto?> Fetch()
{
    using var client = new HttpClient();
    return await client.GetFromJsonAsync<MyDto>("https://api.example.com/x");
}
C#
// ✅ Unity에서는 UnityWebRequest + JsonUtility 또는 Newtonsoft.Json.Unity 사용
using UnityEngine;
using UnityEngine.Networking;

public class Api : MonoBehaviour
{
    public IEnumerator Fetch(System.Action<MyDto> onDone)
    {
        using var req = UnityWebRequest.Get("https://api.example.com/x");
        yield return req.SendWebRequest();
        if (req.result == UnityWebRequest.Result.Success)
            onDone(JsonUtility.FromJson<MyDto>(req.downloadHandler.text));
    }
}

5-4. iOS 빌드는 JIT가 없다 — 리플렉션·동적 코드 주의

IL2CPP는 Ahead-Of-Time(빌드 시점에 네이티브 코드로 모두 변환) 방식이므로, 실행 시점에 IL을 새로 만들어내는 코드는 동작하지 않습니다.

C#
// ❌ iOS IL2CPP에서 런타임 에러
using System.Reflection.Emit;

var dynMethod = new DynamicMethod("Add", typeof(int), new[] { typeof(int), typeof(int) });
var il = dynMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
// IL2CPP는 실행 시점에 새 IL을 JIT할 수 없음 → PlatformNotSupportedException
C#
// ✅ 표현식 트리도 내부적으로 IL 생성이 필요해 동일 문제
// 정적 코드 생성(Source Generator)으로 대체하거나 직접 델리게이트를 작성
public static int Add(int a, int b) => a + b;
public static readonly Func<int, int, int> AddFn = Add;

판단 기준: Reflection.Emit, Expression.Compile(), 일부 리플렉션 기반 DI 컨테이너는 IL2CPP에서 터집니다. Unity 포팅을 생각한다면 소스 제너레이터수동 구현으로 대체하는 설계를 초반부터 고려합니다.

5-5. .NET Framework 4.x 프로젝트를 그대로 net8.0으로 바꾸면 안 되는 것들

레거시 유지보수 중 흔한 실수입니다. net8.0에는 다음이 없습니다.

  • WCF 서버 (클라이언트는 System.ServiceModel.Primitives NuGet으로 일부 가능)
  • Web Forms (ASP.NET Web Forms는 Core로 포팅되지 않음)
  • AppDomain의 일부 기능 (Assembly Unload는 AssemblyLoadContext로 대체)
  • 일부 System.Drawing 기능 (Linux에서 비지원)

판단 기준: 위 기능에 의존하는 레거시 프로젝트는 마이그레이션을 강행하기보다 .NET Framework 4.8.1그대로 둡니다. Microsoft는 Windows가 지원되는 한 계속 지원한다고 공언했습니다.


6. [C# 버전별 변화] 이름과 릴리스 전략의 발전

이 주제는 언어 문법이 아니라 플랫폼 전략의 이야기이므로, C# 버전이 아니라 .NET 이름·릴리스 주기의 변화를 정리합니다.

시기 버전·이름 핵심 변화
2002 .NET Framework 1.0 Windows 전용 런타임 출시
2004 Mono 1.0 오픈소스 복제 런타임 출시 (프로젝트 시작 2001~2002)
2014 .NET Core 발표 크로스플랫폼·오픈소스 전환 시작
2016 .NET Core 1.0 · .NET Standard 1.0 재설계된 CoreCLR 출시 / 라이브러리용 API 규격 분리
2019 .NET Framework 4.8 Windows 전용 라인 유지보수 모드 진입
2020 .NET 5 "Core" 이름 제거. 4를 건너뛴 이유는 .NET Framework 4.x와 버전 충돌 회피
2021 .NET 6 (LTS) · .NET MAUI 프리뷰 Xamarin이 MAUI로 재탄생하여 .NET에 통합
2022 .NET 7 · Native AOT 정식 서버/CLI용 AOT 경로 제공
2023 .NET 8 (LTS) · 동적 PGO 기본 활성 JIT 최적화가 런타임 프로파일을 반영
2025 .NET 10 (LTS 예정) 매년 11월 릴리스, 짝수(6·8·10)는 LTS 유지

Unity 측 변화도 별도 축으로 이해해야 합니다.

  • Unity 2017 이전: Mono 2.x(.NET 3.5 수준 BCL).
  • Unity 2018~2020: Mono 업그레이드, .NET 4.x equivalent API 레벨 추가.
  • Unity 2021 LTS 이후: .NET Standard 2.1 기반으로 정착. 최근 Unity 6부터는 공식적으로 CoreCLR 통합 로드맵이 진행 중(차세대 런타임 전환 예고).

즉, Unity의 런타임도 결국 CoreCLR 계열로 수렴하는 방향이지만, 기존 프로젝트가 전환되기까지는 상당한 시간이 걸립니다. 지금 작성하는 Unity 코드는 여전히 Mono + Boehm GC를 전제로 해야 안전합니다.


7. [정리]

이 글에서 기억해야 할 한 줄 요약

  • C#은 하나, 런타임은 여러 개입니다. 오늘 쓰는 공식 선택지는 .NET Framework(레거시) / .NET 5+(현재 표준) / Unity Mono·IL2CPP(게임)로 좁혀집니다.

이것만 기억하라 — 실무 체크리스트

  1. 신규 프로젝트.NET 8 또는 .NET 10(LTS). 다른 선택지는 "그것을 써야만 하는 구체적 이유"가 있을 때만 검토합니다.
  2. Windows 전용 레거시 → 그대로 .NET Framework 4.8.1 유지. 무리한 마이그레이션은 비용만 키웁니다.
  3. 라이브러리 배포netstandard2.1(Unity 포함) + net8.0(최신 기능) 멀티 타겟팅.
  4. Unity 개발 → 언어 버전은 Unity 공식 문서의 지원 표를 기준으로. Reflection.Emit·Expression.Compile은 iOS IL2CPP에서 금지.
  5. TFM 접두사로 어디서 도는지 판단: net48 = Framework / net8.0 = 통합 .NET / netstandard2.* = 범용 라이브러리 / monoandroid* = Xamarin 유산.
  6. Unity의 GC는 Boehm(비세대별). 서버 .NET의 감각으로 할당을 남발하면 프레임 스파이크로 직결됩니다. StringBuilder 재사용, 구조체 선호, 컬렉션 풀링 같은 절약 습관이 필수입니다.
  7. 런타임 식별은 코드에서 RuntimeInformation.FrameworkDescription#if 전처리 심볼 두 가지로 합니다. 전자는 실행 시점, 후자는 컴파일 시점 분기입니다.
"Unity 모바일에서 성능 문제가 난다" → 가장 먼저 의심할 것은 서버 .NET 감각으로 할당을 남발한 코드입니다. 이름이 많은 이유는 역사적 필연이었고, 그 역사가 오늘 우리 코드의 성능 특성을 결정합니다.
반응형

+ Recent posts