C# 애플리케이션 시작 속도를 크게 끌어올리고, 배포 크기를 줄이고, JIT 의존성을 제거하고 싶다면 NativeAOT가 적합합니다. .NET 7부터 정식 포함되었고 .NET 8에서 더 성숙해졌습니다. 이 글은 콘솔 앱을 기준으로 NativeAOT로 빠르게 빌드/배포하는 실전 가이입니다.
1. NativeAOT 한눈에 보기
- NativeAOT는 IL을 사전에 네이티브 코드로 컴파일해 단일 실행 파일을 생성합니다. 실행 시 JIT이 필요 없어 시작 시간이 매우 빠릅니다.
- ReadyToRun(R2R)와 비교: R2R은 IL+네이티브 혼합으로 여전히 JIT이 개입할 수 있습니다. NativeAOT는 순수 네이티브 바이너리입니다.
- 장점: 빠른 시작, 배포 단순화(단일 파일), 낮은 메모리 풋프린트, 컨테이너/서버리스에 적합합니다.
- 제약: 동적 코드 생성(Reflection.Emit), 런타임 어셈블리 로드, 일부 리플렉션 패턴은 제한됩니다. 호환성 경고(IL2026 등)를 해소해야 합니다.
2. 가장 빠른 시작
최소 콘솔 앱으로 NativeAOT를 확인합니다.
// Program.cs
using System;
Console.WriteLine("Hello, NativeAOT!");
기본적으로는 CLI에서 게시 시점에 NativeAOT를 켜는 것이 가장 간단합니다.
// Windows x64
// 프로젝트 폴더에서 실행
// Release 모드로 NativeAOT 게시
// 결과: bin/Release/net8.0/win-x64/publish 폴더의 단일 exe
dotnet publish -r win-x64 -c Release -p:PublishAot=true -p:IlcOptimizationPreference=Size -p:InvariantGlobalization=true
// Linux x64
dotnet publish -r linux-x64 -c Release -p:PublishAot=true
// macOS Apple Silicon
dotnet publish -r osx-arm64 -c Release -p:PublishAot=true
결과물은 플랫폼별로 빌드되므로 대상 OS에 맞는 Runtime Identifier(RID)를 지정해야 합니다.
3. 프로젝트 파일 설정(선택)
자주 사용하는 옵션을 .csproj에 고정하면 편합니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot> <!-- 기본 NativeAOT 게시 -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <!-- 런타임 포함 -->
<PublishSingleFile>true</PublishSingleFile> <!-- 단일 파일 -->
<InvariantGlobalization>true</InvariantGlobalization> <!-- 크기 절감 -->
<IlcOptimizationPreference>Size</IlcOptimizationPreference> <!-- Size 또는 Speed -->
</PropertyGroup>
</Project>
참고: JIT 기반의 빠른 시작만 원하면 NativeAOT 대신 R2R을 고려할 수 있습니다.
// R2R 대안 (JIT 존재)
dotnet publish -r win-x64 -c Release -p:PublishReadyToRun=true
4. 크기/성능 스위치
- IlcOptimizationPreference: Size(크기 최소화) 또는 Speed(최대 성능)로 최적화 방향을 지정합니다.
- InvariantGlobalization: true로 설정하면 글로벌라이제이션 데이터 포함을 줄여 크기를 줄입니다(다국어 정렬/서식 필요 시 false 권장).
- PublishSingleFile/SelfContained: 단일 파일 배포 및 런타임 포함을 제어합니다. AOT 게시 시 기본적으로 단일 네이티브 바이너리를 생성하지만 명시적으로 설정해 일관성을 유지합니다.
5. 리플렉션/동적 코드 다루기
트리밍과 AOT는 사용되지 않는 코드 제거를 aggressive하게 수행합니다. 리플렉션으로 생성자/메서드를 찾는 경우 보존을 명시해야 합니다. 다음은 생성자를 보존하는 예시입니다.
using System;
using System.Diagnostics.CodeAnalysis;
class MyService
{
public MyService() { }
}
class Program
{
static void Main()
{
var obj = CreateService(typeof(MyService));
Console.WriteLine(obj.GetType().Name);
}
static object CreateService([
DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)
] Type t)
{
return Activator.CreateInstance(t)!;
}
}
- 자주 쓰는 동적 시나리오: JSON 직렬화는 Source Generator(System.Text.Json 소스 생성)를 사용하면 AOT 친화적으로 해결됩니다.
- 제한되는 기능: Reflection.Emit, 런타임 코드 생성 프록시, 런타임 어셈블리 컴파일/로드는 동작하지 않거나 제한됩니다.
6. 호환성 점검과 문제 해결
- 경고 해결: dotnet publish 시 IL2026/IL3050 등 트리밍 경고를 꼼꼼히 확인하고, 필요한 멤버 보존 특성(DynamicallyAccessedMembers)이나 소스 생성 기반 API로 전환합니다.
- 네이티브 종속성: P/Invoke를 사용한다면 대상 플랫폼의 네이티브 라이브러리가 배포 환경에서 로드 가능한지 확인하세요.
- 플랫폼 빌드: 각 OS/RID마다 별도로 게시해야 합니다. 예) win-x64, linux-x64, linux-musl-x64(Alpine), osx-arm64 등.
- 디버깅: AOT 바이너리는 심볼 스트리핑 시 스택 추적이 제한될 수 있습니다. 개발 중에는 StripSymbols를 끄고, Release 배포에만 사용하세요.
7. 언제 NativeAOT를 쓰면 좋은가
- 서버리스/Function, CLI 도구, 데스크톱 유틸리티처럼 시작 시간과 배포 단순성이 중요한 경우 최적입니다.
- 리플렉션/동적 생성이 많은 대형 프레임워크 앱은 마이그레이션 비용이 있을 수 있습니다. 이 경우 R2R 또는 부분적인 소스 생성 도입으로 점진적으로 전환하세요.
NativeAOT는 .NET 앱의 시작 속도와 배포 단순화를 실질적으로 개선합니다. 위 설정을 바탕으로 작은 콘솔 도구부터 단계적으로 적용해 경고를 해소하고, 대상 플랫폼별 성능과 크기를 비교해 보시기 바랍니다.
'C#' 카테고리의 다른 글
| C# 파일 스코프 네임스페이스(File-Scoped Namespace) 활용 (0) | 2026.07.30 |
|---|---|
| C# Marshal 클래스 활용한 메모리 변환 기법 (1) | 2026.07.29 |
| C# GCHandle로 관리 객체 고정(Pinning)하기 (0) | 2026.07.29 |
| C# Semaphore와 Mutex 차이 및 선택 기준 (0) | 2026.07.28 |
| C# PeriodicTimer로 주기적 작업 구현하기 (0) | 2026.07.28 |