.NET의 Native AOT란 무엇인가 - JIT, ReadyToRun, trimming과의 차이를 먼저 정리
· 小村 豪 · C#, .NET, Native AOT, 배포, 설계
전에 C#을 Native AOT로 네이티브 DLL로 만드는 방법 - UnmanagedCallersOnly로 C/C++에서 호출을 썼는데, 사실 순서가 조금 뒤바뀌었습니다.
원래는 그 앞에 「애초에 Native AOT란 무엇인가」를 두는 편이 자연스러웠습니다.
Native AOT 이야기는 처음에 이런 부분들이 섞이기 쉽습니다.
- JIT를 없애는 이야기인가
- self-contained나 single-file과 무엇이 다른가
- ReadyToRun과 같은 계통인가
- trimming warning이 대량으로 나오는 것은 무엇이 일어나고 있는 것인가
- WPF나 WinForms에서도 그대로 올릴 수 있는가
이런 것들이 섞인 채로 있으면 Native AOT가 「그냥 빨라지는 마법」으로 보이거나, 반대로 「제약 투성이라서 건드려서는 안 될 것」으로 보이기도 합니다.
어느 쪽도 조금 거칩니다.
Native AOT는 한마디로 .NET 앱을 publish 시점에 꽤 정적으로 굳히는 배포 모델입니다.
그 대신 기동이나 배포 사정에는 잘 듣습니다.
반대로 실행 시점에 이것저것 동적으로 해결하고 싶은 코드와는 궁합이 떨어집니다.
이 글에서는 그 부분을 실무 쪽으로 정리합니다.
1. Native AOT는 무엇을 하고 있는가
평소의 .NET은 먼저 IL을 만들고, 실행 시점에 필요한 곳을 JIT합니다.
Native AOT는 그 「실행 시점에 네이티브 코드를 만드는」 부분을 publish 시점에 상당히 앞당깁니다.
그래서 겉보기로는 「.NET을 네이티브 앱답게 배포한다」 방향입니다.
여기서 중요한 것은 publish 시점에 필요한 코드가 꽤 보였으면 좋겠다는 점입니다.
즉,
- 실행 시점에 타입을 찾는다
- 실행 시점에 코드를 만들어 낸다
- 실행 시점에 어셈블리를 읽어 판단한다
같은 작성 방식과는 아무래도 궁합이 나빠집니다.
Native AOT를 「빨라지는 기능」으로만 보면 여기서 어긋납니다.
본질은 오히려 동적인 세계를 조금 버리고, 정적으로 굳히기 쉬운 세계로 치우치는 것입니다.
2. ReadyToRun과 무엇이 다른가
여기를 먼저 나누어 두면 꽤 편해집니다.
| 관점 | 일반 JIT | ReadyToRun | Native AOT |
|---|---|---|---|
| 실행 시 JIT | 사용 | 아직 사용 | 사용하지 않음 |
| 배포물 | IL 중심 | IL + 사전 생성 코드 | 네이티브 실행 파일 중심 |
| 기동 | 기준 | 개선하기 쉬움 | 더 개선하기 쉬움 |
| 호환성 | 넓음 | 비교적 넓음 | 제약이 강함 |
ReadyToRun은 JIT의 일을 조금 앞당기는 방향입니다.
한편 Native AOT는 실행 시 JIT를 전제로 하지 않는 방향입니다.
비슷한 말이지만 실제 감촉은 꽤 다릅니다.
ReadyToRun은 「우선 기동을 조금 편하게 하고 싶다」.
Native AOT는 「기동, 배포, 실행 환경의 사정을 위해 설계도 조금 정적 쪽으로 만든다」입니다.
3. Native AOT로 무엇이 좋은가
가장 알기 쉬운 것은 역시 기동입니다.
CLI 도구나 단명 프로세스, 컨테이너의 작은 API, 상주하지만 기능이 좁혀진 도구에서는 JIT의 무거움이 잘 보입니다.
Native AOT는 그곳을 먼저 처리하므로 초동이 가벼워지기 쉽습니다.
또 하나는 배포 이야기입니다.
「배포 대상에 .NET Runtime을 먼저 설치해 주세요」라고 말하고 싶지 않은 장면에서는 상당히 마음이 편해집니다.
- 작은 도구를 1개만 배포하고 싶다
- 컨테이너 이미지를 가능한 한 가볍게 하고 싶다
- 실행 환경에 JIT를 두고 싶지 않다
이런 때 Native AOT는 쓸 곳이 분명합니다.
그래서 Native AOT가 꽂히는 장면은 「모든 앱」이 아니라 기동, 배포, 실행 환경의 전제를 줄이고 싶은 앱입니다.
4. 반대로 무엇이 힘들어지는가
여기는 꽤 분명합니다.
먼저 리플렉션이나 동적 코드 생성입니다.
Assembly.LoadFileSystem.Reflection.Emit- 실행 시점에 타입을 열거하여 찾는 구성
- 문자열 이름으로 타입을 조회해 실체화하는 구조
이쪽은 publish 시점에 필요 코드를 확정하기 어려우므로 AOT warning의 온상이 됩니다.
Native AOT에서 warning이 잔뜩 나올 때 「시끄럽네」가 아니라 「그 설계는 publish 시점에 보이기 어렵구나」로 읽는 편이 본질에 가깝습니다.
특히 RequiresDynamicCode 계열은 대체로 진지하게 보는 편이 좋습니다.
다음으로 trimming입니다.
Native AOT는 trimming과 상당히 깊게 연결됩니다.
여기서 까다로운 것은 자신의 코드뿐만 아니라 의존 라이브러리의 작성 방식까지 영향을 준다는 점입니다.
예를 들어,
- 리플렉션 기반의 시리얼라이저
- 실행 시 스캔을 전제로 하는 DI
- 동적 proxy를 쓰는 라이브러리
이쪽은 번거로워지기 쉽습니다.
즉 Native AOT를 할 때는 「실행 시점에 똑똑하게 어떻게든 한다」보다 빌드 시점에 알 수 있는 형태로 치우치는 편이 다루기 쉽습니다.
5. Windows 데스크톱에서는 조금 신중하게 보는 편이 좋다
Windows에서는 Native AOT에 built-in COM이 없습니다.
게다가 WPF는 trimming과 궁합이 좋지 않고, WinForms도 built-in COM marshalling에의 의존이 무겁습니다.
따라서,
- WPF / WinForms 본체를 바로 Native AOT화한다
- COM interop을 평소의 감각 그대로 가져온다
이쪽은 신중하게 보는 편이 좋습니다.
반대로 입구로 고르기 쉬운 것은,
- 콘솔
- worker
- 작은 Web API
- 경계가 분명한 작은 부품
입니다.
Native AOT는 「.NET이면 뭐든지 그대로 올라간다」는 느낌은 아닙니다.
특히 Windows 데스크톱과 COM이 진한 세계에서는 아직 JIT 상태가 더 자연스러운 경우도 많습니다.
6. 최소 절차는 심플
프로젝트 쪽에서는 우선 csproj에 PublishAot를 넣습니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
publish는 예를 들어 Windows x64라면 이렇습니다.
dotnet publish -c Release -r win-x64
Linux x64라면,
dotnet publish -c Release -r linux-x64
이 시점에서 이미 꽤 「네이티브 앱 같은 세계」입니다.
RID 고정이므로, 하나로 어디서든 돌아가는 DLL이라기보다 그 OS / 아키텍처용으로 만드는 실행물이 됩니다.
그리고 의외로 자주 걸리는 것이 JSON입니다.
System.Text.Json을 평소의 느낌으로 리플렉션 쪽으로 쓰기보다 source generation 쪽으로 치우치는 편이 온화합니다.
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
public sealed class AppConfig
{
public string? Name { get; init; }
public int RetryCount { get; init; }
}
var config = new AppConfig
{
Name = "sample",
RetryCount = 3
};
string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);
Native AOT 대응이라기보다 실행 시점에 타입을 찾게 하지 않는 작성 방식으로 치우친다고 기억해 두면 빗나가기 어렵습니다.
7. 어디부터 시도하는 것이 좋은가
처음 대상은 다음 중 어느 것이 들어오기 쉽습니다.
- 기동이 중요한 CLI
- worker
- 작은 Web API
- 네이티브 연계 중에서도 책임이 좁은 부품
이쪽이라면 동적인 구조를 줄이기 쉽고 Native AOT의 기쁨도 잘 보입니다.
반대로 WPF / WinForms의 기존 대규모 앱 본체, plugin 로드가 주역인 구성, COM 의존이 강한 구성은 처음 대상으로는 그다지 온화하지 않습니다.
8. 정리
Native AOT는 .NET을 publish 시점에 꽤 정적으로 굳히는 구조입니다.
- 기동에는 잘 듣는다
- 배포도 쉬워진다
- 그 대신 동적인 구조에는 엄격해진다
이 3가지를 처음에 잡아 두면 시야가 꽤 좋아집니다.
Native AOT는 「모든 .NET 앱에 붙이는 표준 스위치」가 아닙니다.
하지만 기동이 중요, 배포를 가볍게 하고 싶다, 실행 환경의 전제를 줄이고 싶다는 장면에서는 꽤 강한 선택지입니다.
반대로 Windows 데스크톱이나 COM이 진한 세계에서는 아직 평범한 .NET 쪽이 자연스러운 경우도 많습니다.
여기를 구별할 수 있게 되면 Native AOT는 「어려운 신기능」이 아니라 쓸 곳이 분명한 도구로 보이게 됩니다.
9. 참고 자료
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (日本語)
- Introduction to AOT warnings - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- 관련 기사: C#을 Native AOT로 네이티브 DLL로 만드는 방법 - UnmanagedCallersOnly로 C/C++에서 호출
- 관련 기사: C#에서 네이티브 DLL을 쓴다면 C++/CLI 래퍼가 유력한 이유 - P/Invoke와 비교해 정리
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
.NET의 Generic Host란 무엇인가 - DI, 설정, 로그, BackgroundService를 먼저 정리
Generic Host의 정체를, DI・설정・로그・BackgroundService와 Host.CreateApplicationBuilder / WebApplicationBuilder의 관계로 정리합니다. 어디서 효과적이고 어디서 과잉인지를 .NET...
PeriodicTimer / System.Threading.Timer / DispatcherTimer의 사용 구분 - .NET의 정기 실행을 먼저 정리
PeriodicTimer는 async 루프, System.Threading.Timer는 ThreadPool callback, DispatcherTimer는 WPF UI 스레드라는 책무 차이를 정리하고, .NET 6 이후의 worker・WPF에서 ...
C#을 Native AOT로 네이티브 DLL로 만드는 방법 - UnmanagedCallersOnly로 C/C++에서 호출하기
.NET의 Native AOT와 UnmanagedCallersOnly로 C# 클래스 라이브러리를 네이티브 DLL로 발행해 C/C++에서 in-process로 호출하는 구성을, 핸들 기반 수명 관리와 에러 코드, C ABI 설계 요령으로 정리합니다.
Generic Host / BackgroundService를 데스크톱 앱에 가지고 들어오는 이유 - 기동・수명・graceful shutdown의 정리가 꽤 편해진다
WPF나 WinForms 같은 데스크톱 앱에서 Generic Host와 BackgroundService를 도입해 기동, 상주 처리, graceful shutdown, DI, 로그, 설정의 입구를 한 곳으로 모으는 설계 정리법과 안티패턴을 실무 시...
FileSystemWatcher 사용법과 주의점 - 누락, 중복 알림, 완료 판정의 함정
Windows .NET 파일 감시에서 FileSystemWatcher의 이벤트를 완료 알림으로 오인하기 쉬운 함정과, 재스캔 요청·원자적 claim·idempotency를 축으로 누락과 중복을 견디는 안전한 설계 패턴을 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
32비트 / 64비트 상호 운용
32비트 / 64비트 상호 운용, 네이티브 경계, 관련된 Windows 설계 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크