이 페이지에는 자동 번역된 텍스트가 포함될 수 있습니다.

.NET에서 Native AOT 애플리케이션을 개발하는 방법

.NET 8은 Native AOT라고도 하는 ahead-of-time 컴파일을 포괄적으로 지원합니다. 이러한 애플리케이션은 일반적으로 관리형 솔루션보다 더 빠르게 시작하고 메모리를 덜 사용합니다.

.NET AOT 게시에서는 잘림이 적용된 self-contained 애플리케이션이 생성됩니다. 이러한 애플리케이션은:

  1. .NET 런타임이 설치되어 있지 않은 컴퓨터에서 실행할 수 있습니다.
  2. Windows x64와 같은 특정 런타임 환경을 대상으로 합니다.
  3. 사용되지 않는 코드를 포함하지 않습니다.

.NET AOT 애플리케이션 배포

저희는 PDF 처리를 위한 Docotic.Pdf 라이브러리를 개발합니다. AOT 호환성은 2024년에 가장 많이 요청된 기능 중 하나입니다. 이 글에서는 AOT 호환성을 달성하기까지의 과정을 설명합니다. .NET 프로젝트에서 AOT 문제를 찾고 수정하는 방법을 배울 수 있습니다.

.NET AOT 게시 기본 사항

Native AOT 애플리케이션을 게시하려면 프로젝트 파일에 다음 속성을 추가하세요:

<PropertyGroup>
    <PublishAot>true</PublishAot>
</PropertyGroup>

또한 일부 필수 조건을 설치해야 할 수 있습니다. 그런 다음 Visual Studio에서 앱을 게시할 수 있습니다. 또는 dotnet publish 명령을 사용하여 명령줄에서 프로젝트를 게시할 수 있습니다:

dotnet publish -r linux-x64 -c Release

.NET 라이브러리의 AOT 호환성

Native AOT 게시 기능은 .NET 7 및 .NET 8에서 사용할 수 있습니다. .NET 7에서는 콘솔 애플리케이션만 게시할 수 있습니다. .NET 8에서는 ASP.NET Core 애플리케이션도 추가로 게시할 수 있습니다.

하지만 Docotic.Pdf 핵심 라이브러리는 .NET Standard 2.0을 대상으로 합니다. 이것을 AOT 친화적으로 만들 수 있을까요? 물론입니다.

.NET Standard, .NET 5 또는 .NET 6을 대상으로 하는 .NET 라이브러리에서도 AOT 호환성 문제를 계속 찾아 수정할 수 있습니다. 그러면 Native AOT 애플리케이션이 그 라이브러리에 의존할 수 있습니다.

AOT 문제 찾기

AOT 호환성에 대한 첫 번째 지원 요청은 다음과 같습니다:

WebAssembly로 컴파일된 Docotic.Pdf를 사용하려고 검토 중입니다. AOT로 컴파일하면 트리밍 경고가 발생합니다.
warning IL2104: Assembly 'BitMiracle.Docotic.Pdf' produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries

그다지 유익하지 않습니다. 문제를 재현하고 이러한 트리밍 경고에 대한 더 자세한 정보를 확인해 보겠습니다.

게시 경고 가져오기

테스트 애플리케이션을 게시하는 것이 .NET AOT 문제를 찾는 가장 유연한 방법입니다. 다음이 필요합니다:

  1. PublishAot = true로 .NET 8 콘솔 프로젝트를 만듭니다.
  2. 프로젝트에 참조를 추가하고 해당 형식 중 일부를 사용합니다.

Docotic.Pdf에 사용하는 csproj 구성은 다음과 같습니다:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <PublishAot>true</PublishAot>
        <TrimmerSingleWarn>false</TrimmerSingleWarn>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        <WarningsAsErrors />
    </PropertyGroup>

    <ItemGroup>
        <ProjectReference Include="..\Docotic\Docotic.csproj" />
    </ItemGroup>
</Project>

TrimmerSingleWarn = false를 사용하면 경고에 대한 자세한 정보를 얻을 수 있습니다. 그렇지 않으면 "Assembly 'X' produced trim warnings" 메시지만 표시됩니다.

그리고 Program.cs의 C# 코드는 다음과 같습니다:

using BitMiracle.Docotic.Pdf;

using var pdf = new PdfDocument();

검사 중인 어셈블리의 로드를 강제하는 코드를 추가하세요. 게시 경고를 얻기 위해 프로젝트의 모든 기능을 다룰 필요는 없습니다. 단일 형식을 사용하는 것만으로 충분합니다. 하지만 런타임 오류를 포착하려면 더 많은 코드를 작성해야 합니다.

이 프로젝트에 대해 dotnet publish -r win-x64 -c Release 명령을 실행하면 트리밍 및 AOT 경고가 반환되었습니다. 간단한 목록은 다음과 같습니다:

AOT analysis warning IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling.

Trim analysis warning IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): Using member 'System.Linq.Expressions.Expression.Property(Expression,String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code.

Trim analysis warning IL2057: LogProviders.LogProviderBase.FindType(String,String[]): Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'.

Trim analysis warning IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The parameter 'loggerType' of method 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Trim analysis warning IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String)'. The return value of method 'LogProviders.LogProviderBase.FindType(String,String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

근본 원인을 찾았습니다. 문제를 수정하기 전에, AOT 호환성 문제를 찾는 대안적 옵션을 확인해 보겠습니다.

AOT 호환성 분석기 사용

Microsoft는 .NET 7 또는 .NET 8 프로젝트에서 AOT 문제를 찾기 위한 Roslyn 분석기를 제공합니다. 코드를 작성할 때 AOT 문제를 강조 표시할 수 있습니다. 하지만 테스트 애플리케이션을 게시하면 더 많은 문제를 감지합니다.

정적 분석을 사용하려면 .csproj 파일에 다음 속성을 추가하세요:

<PropertyGroup>
    <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

.NET Standard 2.0 프로젝트에서 이러한 분석을 사용하려면 .NET 7 또는 .NET 8 프레임워크를 대상으로 해야 합니다. 프로젝트 구성은 다음과 같을 수 있습니다:

<PropertyGroup>
    <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
    <IsAotCompatible Condition="'$(TargetFramework)'=='net8.0'">true</IsAotCompatible>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors />
</PropertyGroup>

저희는 이 구성을 대규모 핵심 프로젝트에 적용했고 1036개의 빌드 오류를 얻었습니다.

많은 빌드 오류

이 오류들이 모두 AOT 호환성과 관련 있었을까요? 저희는 IsAotCompatible 속성 없이 프로젝트를 다시 빌드했습니다. 여전히 1036개의 오류가 있었습니다.

모든 오류는 .NET Standard 2.0에 비해 .NET 8에서 개선된 사항과 관련되어 있었습니다. 저희 경우 대부분의 문제는 nullable 참조 형식 지원 개선과 관련 있었습니다. 또한 새로운 ArgumentOutOfRangeException.ThrowIfLessThanArgumentNullException.ThrowIfNull 헬퍼 메서드에 대한 CA1500 및 CA1512 오류도 많이 있었습니다.

저희는 모든 오류를 수정하고 IsAotCompatible 속성을 켰습니다. 프로젝트 빌드는 AOT 오류 없이 끝났습니다. 정적 분석은 게시 과정에서 발견된 경고를 전혀 잡아내지 못했습니다 🙁

이를 확인하기 위해 다음 테스트 코드를 코드베이스에 추가했습니다:

Type t = typeof(int);
t = typeof(List<>).MakeGenericType(t);
Console.WriteLine(Activator.CreateInstance(t));

string s = Console.ReadLine() ?? string.Empty;
Type? type = Type.GetType(s);
if (type != null)
{
    foreach (var m in type.GetMethods())
        Console.WriteLine(m.Name);
}

이 C# 코드는 .NET AOT 및 트리밍과 100% 호환되지 않습니다. 정적 분석은 다음 오류를 식별했습니다:

error IL3050: Using member 'System.Type.MakeGenericType(params Type[])' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. The native code for this instantiation might not be available at runtime.

error IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'. It's not possible to guarantee the availability of the target type.

따라서 Roslyn 분석기는 작동하며 일부 문제를 찾을 수 있습니다. 저희 경우에는 아무것도 찾지 못했습니다.

런타임 오류 포착

라이브러리를 native AOT와 호환되게 만드는 방법 문서에는 다음 원칙이 나와 있습니다:

애플리케이션이 AOT용으로 게시될 때 경고가 없다면, AOT 이후에도 AOT가 없을 때와 동일하게 동작합니다.

실제로는 AOT 후 버전이 여전히 다르게 동작할 수 있습니다. 저희는 모든 AOT 및 트리밍 경고를 수정했습니다. 모든 자동 테스트도 통과했습니다. 테스트 애플리케이션은 PDF 문서를 올바르게 처리했습니다. 그런데 동일한 애플리케이션의 AOT 버전은 잘못된 문서를 생성했습니다.

AOT 애플리케이션은 여전히 작동하지 않았습니다

AOT 게시가 필요한 일부 코드를 잘못 제거했습니다. 저희는 필요한 코드를 제거하는 최적화 이슈를 만들었습니다. .NET 팀은 이를 신속하게 확인하고 수정했습니다. 그리고 수정이 제공될 때까지 C# 코드에 임시 대응을 추가했습니다.

이 이야기의 교훈은 게시 및 정적 분석 경고만으로는 충분하지 않을 수 있다는 점입니다. 테스트 애플리케이션에서 프로젝트의 핵심 기능을 사용하세요. 그리고 배포 후 테스트 애플리케이션을 실행하세요.

Docotic.Pdf 테스트 애플리케이션은 PDF 압축, 텍스트 추출 및 기타 핵심 기능을 사용합니다. 다음과 같이 사용합니다:

AotCompatibility.exe compress "C:\compressed.pdf" "C:\input.pdf" "optional-password"

게시된 애플리케이션을 수동으로 테스트하는 것은 편리하지 않습니다. 이 과정을 자동화할 가치가 있습니다.

AOT 문제 자동 감지

Docotic.Pdf 개발은 자동 테스트를 기반으로 합니다. 모든 기능이나 버그에 대한 테스트가 있습니다. .NET AOT 호환성도 예외는 아닙니다.

Native AOT 애플리케이션의 테스트를 자동화하는 방법은 두 가지가 있습니다.

AOT 애플리케이션 외부에 테스트 배치

여기서는 일반 NUnit/xUnit/MSTest 프로젝트를 사용합니다. 이 프로젝트를 빌드하기 전에 AOT 테스트 애플리케이션을 게시합니다. 테스트는 Process.Start 메서드를 사용해 게시된 앱을 실행합니다.

주요 장점은 테스트 인프라와 AOT 앱이 분리된다는 점입니다. 단위 테스트 프레임워크의 전체 기능을 사용할 수 있습니다. 테스트 프레임워크에 AOT 호환성 문제가 있어도 상관없습니다. 기존 테스트 사례를 재사용하기도 더 쉽습니다.

테스트 프로젝트의 샘플 구성은 GitHub에서 확인할 수 있습니다. Docotic.Tests 프로젝트에는 관리형 버전과 AOT 버전 모두에 대한 자동 테스트가 포함되어 있습니다. AotCompatibility는 AOT 게시를 위한 테스트 애플리케이션입니다.

AotCompatibility.csprojRelease 구성에서 빌드 후 이벤트로 테스트 애플리케이션을 게시합니다. '$(PublishProtocol)'=='' 조건은 Visual Studio에서 "Publish" 마법사를 사용할 때 이 단계를 비활성화합니다.

<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release' And '$(PublishProtocol)'==''">
    <Exec Command="&quot;$(SolutionDir)Scripts\publish.bat&quot;" />
</Target>

publish.bat 스크립트는 간단합니다:

@echo off

SET AOT_TEST_DIR=%~dp0\..\AotCompatibility
dotnet publish "%TRIM_TEST_DIR%\AotCompatibility.csproj" --no-build /warnaserror /p:PublishProfile="%AOT_TEST_DIR%\Properties\PublishProfiles\win-x64-installer.pubxml"

여기서는 --no-build 인수를 사용한다는 점에 주의하세요. 프로젝트는 이미 빌드 후 이벤트 전에 빌드되어 있습니다. 다시 빌드할 필요가 없습니다.

NativeAot.cs의 테스트는 게시된 AotCompatibility.exe를 서로 다른 인수로 실행합니다. Docotic.TestsAotCompatibility 프로젝트는 동일한 테스트 대상 코드를 공유합니다. 이를 통해 코드를 중복하지 않고 관리형 버전과 AOT 버전을 모두 확인할 수 있습니다.

AOT 애플리케이션 내부에 테스트 배치

대안적인 접근 방식은 AOT 테스트 애플리케이션 내부에 테스트를 작성하는 것입니다. 2024년 7월 기준으로는 MSTest 초기 미리 보기만 사용할 수 있습니다. 자세한 내용은 네이티브 AOT 애플리케이션 테스트 문서를 읽어보세요.

핵심 문제는 테스트 프레임워크의 어떤 AOT 문제든 테스트를 신뢰할 수 없게 만든다는 점입니다. 또한 제한된 테스트 프레임워크 기능만 사용할 수 있습니다. 그래서 저희는 테스트 인프라를 AOT 애플리케이션 외부에 두는 방식을 선호합니다.

AOT 및 트리밍 경고 수정

게시 경고를 억제하지 마세요. 일반적으로 경고 억제는 잘못된 해결책입니다. AOT 또는 트리밍 경고를 억제하면 프로젝트가 AOT와 호환된다고 말하는 것과 같습니다. 그러나 경고의 근본 원인은 여전히 남아 있습니다. 억제된 경고는 결국 애플리케이션의 런타임 실패로 이어질 수 있습니다.

각 경고와 관련된 코드를 제거하거나 다시 작성해 보세요. 서드파티 구성 요소를 업데이트하는 것도 도움이 될 수 있습니다.

Docotic.Pdf에서 AOT 및 트리밍 경고를 어떻게 수정했는지 살펴보겠습니다. 이미 게시 경고 가져오기 섹션에서 일부를 보셨습니다.

LibLog의 트리밍 경고

로깅을 위해 LibLog 라이브러리를 사용했습니다. LibLog는 리플렉션에 의존하여 널리 사용되는 로깅 프레임워크를 감지하고 사용합니다. 설계상 AOT와 호환되지 않습니다.

현재 .NET에서 로깅의 표준은 Microsoft.Extensions.Logging 패키지입니다. 그리고 LibLog 개발은 현재 동결된 상태입니다.

그에 따라 저희는 코드에서 LibLog를 완전히 제거했습니다. 대신 Docotic.Pdf.Logging 애드온을 출시했습니다. 이 애드온은 Microsoft.Extensions.Logging.Abstractions 인터페이스에 의존합니다. 그 결과 핵심 라이브러리 크기가 줄었고 관련된 모든 트리밍 경고가 해결되었습니다.

트리밍 경고 없음

Docotic.Layout 애드온의 디버깅 코드

레이아웃 애드온에서는 내부 디버깅을 위해 리플렉션을 사용합니다. 이로 인해 트리밍 분석 경고 IL2075가 발생했습니다.

해당 코드는 코드베이스에 존재하지만 클라이언트는 공용 API를 통해 사용할 수 없습니다. 해결책은 Release 구성에서 해당 코드를 제외하는 것입니다. 이제는 Debug 구성에서만 사용됩니다.

BouncyCastle의 AOT 경고 IL3050

BouncyCastle 라이브러리는 PDF 문서 서명에 도움이 됩니다. 다음 경고는 해당 코드에서 발생합니다:

Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): Using member 'System.Enum.GetValues(Type)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. It might not be possible to create an array of the enum type at runtime. Use the GetValues<TEnum> overload or the GetValuesAsUnderlyingType method instead.

BouncyCastle은 AOT와 호환되지 않는 Enum.GetValues(Type) 메서드를 사용합니다.

가장 간단한 해결책은 대신 Enum.GetValues<T>() 오버로드를 사용하는 것입니다. 최신 BouncyCastle 버전은 이미 이를 사용합니다. 안타깝게도 이 오버로드는 .NET 5 이상에서만 사용할 수 있습니다. 이는 .NET Standard 2.0에서는 선택지가 아닙니다.

저희는 더 깊이 파고들어 BouncyCastle 코드를 분석했습니다. 그 결과 BouncyCastle이 열거형 상수의 난독화를 방지하기 위해 이를 사용한다는 사실을 알게 되었습니다. 대신 [System.Reflection.Obfuscation(Exclude = true)] 특성을 사용해 해당 열거형의 난독화를 비활성화했습니다. 그리고 더 이상 필요하지 않은 Enum.GetValues(Type) 사용을 제거했습니다.

난독화된 빌드의 이상한 문제

저희는 배포 빌드를 보호하기 위해 난독화를 사용합니다. 놀랍게도 난독화된 빌드의 .NET AOT 배포에서도 비슷한 경고가 반환되었습니다:

<unknown>:0: error: symbol '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' is already defined

이러한 오류에는 관련 IL30## 또는 IL2### 코드가 없었습니다. 관련 코드를 어떻게 식별해야 하는지 불분명했습니다.

오류는 난독화된 버전에서만 발생했습니다. 난독화 도구의 버그일 수 있습니다. 난독화 도구를 업데이트했지만 오류는 여전히 남아 있었습니다.

문제의 범위를 좁혀야 했습니다. 저희는 이진 검색에 기반한 다음 절차를 사용했습니다:

  1. 절반의 네임스페이스에 대해 난독화를 비활성화합니다. 오류가 발생하는지 확인합니다. 오류를 유발하는 네임스페이스를 찾을 때까지 계속합니다.
  2. 1단계에서 찾은 네임스페이스 안의 절반의 형식에 대해 난독화를 비활성화합니다. 오류가 발생하는지 확인합니다. 오류를 유발하는 형식을 찾을 때까지 계속합니다.
  3. 2단계에서 찾은 형식 안의 절반의 멤버에 대해 난독화를 비활성화합니다. 오류가 발생하는지 확인합니다. 오류를 유발하는 멤버를 찾을 때까지 계속합니다.
  4. 3단계의 멤버 코드를 검토합니다. 특이한 부분을 주석 처리하고 오류가 발생하는지 확인합니다.

결국 1단계에서 끝났습니다 🙂 모든 네임스페이스에 대한 난독화를 비활성화한 후에도 오류는 계속 발생했습니다.

ILSpy로 생성된 어셈블리를 분석했습니다. 몇 개의 예상치 못한 컴파일러 생성 형식을 발견했습니다. ILSpy는 비슷한 C# 코드에서의 사용 예를 보여주었습니다:

interface X
{
    void f();
}

X obj = ..;
if (obj.f != null)
    ..

이 이상한 코드는 오래된 스타일 C 코드를 C#으로 마이그레이션하는 과정에서 나왔습니다. if (obj.f != null) 조건은 완전히 중복됩니다. 저희는 이러한 조건을 제거했고 오류는 사라졌습니다.

GetCallingAssembly를 사용하지 마세요

또 다른 런타임 문제를 발견했습니다. LicenseManager.AddLicenseData(string) 메서드 호출은 AOT 게시 후 다음 오류와 함께 실패했습니다:

Assembly.GetCallingAssembly() throws System.PlatformNotSupportedException: Operation is not supported on this platform.

Assembly.GetCallingAssembly 메서드는 .NET 9에서 Native AOT용으로 구현되어야 합니다. 수정이 준비될 때까지 저희는 부정적 영향을 최소화하도록 코드를 리팩터링했습니다. 이제 Application License 검증에만 호출 어셈블리 특성을 사용합니다. 다른 라이선스 유형은 AOT와 호환됩니다.

특수 특성으로 코드를 표시

다행히도 핵심 Docotic.Pdf 라이브러리와 대부분의 애드온에서 모든 AOT 및 트리밍 경고를 수정할 수 있었습니다. 하지만 모든 AOT 비호환 코드를 다시 작성할 수는 없을 수도 있습니다.

.NET은 이러한 상황을 위한 특수한 특성을 제공합니다. 알려진 AOT 문제를 클라이언트에 알리기 위해 API에 특성을 표시합니다. API 사용자는 표시된 메서드를 호출할 때 경고를 받게 됩니다.

AOT 비호환 .NET 코드를 표시하는 데 중요한 특성은 다음과 같습니다:

  • RequiresDynamicCode
  • RequiresUnreferencedCode
  • DynamicallyAccessedMembers

이러한 특성에 대한 자세한 내용은 공식 AOT 경고 소개.NET 라이브러리를 트리밍용으로 준비 문서에서 확인할 수 있습니다.

결론

Native AOT 배포는 .NET 세계에서 큰 진전입니다. 일반 C# 코드를 작성해서 네이티브 애플리케이션이나 라이브러리를 얻을 수 있습니다. 이러한 애플리케이션은 일반적으로 더 빠르고 .NET 런타임이 설치되어 있지 않아도 실행할 수 있습니다. C, Rust 또는 다른 비-.NET 프로그래밍 언어에서 게시된 DLL도 사용할 수 있습니다.

AOT 호환성 문제를 찾으려면 테스트용 .NET 8 애플리케이션을 게시하세요. Roslyn 분석기도 있지만, 발견하는 문제는 더 적습니다.

현재 핵심 Docotic.Pdf 라이브러리는 AOT 및 트리밍과 호환됩니다. 다음 애드온도 호환됩니다:

  • Conformance 애드온
  • Layout 애드온
  • Gdi 애드온
  • Logging 애드온

HTML to PDF 애드온에는 아직 트리밍 경고가 남아 있습니다. 이것이 Native AOT 로드맵의 다음 목표입니다.

Native AOT 또는 PDF 처리에 대해 질문이 있으면 문의해 주세요.