.NET Native AOT란 무엇인가 - JIT·trimming과의 차이

· 업데이트: · · C#, .NET, Native AOT, 게시, 설계

수정 이력(3건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
한국어 전면 재작성에 맞춰 본문 표현을 바로잡았습니다. 기술적인 주장은 일본어판과 같습니다.
일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635127)
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635126)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

Go Komura (2026). 「.NET Native AOT란 무엇인가 - JIT·trimming과의 차이」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635126 https://comcomponent.com/ko/blog/dotnet-native-aot-what-is/

DOI(최신 버전)
10.5281/zenodo.21635126
DOI(이 버전)
10.5281/zenodo.22217431

이미 C# Native AOT DLL을 C/C++에서 호출하는 방법에서는 Native AOT로 C/C++에서 C#을 호출하는 이야기를 썼습니다. 다만 그 앞에 애초에 Native AOT란 무엇인가를 두는 편이 친절했습니다. 순서가 조금 뒤바뀌었습니다.

Native AOT 이야기는 처음에 용어가 섞이기 쉽습니다.

  • JIT를 없애는 이야기인가
  • self-contained나 single-file과 무엇이 다른가
  • ReadyToRun과 같은 계통인가
  • trimming warning이 대량으로 나오는 것은 무엇이 일어나고 있는가
  • WPF / WinForms / ASP.NET Core에서도 같은 무게로 쓸 수 있는가

이 부분이 한데 섞이면 Native AOT가 「그냥 빨라지는 마법」으로 보이거나, 반대로 「제약 투성이라 무서운 것」으로 보이기도 합니다. 어느 쪽도 조금 거칠습니다.

용어가 섞이면 생기는 오해JIT나 self-contained, ReadyToRun, trimming 같은 용어가 한데 섞이면 Native AOT가 그냥 빨라지는 마법으로 보이거나 제약 투성이라 무섭게 보이지만, 어느 쪽도 거친 시각이다.용어가 한데 섞인다빨라지는 마법으로 보인다제약 투성이라 무섭게 보인다말을 나눠 정리한다쓸 곳이 보이는 선택지가 된다

그림 1: 오해의 원인은 용어가 섞이는 것이다. 먼저 말을 나누는 것부터 시작한다.

이 글에서는 주로 .NET 8 이후의 현재 실무 감각을 전제로, 다음 네 가지를 먼저 정리합니다.

  • Native AOT의 정체
  • 무엇이 반갑고, 어디가 까다로워지는가
  • ReadyToRun이나 trimming과 어떻게 다른가
  • 어떤 앱부터 시도하면 무난한가

목차

  1. 먼저 결론 (한 줄로)
  2. 먼저 보는 정리 표
    • 2.1. Native AOT 주변의 말
    • 2.2. JIT / ReadyToRun / Native AOT의 차이
  3. Native AOT의 전체 모습 (그림)
  4. Native AOT에서 무엇이 반가운가
    • 4.1. 시작이 가벼워지기 쉽다
    • 4.2. 런타임 사전 설치를 전제로 하지 않아도 된다
    • 4.3. 제약이 있는 실행 환경에 맞다
  5. Native AOT에서 무엇이 까다로워지는가
    • 5.1. 리플렉션과 동적 코드 생성
    • 5.2. trimming을 전제로 생각할 필요가 있다
    • 5.3. 플랫폼마다 게시한다
    • 5.4. Windows 데스크톱 / COM 맥락은 상당히 신중하게
  6. 최소 절차
    • 6.1. csproj
    • 6.2. publish
    • 6.3. JSON 작성법
  7. 잘 맞는 경우
  8. 맞지 않는 경우
  9. 빠지기 쉬운 지점
  10. 정리
  11. 참고 자료

이 글의 지식 맵

.NET Native AOT는 실행 시 JIT를 쓰지 않고 publish 시점에 네이티브 코드로 사전 컴파일하는 발행 방식으로, 기동 시간과 메모리 점유를 개선하기 쉽고, .NET 런타임이 설치되지 않은 환경에도 배포하기 쉽습니다. ReadyToRun이 IL을 남긴 채 JIT의 일부를 앞당기는 방향인 데 비해, Native AOT는 실행 시 JIT 자체를 전제로 하지 않는 만큼 trimming이 거의 전제가 되어, 자유로운 리플렉션이나 동적 코드 생성, built-in COM과는 궁합이 나빠집니다. Windows의 Native AOT에는 built-in COM이 없으며, WPF는 trimming과의 궁합이 나쁘다는 이유로, WinForms는 built-in COM marshalling에 대한 의존이 무겁다는 이유로, 둘 다 .NET SDK 쪽에서 trimming 지원이 비활성화되어 있어, 첫 Native AOT 후보로는 신중하게 다뤄야 한다고 여겨집니다. COM이 필요한 경우에는 ComWrappers를 전제로 다시 설계하는 것이 유력한 선택지가 됩니다.

.NET Native AOTNative AOT가 JIT나 ReadyToRun과 어떻게 다르고, trimming이나 리플렉션·built-in COM과 어떻게 충돌하며, WPF와 WinForms가 왜 신중하게 다뤄지는지를 보여주는 그림양립하지 않는다전제로 한다전제로 한다양립하지 않는다양립하지 않는다양립하지 않는다권장되는 대응양립하지 않는다전제로 한다사용은 비권장사용은 비권장이용한다완화한다전제로 한다이용한다전제로 한다Native AOTJIT(Just-In-Time) 컴파일ReadyToRuntrimming(트리밍)리플렉션동적 코드 생성(Reflection.Emit 등)built-in COM interopComWrappers / source-generated COMWPFWindows Formssource generatorSystem.Text.Json의 source generation자체 포함 배포single-file 게시.NET(Core 이후)

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 16건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

1. 먼저 결론 (한 줄로)

  • Native AOT는 .NET 앱을 publish 시점에 네이티브 코드로 미리 컴파일해 배포하는 방식입니다.
  • 실행 시 JIT를 쓰지 않으므로 시작 시간과 메모리 점유가 나아지기 쉽고, .NET 런타임이 설치되지 않은 환경에도 배포하기 쉽습니다.
  • 다만 자유로운 리플렉션, 동적 코드 생성, built-in COM, trimming 미대응 라이브러리와는 궁합이 나빠집니다.
  • 즉 빨라지는 마법이라기보다, 시작·배포·실행 환경 사정 때문에 동적인 세계를 조금 버리고 정적인 세계로 치우치는 배포 모델입니다.

Native AOT는 「.NET을 네이티브답게 배포하기 위한 장치」이지, 단순한 컴파일 가속 체크박스가 아닙니다.

무엇을 얻고 무엇을 버리는가Native AOT는 publish 시점의 사전 컴파일로 시작 시간·메모리 점유·런타임 불필요 배포를 얻는 대신, 자유로운 리플렉션·동적 코드 생성·built-in COM과의 궁합을 버리는 배포 모델이다.Native AOT얻는 것버리는 것시작과 메모리와 배포의 가벼움자유로운 리플렉션동적 코드 생성과 built-in COM

그림 2: 빨라지는 마법이 아니라, 동적인 세계를 조금 버리고 정적인 세계로 치우치는 거래다.

2. 먼저 보는 정리 표

2.1. Native AOT 주변의 말

처음에 이 부분을 나눠 두면 뒤가 편합니다.

용어 무엇을 하는가 Native AOT와의 관계
JIT 실행 시에 IL에서 네이티브 코드를 만든다 Native AOT는 여기를 미리 끝낸다
self-contained 실행에 필요한 .NET 구성 전부도 함께 배포한다 Native AOT는 이 계통으로 생각한다
single-file 배포물을 1개 파일로 모은다 Native AOT의 본질과는 다르지만, 결과의 겉모습은 비슷해지기 쉽다
trimming 쓰지 않는 코드를 깎는다 Native AOT에서는 거의 전제가 된다
ReadyToRun IL을 남긴 채로 JIT의 일을 조금 앞당긴다 Native AOT와는 비슷해 보이지만 다른 것이다
source generator 실행 시의 동적 처리를 빌드 시 코드 생성으로 치우친다 Native AOT와 궁합이 좋다

섞이기 쉬운 점은, Native AOT가 기능 하나라기보다 self-contained, trimming, source generation, RID 고정 publish 등과 함께 움직이는 배포 모델이라는 것입니다.

Native AOT와 함께 움직이는 장치Native AOT는 기능 하나가 아니라 self-contained, trimming, source generation, RID 고정 publish와 맞물려 움직이는 배포 모델임을 나타낸다.Native AOT〔배포 모델〕self-contained 계통trimming은 거의 전제source generator와 궁합이 좋음RID 고정으로 publish

그림 3: Native AOT는 단독 기능이 아니라, 주변 장치와 세트로 움직이는 배포 모델이다.

2.2. JIT / ReadyToRun / Native AOT의 차이

여기도 처음에 한 장으로 보는 편이 빠릅니다.

관점 평범한 JIT 실행 ReadyToRun Native AOT
실행 시 JIT 사용한다 아직 쓰는 장면이 있다 쓰지 않는다
배포물의 내용 IL 중심 IL + 사전 생성 코드 네이티브 실행 파일 중심
시작 기준 나아지기 쉽다 상당히 나아지기 쉽다
호환성 가장 넓다 넓다 제약이 강하다
동적 기능 쓰기 쉽다 대체로 쓰기 쉽다 제한이 많다
맞는 목적 평범한 .NET 개발 전반 우선 시작을 개선하고 싶다 시작·배포·제한 환경을 강하게 가져간다

ReadyToRun이 「JIT를 조금 덜어 주는」 방향이라면, Native AOT는 「실행 시 JIT 자체를 전제로 하지 않는」 방향입니다. 같은 AOT라는 말이 붙어 있어도 결이는 꽤 다릅니다.

ReadyToRun과 Native AOT의 방향 차이ReadyToRun은 IL을 남긴 채로 JIT의 일을 조금 앞당겨 덜어 주는 방향이고, Native AOT는 실행 시 JIT 자체를 전제로 하지 않는 방향이며, 같은 AOT라는 말이라도 결이가 꽤 다르다.조금 덜어 준다전제로 하지 않는다JIT를 어떻게 하고 싶은가ReadyToRun〔IL은 남는다〕Native AOT〔네이티브 중심〕호환성은 넓은 채로시작은 빠르지만 제약은 강하다

그림 4: 같은 AOT라도 「JIT를 덜어 준다」와 「JIT를 전제로 하지 않는다」는 다른 것이다.

그다음 「결국 어느 배포 형태를 고를 것인가」를 한 장으로 그리면 이렇게 됩니다.

넣을 수 있다그 정도는 아니다줄이고 싶다넣고 싶지 않다 / 넣을 수 없다의존한다의존하지 않는다아니요배포 형태를 정하고 싶다배포 대상에 .NET 런타임을 넣을 수 있는가?시작 시간을 더 줄이고 싶은가?framework-dependent평범한 게시ReadyToRun호환성을 유지한 채로 시작 개선reflection·동적 코드 생성·built-in COM에 의존하는가?self-contained필요하면 single-file로 모은다대상은 console / worker / 작은 API인가?Native AOT우선 self-contained동적인 의존을 줄인 뒤 다시 검토

그림 5: 배포 형태를 고르는 방법. 런타임을 둘 수 있는지, 동적인 구조를 줄일 수 있는지의 순으로 갈린다.

첫 분기는 런타임을 배포 대상에 둘 수 있는지, 다음 분기는 동적인 구조를 어디까지 줄일 수 있는지입니다. self-contained와 single-file은 「배포하는 방법」의 이야기이고, ReadyToRun과 Native AOT는 「언제 네이티브 코드를 만드는가」의 이야기이므로, 실제로는 조합해서 생각하게 됩니다.

3. Native AOT의 전체 모습 (그림)

Native AOT를 대략 그림으로 그리면 이렇습니다.

일반 실행dotnet publish + PublishAotC# / .NET 소스 코드IL 어셈블리실행 시 JIT앱 실행AOT / trim 분석불필요 코드 축소네이티브 코드 생성RID 고유 실행 파일

그림 6: 일반 실행은 실행 시에 JIT하지만, Native AOT는 분석·축소·네이티브 생성을 publish 시점으로 앞당긴다.

평소의 .NET은 먼저 IL을 만들고, 실행 시에 필요한 만큼만 JIT합니다. Native AOT는 그 뒷단의 큰 부분을 publish 시점으로 앞당깁니다.

이때 중요한 것은, publish 시점에 「실행 시에 필요해질 코드를 거의 전부 알고 있어야 한다」는 점입니다. 여기서 허용되는 작성법의 전제가 바뀝니다.

  • 실행 시에 타입을 찾는다
  • 실행 시에 코드를 만들어 낸다
  • 실행 시에 Assembly를 읽는다
  • 실행 시에 「어떻게든 되겠지」로 지연 해결한다

이런 작성법은 Native AOT와 갑자기 궁합이 나빠집니다.

publish 시점에 전부 알고 있어야 한다Native AOT는 publish 시점에 실행 시에 필요해질 코드를 거의 전부 알고 있어야 하며, 실행 시에 타입을 찾거나 코드를 만들어 내거나 Assembly를 읽거나 지연 해결하는 작성법과 궁합이 나빠진다.publish 시점에 필요 코드를 확정실행 시에 타입을 찾는다실행 시에 코드를 만들어 낸다실행 시에 Assembly를 읽는다지연 해결로 버틴다어느 쪽이든 AOT와 궁합이 나쁘다

그림 7: 전제가 바뀌는 핵심이다. 「실행 시에 정한다」는 작성법일수록 Native AOT와 충돌한다.

4. Native AOT에서 무엇이 반가운가

4.1. 시작이 가벼워지기 쉽다

Native AOT의 가장 알기 쉬운 효과는 역시 시작입니다.

  • CLI 도구
  • 단명 프로세스
  • 서버리스에 가까운 시작
  • 컨테이너의 시작·교체
  • 감시 도구나 작은 상주 프로세스

이 장면에서는 JIT 비용이 보이기 쉽고, Native AOT로 그 부분을 앞당길 수 있는 만큼 초반이 가벼워집니다.

메모리 점유도 나아지기 쉬우므로, 같은 대수를 더 넣고 싶은 장면에서도 유리합니다. 특히 같은 프로세스가 대량으로 뜨는 클라우드 쪽에서는 이 차이가 서서히 먹힙니다.

4.2. 런타임 사전 설치를 전제로 하지 않아도 된다

Native AOT로 publish한 앱은 .NET 런타임이 설치되지 않은 환경에서도 돌리기 쉬워집니다.

이건 은근히 큽니다.

  • 배포 대상에 「.NET 9 Runtime을 먼저 넣어 주세요」라고 말하고 싶지 않다
  • 컨테이너 이미지를 가늘게 하고 싶다
  • 작은 도구를 한 개만 두고 돌리고 싶다
  • 실행 환경에서 JIT를 허용하고 싶지 않다 / 허용되지 않는다

이런 장면에서는 「런타임을 따로 맞추는 전제」가 없다는 것만으로 이야기가 꽤 조용해집니다.

여기서 말하는 「런타임 불필요」는 배포 대상에 따로 .NET을 넣지 않아도 된다는 뜻입니다. 앱 안에서 필요한 runtime 상당 부분까지 완전히 사라진다는 이야기가 아닙니다.

런타임 불필요의 올바른 의미Native AOT의 런타임 불필요는 배포 대상에 따로 .NET을 넣지 않아도 된다는 뜻이며, 앱 안에서 필요한 runtime 상당 부분까지 완전히 사라진다는 뜻이 아니다.런타임 불필요라는 말배포 대상에 .NET을 넣지 않아도 된다runtime 상당이 사라진다는 뜻이 아니다배포와 시작의 전제가 줄어든다

그림 8: 「런타임 불필요」는 배포 대상 이야기다. runtime 상당은 앱 안에 들어 있다.

4.3. 제약이 있는 실행 환경에 맞다

Native AOT는 실행 시 JIT를 쓰지 않으므로, JIT가 허용되지 않는 환경에서도 돌리기 쉽습니다.

여기는 desktop보다 클라우드, 컨테이너, 모바일 쪽 이야기에서 먹히기 쉽습니다. 다만 Windows 개발 맥락에서도 「배포 대상에서 불필요한 전제를 줄이고 싶다」는 뜻에서는 충분히 반가운 점입니다.

5. Native AOT에서 무엇이 까다로워지는가

5.1. 리플렉션과 동적 코드 생성

Native AOT의 핵심 제약은 여기입니다.

  • Assembly.LoadFile 같은 동적 로드
  • System.Reflection.Emit 같은 실행 시 코드 생성
  • 실행 시에 제한 없이 타입을 따라가는 리플렉션
  • 실행 시에 generic을 마음대로 조립하는 작성법

이 부분은 publish 시점에 필요 코드를 확정하기 어려워서 AOT warning의 온상이 됩니다.

물론 리플렉션이 한 줄이라도 있으면 바로 탈락일 만큼 단순하지는 않습니다. 다만 「실행 시에 보고 정한다」 쪽 설계일수록 까다로워진다는 경향은 빗나가지 않습니다.

warning 이름으로 자주 나오는 것은 RequiresDynamicCode 계열입니다. 「그 호출은 AOT에서 깨질 수 있다」는 뜻이므로, 대충 suppress하지 않는 편이 안전합니다.

Native AOT를 할 때는 「실행 시의 똑똑함」을 줄이고 「빌드 시의 명시」를 늘리는 쪽으로 치우친다고 보면 이해하기 쉽습니다.

실행 시의 똑똑함에서 빌드 시의 명시로동적 로드나 실행 시 코드 생성, 제한 없는 리플렉션은 AOT warning의 온상이 되므로, 실행 시의 똑똑함을 줄이고 빌드 시의 명시를 늘리는 쪽으로 설계를 치우친다.실행 시의 똑똑함에 기대는 설계RequiresDynamicCode 계열 warning빌드 시의 명시를 늘린다publish 시점에 필요 코드가 확정된다대충 suppress하지 않는다

그림 9: 핵심 제약을 다루는 방향은 한 가지다. 「실행 시에 보고 정한다」를 「빌드 시에 명시한다」로.

5.2. trimming을 전제로 생각할 필요가 있다

Native AOT는 trimming과 깊게 묶여 있습니다. 여기서 놓치기 쉬운 것은 자기 코드뿐 아니라 의존 라이브러리 쪽 작성법도 먹힌다는 점입니다.

주의할 것은 이 부근입니다.

  • reflection 기반 직렬화기
  • 실행 시 스캔으로 타입을 모으는 DI / 플러그인 구성
  • 문자열 이름으로 타입을 찾아 실체화하는 장치
  • 동적 proxy나 IL 생성에 기대는 라이브러리

여기서 warning이 나오는데 「publish는 통과했으니 됐다」고 하면 나중에 꽤 씁쓸합니다. Native AOT에서는 warning을 대체로 진지하게 읽는 편이 좋습니다.

자기 코드만으로는 정해지지 않는 trimmingtrimming에서는 자기 코드뿐 아니라 의존 라이브러리 쪽 작성법도 먹히며, reflection 기반 직렬화기나 실행 시 스캔 DI, 문자열 이름에서 타입을 찾는 방식, 동적 proxy에 기대는 라이브러리가 주의 대상이 된다.trimming의 성패자기 코드의 작성법의존 라이브러리의 작성법reflection 계열이나 실행 시 스캔 계열warning을 진지하게 읽고 확인한다

그림 10: 놓치기 쉬운 것은 의존 라이브러리 쪽이다. 「publish가 통과했으니 됐다」는 나중에 씁쓸해진다.

그렇다고 「진지하게 읽어라」만으로는 다음 행동이 나오지 않으므로, 대처 우선순위를 적어 둡니다. 공식 문서도 이 순서로 시도하라고 권합니다.

순서 대처 쓰는 장면
1 reflection을 그만둔다 Activator.CreateInstance(Type)를 generic 인자로 바꿀 수 있다, source generator로 치우칠 수 있다
2 DynamicallyAccessedMembers를 붙인다 reflection은 필요하지만 대상 타입은 컴파일 시점에 알고 있다
3 RequiresUnreferencedCode를 붙인다 타입 이름을 실행 시 문자열로 정하는 등, 본질적으로 정적 분석이 안 된다
4 UnconditionalSuppressMessage로 억제한다 위가 전부 안 되고, 안전하다고 확인했을 때의 마지막 수단

첫 걸음으로 가장 많은 것은 2번, 「타입은 알고 있는데 경고만 나는」 패턴입니다. 예를 들어 다음 코드는 IL2070이 납니다.

// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

GetMethods()를 부를 거라면 public 메서드를 남겨 달라고 특성으로 선언하면 사라집니다.

using System.Diagnostics.CodeAnalysis;

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// 호출 쪽이 typeof로 넘기고 있으면 요구는 자동으로 충족됩니다
PrintMethodNames(typeof(DateTime));

호출하는 API와 필요한 지정은 대체로 대응합니다. GetMethod / GetMethodsPublicMethods, GetProperty / GetPropertiesPublicProperties, Activator.CreateInstancePublicParameterlessConstructor 또는 PublicConstructors입니다. DynamicallyAccessedMemberTypes.All은 편하지만 대상 타입의 멤버를 통째로 남겨 크기가 부풀어 오르고, 남긴 멤버가 다른 warning을 끌고 오므로 필요 최소한을 지정하는 것이 원칙입니다.

참고로 특성을 붙여도 warning이 안 사라질 때는 reflection을 쓰는 곳에서 호출 쪽으로 거슬러 올라가, 경로 전부에 붙어 있는지를 확인합니다. 중간에 한 곳이 빠지면 거기서 요구가 끊깁니다.

특성을 붙여도 warning이 안 사라질 때DynamicallyAccessedMembers는 필요 최소한을 지정하고, warning이 안 사라질 때는 reflection을 쓰는 곳에서 호출 쪽으로 거슬러 올라가 경로 전부에 특성이 붙어 있는지 확인한다. 한 곳이 빠지면 거기서 요구가 끊긴다.특성을 붙였는데 warning이 남는다호출 쪽으로 거슬러 올라간다경로 전부에 붙어 있는지 확인한다한 곳이 빠지면 요구가 끊긴다지정은 필요 최소한이 원칙

그림 11: 특성은 점이 아니라 경로에 붙인다. 중간에 한 곳이 빠지면 요구는 거기서 끊긴다.

5.3. 플랫폼마다 게시한다

Native AOT는 RID(Runtime Identifier) 고정으로 publish합니다. 즉 win-x64용으로 만든 것을 그대로 linux-x64에서 돌리는 세계가 아닙니다.

  • Windows x64
  • Windows Arm64
  • Linux x64
  • Linux Arm64
  • macOS Arm64

처럼 타깃마다 배포물을 만드는 전제입니다.

여기는 평범한 framework-dependent .NET보다 한결 「네이티브 앱 같은」 감각이 됩니다.

5.4. Windows 데스크톱 / COM 맥락은 상당히 신중하게

KomuraSoft 맥락에서는 여기가 특히 중요합니다.

Windows에서는 Native AOT에 built-in COM이 없습니다. 게다가 WPF는 trimming과 궁합이 나쁘고, WinForms는 built-in COM marshalling에 대한 의존이 무겁기 때문에, 적어도 지금은 둘 다 「첫 Native AOT 후보」로는 상당히 신중하게 보는 편이 좋습니다.

여기서 말하는 「지금」의 기준을 적어 둡니다. 이 글의 서술은 .NET 8 / 9 / 10 세대의 공식 문서(2026년 7월 시점)에 근거합니다. WPF와 WinForms에 대해서는 Microsoft의 「Known trimming incompatibilities」에서, WPF는 reflection과 실행 시 코드 검사에 대한 의존이 강해 trimming 뒤에 거의 동작하지 않으므로, WinForms는 built-in COM marshalling에 대한 의존이 무거우므로, 둘 다 .NET SDK 쪽에서 trimming 지원이 비활성화되어 있다고 명시되어 있습니다. 즉 「신중하게 보는 편이 좋다」가 아니라, 현황은 SDK가 막고 있는 상태입니다. 나중에 이 서술이 바뀔 가능성은 있으므로, 읽는 시점에 같은 페이지를 확인하시기 바랍니다.

WPF와 WinForms가 막혀 있는 이유Windows의 Native AOT에는 built-in COM이 없고, WPF는 reflection과 실행 시 코드 검사 의존 때문에 trimming 뒤에 거의 동작하지 않으며, WinForms는 built-in COM marshalling 의존이 무거워서, 둘 다 .NET SDK 쪽에서 trimming 지원이 비활성화되어 있다.Windows의 Native AOTbuilt-in COM이 없다WPFreflection 의존이 강하다WinFormsCOM marshalling 의존이 무겁다SDK 쪽에서 trimming이 비활성화

그림 12: 「신중하게」가 아니라, 현황은 SDK가 막고 있다. 데스크톱 본체는 첫 후보로 두지 않는다.

요컨대,

  • WPF / WinForms 본체를 갑자기 Native AOT화한다
  • COM interop를 평범한 감각으로 그대로 가져온다

이 부근은 publish 시점의 warning과 실행 시 제약이 한꺼번에 늘어서, 대응 비용이 튀기 쉽습니다.

반대로,

  • 콘솔
  • worker
  • 작은 Web API
  • 네이티브 연동 중에서도 C 함수 경계에 치우치기 쉬운 부품

이 입구로서 수월합니다.

COM이 필요하면 JIT 상태로 두거나, ComWrappers / source-generated COM을 전제로 설계를 다시 하는 편이 맞는 장면도 있습니다.

6. 최소 절차

그 전에 Native AOT의 publish에는 네이티브 툴체인이 필요합니다. PublishAot만 붙인 채로 dotnet publish를 치면, 컴파일이 아니라 마지막 네이티브 링크에서 실패합니다. 여기가 첫 관문이므로 먼저 넣어 두시기 바랍니다.

환경 필요한 것
Windows Visual Studio 2022 이후. 「C++를 사용한 데스크톱 개발」 워크로드를 기본 구성 요소를 전부 넣은 상태로 설치한다
Ubuntu 18.04 이후 sudo apt-get install clang zlib1g-dev
Alpine 3.15 이후 sudo apk add clang build-base zlib-dev
Fedora 39 이후 / RHEL 8 이후 sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel
macOS Xcode의 Command Line Tools(.NET 8 이후에서 대응)

요컨대 컴파일러 툴체인과, .NET 런타임이 의존하는 라이브러리의 개발용 패키지입니다. CI에서 돌린다면 dotnet/samples의 Native AOT 샘플에 Linux / Windows 양쪽 Dockerfile이 있으므로, 거기서 전제 설치 절차를 가져오는 편이 빠릅니다.

참고로 Linux에서 빌드한 바이너리는 같거나 그보다 새로운 Linux에서만 동작합니다. Ubuntu 20.04에서 만든 것은 20.04 이후에서는 동작하지만 18.04에서는 동작하지 않습니다. 빌드 환경을 고르는 방식이 곧 배포 가능 범위가 됩니다.

publish 전의 관문과 배포 가능 범위Native AOT의 publish에는 네이티브 툴체인이 필요하며, 없으면 마지막 네이티브 링크에서 실패한다. 또한 Linux에서 빌드한 바이너리는 같거나 그보다 새로운 Linux에서만 동작하며, 빌드 환경을 고르는 방식이 배포 가능 범위가 된다.없다있다툴체인이 있는가네이티브 링크에서 실패한다RID마다 바이너리가 나온다Linux는 같거나 새로운 환경에서만 동작한다빌드 환경을 고르는 방식이 배포 범위

그림 13: 첫 관문은 툴체인이다. Linux에서는 빌드 환경의 오래됨이 배포 가능 범위를 정한다.

6.1. csproj

먼저 프로젝트 파일에 PublishAot를 넣습니다.

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

샘플은 net8.0이면 충분합니다. 생각 자체는 .NET 9 / 10에서도 거의 같습니다.

중요한 것은 dotnet publish 명령줄에만 잠깐 붙이는 것보다, 평소부터 프로젝트에 두고 build / publish 시점의 분석을 일상적으로 보는 것입니다.

참고로 <PublishAot>true</PublishAot>를 넣어도 평소의 로컬 실행까지 갑자기 Native AOT가 되는 것은 아닙니다. 일상의 dotnet run이나 일반 실행은 JIT이고, Native AOT 컴파일의 실제 작업은 publish 때입니다.

PublishAot를 넣은 뒤의 일상PublishAot를 프로젝트에 두어도 일상의 dotnet run은 JIT 그대로이고, Native AOT 컴파일의 실제 작업은 publish 때 돈다. 평소부터 프로젝트에 두고 build와 publish 시점의 분석을 일상적으로 보는 것이 중요하다.csproj에 PublishAot를 둔다일상의 dotnet run은 JIT 그대로publish 때 AOT 컴파일분석과 warning을 일상적으로 본다

그림 14: 일상은 JIT, 실제 작업은 publish. 그래서 설정은 잠깐 붙임이 아니라 상시 두고 분석을 계속 본다.

6.2. publish

예를 들어 Windows x64용이라면 이런 형태입니다.

dotnet publish -c Release -r win-x64

Linux x64용이라면 이렇습니다.

dotnet publish -c Release -r linux-x64

출력은 RID 고정입니다. 「한 개로 어디서든 동작하는 .NET DLL」보다 「그 OS / 아키텍처용으로 만든 실행 파일」이라는 관점으로 바뀝니다.

Web API 쪽에서 손대려면 Native AOT 전제 템플릿에서 들어가는 편이 편합니다.

dotnet new webapiaot -o MyFirstAotWebApi

worker라면 이쪽입니다.

dotnet new worker -o WorkerWithAot --aot

6.3. JSON 작성법

Native AOT에서 은근히 자주 부딪히는 것이 JSON입니다. System.Text.Json은 평소 감각으로 쓰면 reflection 쪽으로 치우치기 쉬우므로, 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. 잘 맞는 경우

Native AOT가 기분 좋게 맞기 쉬운 것은 이런 경우입니다.

  • 시작이 주역인 CLI / 도구
  • 컨테이너로 대량 배포하는 작은 API
  • worker / 백그라운드 서비스
  • 서버리스나 단명 프로세스
  • 네이티브 앱에 끼워 넣는 작은 .NET 부품
  • 실행 환경에 .NET 런타임 사전 설치를 요구하고 싶지 않은 장면

공통점은 경계가 비교적 분명하고, 동적인 구조를 줄이기 쉽다는 것입니다.

8. 맞지 않는 경우

반대로 처음부터 Native AOT를 주력으로 두지 않는 편이 좋은 장면도 분명합니다.

  • WPF / WinForms의 기존 대규모 앱 본체
  • built-in COM interop 전제의 구성
  • 실행 시 plugin 로드가 주역인 앱
  • reflection으로 타입을 탐색하는 프레임워크 의존이 강한 구성
  • System.Reflection.Emit이나 동적 proxy를 당연하게 쓰는 라이브러리
  • C++/CLI를 끼운 설계

이 부근은 JIT 전제의 평범한 .NET, 또는 ReadyToRun, 또는 설계의 경계 재검토가 더 맞습니다.

9. 빠지기 쉬운 지점

마지막으로 Native AOT 첫걸음에서 밟기 쉬운 점을 정리합니다.

  • publish warning을 가볍게 본다
    • 앞에서 말한 대로 Native AOT의 warning은 진지하게 읽을 대상입니다.
  • build는 통과하는데 publish에서 깨진다
    • publish 시점에 의존 라이브러리까지 포함한 분석이 본격적으로 돌아가므로, 여기서 처음 보이는 것이 있습니다.
  • ReadyToRun과 Native AOT를 같은 기분으로 다룬다
    • 비슷한 말이지만 제약의 세기가 꽤 다릅니다.
  • desktop 앱 본체부터 갑자기 시작한다
    • 우선 console / worker / 작은 API가 더 무난합니다.
  • JSON이나 설정 바인딩을 평소 감각으로 쓴다
    • reflection 전제 작성법은 나중에 먹힙니다.
  • 플랫폼 독립인 줄 알고 배포한다
    • Native AOT의 배포물은 RID 고정입니다.
  • 「Native AOT = 무엇이든 빨라진다」고 생각한다
    • 주역은 시작, 배포, 실행 환경입니다. 여기를 빼면 기대치가 어긋납니다.

Native AOT에서는 가능 여부를 최종적으로 정하는 것은 dotnet build가 아니라 dotnet publish입니다. 여기를 일찍 돌리기 시작하면 후반에 덜 막힙니다.

가능 여부를 정하는 것은 publishbuild는 통과하는데 publish에서 깨지는 일이 있는 것은, publish 시점에 의존 라이브러리까지 포함한 분석이 본격적으로 돌아가기 때문이며, Native AOT의 가능 여부를 최종적으로 정하는 것은 dotnet build가 아니라 dotnet publish이다.dotnet build는 통과한다아직 가능 여부는 정해지지 않았다dotnet publish의존 포함 분석이 본격적으로 돈다여기서 처음 가능 여부가 보인다그래서 일찍 publish를 돌린다

그림 15: 빠지기 쉬운 지점의 공통점. 「build가 통과했다」로 안심하지 말고, publish를 일찍 돌린다.

10. 정리

Native AOT를 한 줄로 말하면, .NET 앱을 동적인 실행 모델에서, 정적으로 확정하기 쉬운 배포 모델로 치우치는 장치입니다.

봐 둘 포인트는 이 다섯 가지로 끝입니다.

  1. Native AOT는 publish 시점에 네이티브 코드로 미리 컴파일한다
  2. 시작, 메모리, 배포 사정에는 잘 먹힌다
  3. 그 대신 reflection, 동적 코드 생성, built-in COM, trimming 미대응 코드에는 엄하다
  4. 첫 대상은 desktop 본체보다 console / worker / 작은 API가 더 무난하다
  5. warning을 지우면서 publish 기반으로 일찍 확인하는 것이 중요하다

Native AOT는 모든 .NET 앱에 붙이는 표준 스위치가 아닙니다. 다만 시작이 중요하고, 배포를 가볍게 하고 싶고, 실행 환경의 전제를 줄이고 싶다는 장면에서는 꽤 강한 무기입니다.

반대로 WPF / WinForms / COM이 진한 세계에서는 아직 평범한 .NET이 더 맞는 장면도 많습니다. 여기를 가릴 수 있으면 Native AOT는 「어려운 신기능」이 아니라, 쓸 곳이 분명한 선택지가 됩니다.

11. 참고 자료

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

Native AOT란 무엇인가요?
.NET 앱을 publish 시점에 네이티브 코드로 미리 컴파일해 배포하는 게시 방식입니다. 실행 시 JIT를 쓰지 않으므로 시작 시간과 메모리 점유가 나아지기 쉽고, .NET 런타임이 설치되지 않은 환경에도 배포하기 쉬워집니다. 그 대신 자유로운 리플렉션, 동적 코드 생성, built-in COM, trimming 미대응 라이브러리와는 궁합이 나빠집니다. 빨라지는 마법이라기보다, 시작·배포·실행 환경 사정 때문에 동적인 세계를 조금 버리고 정적인 세계로 치우치는 배포 모델입니다.
Native AOT와 ReadyToRun은 무엇이 다른가요?
ReadyToRun은 IL을 남긴 채로 JIT의 일을 조금 앞당기는 방식이며, 실행 시 JIT를 아직 쓰는 장면이 있습니다. 호환성은 넓고 동적 기능도 대체로 쓰기 쉬운 편입니다. 반면 Native AOT는 실행 시 JIT 자체를 전제로 하지 않고, 배포물은 네이티브 실행 파일 중심이 되며, 시작은 상당히 나아지기 쉬운 대신 제약이 강해집니다. 같은 AOT라는 말이 붙어 있어도 ReadyToRun은 「JIT를 조금 덜어 주는」 방향, Native AOT는 「JIT를 전제로 하지 않는」 방향이라 결이 꽤 다릅니다.
WPF나 WinForms 앱에서 Native AOT를 쓸 수 있나요?
지금은 상당히 신중하게 보는 편이 좋습니다. Windows의 Native AOT에는 built-in COM이 없고, WPF는 trimming과 궁합이 나쁘며, WinForms는 built-in COM marshalling에 대한 의존이 무겁기 때문에, 둘 다 첫 Native AOT 후보로는 맞지 않습니다. COM이 필요하면 JIT 상태로 두거나 ComWrappers / source-generated COM을 전제로 설계를 다시 하는 편이 맞는 장면도 있습니다. 입구로는 콘솔, worker, 작은 Web API가 더 수월합니다.
Native AOT는 어떤 앱에 잘 맞나요?
시작이 주역인 CLI 도구, 컨테이너로 대량 배포하는 작은 API, worker나 백그라운드 서비스, 서버리스나 단명 프로세스, 네이티브 앱에 끼워 넣는 작은 .NET 부품, 실행 환경에 .NET 런타임 사전 설치를 요구하고 싶지 않은 장면에 맞습니다. 공통점은 경계가 비교적 분명하고 동적인 구조를 줄이기 쉽다는 것입니다. 반대로 실행 시 플러그인 로드가 주역인 앱이나, 리플렉션으로 타입을 탐색하는 프레임워크 의존이 강한 구성에는 맞지 않습니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기