다음을 통해 공유


트리밍을 위해 .NET 라이브러리 준비

.NET SDK를 사용하면 자르기를 통해 독립형 앱의 크기를 줄일 수 있습니다. 트리밍은 앱과 해당 종속성에서 사용되지 않는 코드를 제거합니다. 모든 코드가 트리밍과 호환되는 것은 아닙니다. .NET은 트리밍된 앱을 중단할 수 있는 패턴을 검색하는 트리밍 분석 경고를 제공합니다. 이 문서의 내용:

필수 조건

.NET 8 SDK 이상.

라이브러리 트리밍 경고 사용

라이브러리의 자르기 경고는 다음 방법 중 하나로 찾을 수 있습니다.

  • IsTrimmable 속성을 사용하여 프로젝트별 트리밍을 사용하도록 설정합니다.
  • 라이브러리를 사용하는 트리밍 테스트 앱을 만들고 테스트 앱에 대한 트리밍을 사용하도록 설정합니다. 라이브러리의 모든 API를 참조할 필요는 없습니다.

두 가지 방식을 모두 사용하는 것이 좋습니다. 프로젝트별 트리밍은 편리하고 한 프로젝트에 대한 트림 경고를 표시하지만 모든 경고를 보려면 트림 호환으로 표시된 참조에 의존합니다. 테스트 앱을 다듬는 것은 더 많은 작업이지만 모든 경고를 표시합니다.

프로젝트별 트리밍 사용

프로젝트 파일에서 <IsTrimmable>true</IsTrimmable>을 설정합니다.

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

MSBuild 속성 IsTrimmabletrue로 설정하면 어셈블리가 "트리밍 가능"으로 표시되고 자르기 경고가 사용하도록 설정됩니다. "트리밍 가능"은 프로젝트를 의미합니다.

  • 트리밍과 호환되는 것으로 간주됩니다.
  • 빌드할 때 자르기 관련 경고를 생성해서는 안 됩니다. 잘린 앱에서 사용될 때 어셈블리의 사용되지 않은 멤버는 최종 출력에서 잘립니다.

프로젝트를 <IsAotCompatible>true</IsAotCompatible>과 AOT 호환으로 구성할 때 IsTrimmable 속성의 기본값은 true입니다. 자세한 내용은 AOT 호환성 분석기를 참조하세요.

프로젝트를 트리밍 호환으로 표시하지 않고 자르기 경고를 생성하려면 <IsTrimmable>true</IsTrimmable> 대신 <EnableTrimAnalyzer>true</EnableTrimAnalyzer>를 사용합니다.

테스트 앱으로 모든 경고 표시

라이브러리에 대한 모든 분석 경고를 표시하려면 트리머는 라이브러리의 구현과 라이브러리가 사용하는 모든 종속성을 분석해야 합니다.

라이브러리를 빌드하고 게시할 때:

  • 종속성 구현을 사용할 수 없습니다.
  • 사용 가능한 참조 어셈블리에는 트리머가 트리밍과 호환되는지 확인할 수 있는 충분한 정보가 없습니다.

종속성 제한 사항으로 인해 라이브러리와 해당 종속성을 사용하는 자체 포함 테스트 앱을 만들어야 합니다. 테스트 앱에는 트리머가 트리밍 비호환성에 대한 경고를 발급하는 데 필요한 모든 정보가 포함되어 있습니다.

  • 라이브러리 코드입니다.
  • 라이브러리가 종속성에서 참조하는 코드입니다.

참고 항목

라이브러리가 대상 프레임워크에 따라 동작이 다른 경우 트리밍을 지원하는 각 대상 프레임워크에 대해 트리밍 테스트 앱을 만듭니다. 예를 들어, 라이브러리가 동작을 변경하기 위해 #if NET7_0과 같은 조건부 컴파일을 사용하는 경우입니다.

트리밍 테스트 앱을 만들려면:

  • 별도의 콘솔 애플리케이션 프로젝트를 만듭니다.
  • 라이브러리에 대한 참조를 추가합니다.
  • 다음 목록을 사용하여 아래 표시된 프로젝트와 유사한 프로젝트를 수정합니다.

라이브러리가 트리밍할 수 없는 TFM(예: net472 또는 netstandard2.0)을 대상으로 하는 경우 트리밍 테스트 앱을 만들어도 이점이 없습니다. 트리밍은 .NET 6 이상에서만 지원됩니다.

  • <PublishTrimmed>true</PublishTrimmed>를 추가합니다.
  • <ProjectReference Include="/Path/To/YourLibrary.csproj" />를 사용하여 라이브러리 프로젝트에 대한 참조를 추가합니다.
  • <TrimmerRootAssembly Include="YourLibraryName" />을 사용하여 라이브러리를 트리머 루트 어셈블리로 지정합니다.
    • TrimmerRootAssembly는 라이브러리의 모든 부분이 분석되도록 합니다. 이 어셈블리가 "루트"임을 트리머에 알려 줍니다. "루트" 어셈블리는 트리머가 라이브러리의 모든 호출을 분석하고 해당 어셈블리에서 발생하는 모든 코드 경로를 트래버스함을 의미합니다.

.csproj 파일

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

프로젝트 파일이 업데이트되면 대상 RID(런타임 식별자)를 사용하여 dotnet publish를 실행합니다.

dotnet publish -c Release -r <RID>

여러 라이브러리에 대해서는 앞의 패턴을 따릅니다. 한 번에 두 개 이상의 라이브러리에 대한 자르기 분석 경고를 보려면 해당 라이브러리를 모두 동일한 프로젝트에 ProjectReferenceTrimmerRootAssembly 항목으로 추가합니다. ProjectReferenceTrimmerRootAssembly 항목을 사용하여 동일한 프로젝트에 모든 라이브러리를 추가하면 루트 라이브러리 중 하나가 종속성에서 자르기에 적합하지 않은 API를 사용하는 경우 종속성에 대해 경고합니다. 특정 라이브러리에서만 수행해야 하는 경고를 보려면 해당 라이브러리만 참조하세요.

참고 항목

분석 결과는 종속성의 구현 세부 정보에 따라 달라집니다. 종속성의 새 버전으로 업데이트하면 분석 경고가 발생할 수 있습니다.

  • 새 버전에 이해할 수 없는 반사 패턴이 추가된 경우.
  • API 변경이 없더라도 마찬가지입니다.
  • 자르기 분석 경고를 도입하는 것은 라이브러리가 PublishTrimmed와 함께 사용될 때 호환성이 손상되는 변경입니다.

트리밍 경고 해결

이전 단계에서는 트리밍된 앱에서 사용할 때 문제를 일으킬 수 있는 코드에 대한 경고를 생성합니다. 다음 예에서는 가장 일반적인 경고와 이를 해결하기 위한 권장 사항을 보여 줍니다.

RequiresUnreferencedCode

지정된 메서드가 정적으로 참조되지 않는 코드(예: System.Reflection을 통해)에 대한 동적 액세스가 필요함을 나타내기 위해 [RequiresUnreferencedCode]를 사용하는 다음 코드를 고려합니다.

public class MyLibrary
{
    public static void MyMethod()
    {
        // warning IL2026 :
        // MyLibrary.MyMethod: Using 'MyLibrary.DynamicBehavior'
        // which has [RequiresUnreferencedCode] can break functionality
        // when trimming app code.
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

앞의 강조 표시된 코드는 라이브러리가 트리밍과 호환되지 않는 것으로 명시적으로 주석이 달린 메서드를 호출한다는 것을 나타냅니다. 경고를 제거하려면 MyMethodDynamicBehavior를 호출해야 하는지 여부를 고려합니다. 그렇다면 호출자 MyMethod에 경고를 전파하는 [RequiresUnreferencedCode]로 주석을 달아 MyMethod의 호출자가 대신 경고를 가져오도록 합니다.

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

특성을 공용 API까지 전파한 후에는 라이브러리를 호출하는 앱이 다음을 수행합니다.

  • 자를 수 없는 공용 메서드에 대해서만 경고를 가져옵니다.
  • IL2104: Assembly 'MyLibrary' produced trim warnings와 같은 경고를 가져오지 마세요.

DynamicallyAccessedMembers

public class MyLibrary3
{
    static void UseMethods(Type type)
    {
        // warning IL2070: MyLibrary.UseMethods(Type): 'this' argument does not satisfy
        // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
        // 'System.Type.GetMethods()'.
        // The parameter 't' of method 'MyLibrary.UseMethods(Type)' doesn't have
        // matching annotations.
        foreach (var method in type.GetMethods())
        {
            // ...
        }
    }
}

앞의 코드에서 UseMethods[DynamicallyAccessedMembers] 요구 사항이 있는 리플렉션 메서드를 호출합니다. 요구 사항에 따르면 형식의 퍼블릭 메서드를 사용할 수 있습니다. UseMethods의 매개 변수에 동일한 요구 사항을 추가하여 요구 사항을 충족합니다.

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

이제 UseMethods 호출이 PublicMethods 요구 사항을 충족하지 않는 값을 전달하면 경고가 발생합니다. [RequiresUnreferencedCode]와 유사하게 이러한 경고를 공용 API에 전파하면 작업이 완료됩니다.

다음 예에서는 알 수 없는 형식이 주석이 달린 메서드 매개 변수로 유입됩니다. 알 수 없는 Type은 다음 필드에서 왔습니다.

static Type type;
static void UseMethodsHelper()
{
    // warning IL2077: MyLibrary.UseMethodsHelper(Type): 'type' argument does not satisfy
    // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
    // 'MyLibrary.UseMethods(Type)'.
    // The field 'System.Type MyLibrary::type' does not have matching annotations.
    UseMethods(type);
}

마찬가지로 type 필드가 이러한 요구 사항과 함께 매개 변수로 전달되는 것이 문제입니다. 필드에 [DynamicallyAccessedMembers]를 추가하면 문제가 해결됩니다. [DynamicallyAccessedMembers]는 필드에 호환되지 않는 값을 할당하는 코드에 대해 경고합니다. 경우에 따라 이 프로세스는 공용 API에 주석이 추가될 때까지 계속되고, 다른 경우에는 구체적인 형식이 이러한 요구 사항이 있는 위치로 유입될 때 종료됩니다. 예시:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

이 경우 자르기 분석은 Tuple의 공용 메서드를 유지하고 추가 경고를 생성합니다.

권장 사항

  • 가능하면 리플렉션을 방지합니다. 리플렉션을 사용할 때는 라이브러리의 작은 부분에서만 도달할 수 있도록 리플렉션 범위를 최소화합니다.
  • 가능한 경우 트리밍 요구 사항을 정적으로 표현하려면 코드에 DynamicallyAccessedMembers 주석을 답니다.
  • DynamicallyAccessedMembers로 주석을 달 수 있는 분석 가능한 패턴을 따르도록 코드를 다시 구성하는 것이 좋습니다.
  • 코드가 트리밍과 호환되지 않는 경우 코드에 RequiresUnreferencedCode 주석을 추가하고 관련 공용 API에 주석이 추가될 때까지 이 주석을 호출자에게 전파합니다.
  • 정적 분석에서 이해되지 않는 방식으로 리플렉션을 사용하는 코드를 사용하지 마세요. 예를 들어, 정적 생성자의 리플렉션은 피해야 합니다. 정적 생성자에서 정적으로 분석할 수 없는 리플렉션을 사용하면 클래스의 모든 멤버에 경고가 전파됩니다.
  • 가상 메서드나 인터페이스 메서드에 주석을 달지 마세요. 가상 또는 인터페이스 메서드에 주석을 추가하려면 모든 재정의에 일치하는 주석이 있어야 합니다.
  • API가 대부분 호환되지 않는 경우 API에 대한 대체 코딩 방법을 고려해야 할 수 있습니다. 일반적인 예로 리플렉션 기반 직렬 변환기가 있습니다. 이러한 경우에는 더 쉽게 정적으로 분석되는 코드를 생성하기 위해 소스 생성기와 같은 다른 기술을 채택하는 것이 좋습니다. 예를 들어, System.Text.Json에서 원본 생성을 사용하는 방법을 참조하세요.

분석할 수 없는 패턴에 대한 경고 해결

가능하면 [RequiresUnreferencedCode]DynamicallyAccessedMembers를 사용하여 코드의 의도를 표현함으로써 경고를 해결하는 것이 좋습니다. 그러나 경우에 따라 해당 특성으로 표현할 수 없거나 기존 코드를 리팩터링하지 않고도 패턴을 사용하는 라이브러리의 트리밍을 사용하도록 설정하는 데 관심이 있을 수 있습니다. 이 섹션에서는 트리밍 분석 경고를 해결하는 몇 가지 고급 방법을 설명합니다.

Warning

이러한 기술을 잘못 사용하면 동작이나 코드가 변경되거나 런타임 예외가 발생할 수 있습니다.

UnconditionalSuppressMessage

다음과 같은 코드를 고려해보세요.

  • 의도는 주석으로 표현할 수 없습니다.
  • 경고를 생성하지만 런타임 시 실제 문제를 나타내지는 않습니다.

에서 경고를 표시 UnconditionalSuppressMessageAttribute하지 않을 수 있습니다. SuppressMessageAttribute와 비슷하지만, 이 특성은 IL에서 유지되고 트리밍 분석 중에 적용됩니다.

Warning

경고를 억제할 때 검사 및 테스트를 통해 사실로 알려진 고정성을 기반으로 코드의 자르기 호환성을 보장할 책임은 사용자에게 있습니다. 이러한 주석에 주의해야 합니다. 주석이 올바르지 않거나 코드의 고정성이 변경되면 결국 잘못된 코드가 숨겨질 수 있기 때문입니다.

예시:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        // warning IL2063: TypeCollection.Item.get: Value returned from method
        // 'TypeCollection.Item.get' can't be statically determined and may not meet
        // 'DynamicallyAccessedMembersAttribute' requirements.
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

이전 코드에서는 반환된 TypeCreateInstance의 요구 사항을 충족하도록 인덱서 속성에 주석이 추가되었습니다. 이렇게 하면 TypeWithConstructor 생성자가 유지되고 CreateInstance 호출이 경고하지 않습니다. 인덱서 setter 주석은 Type[]에 저장된 모든 형식에 생성자가 있는지 확인합니다. 그러나 분석에서 이를 확인할 수 없으며, 반환된 형식에 해당 생성자가 유지되고 있음을 알 수 없기 때문에 getter에 대한 경고가 계속 생성됩니다.

요구 사항이 충족되면 getter에 [UnconditionalSuppressMessage]를 추가하여 이 경고를 해결할 수 있습니다.

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
            Justification = "The list only contains types stored through the annotated setter.")]
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

리플렉션된 멤버가 리플렉션의 가시적 대상이 되도록 보장하는 주석이나 코드가 있는 경우에만 경고를 억제하는 것이 유효하다는 점을 강조해야 합니다. 멤버가 호출, 필드 또는 속성 액세스의 대상인 것으로 충분하지 않습니다. 경우에 따라 표시될 수 있지만 더 많은 트리밍 최적화가 추가됨에 따라 이러한 코드는 결국 중단될 수밖에 없습니다. 리플렉션의 표시 대상이 아닌 속성, 필드 및 메서드를 인라인 처리하거나, 이름을 제거하거나, 다른 형식으로 이동하거나, 반사를 중단하는 방식으로 최적화할 수 있습니다. 경고를 억제할 때 다른 곳의 트리밍 분석기에 대한 반사 대상이었던 대상에만 반사하는 것이 허용됩니다.

// Invalid justification and suppression: property being non-reflectively
// used by the app doesn't guarantee that the property will be available
// for reflection. Properties that are not visible targets of reflection
// are already optimized away with Native AOT trimming and may be
// optimized away for non-native deployment in the future as well.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
    Justification = "*INVALID* Only need to serialize properties that are used by"
                    + "the app. *INVALID*")]
public string Serialize(object o)
{
    StringBuilder sb = new StringBuilder();
    foreach (var property in o.GetType().GetProperties())
    {
        AppendProperty(sb, property, o);
    }
    return sb.ToString();
}

DynamicDependency

[DynamicDependency] 특성을 사용하여 멤버에게 다른 멤버에 대한 동적 종속성이 있음을 나타낼 수 있습니다. 이렇게 하면 특성을 포함하는 멤버가 유지될 때마다 참조된 멤버가 유지되지만 단독으로 경고가 해제되지 않습니다. 코드의 반영 동작에 대한 자르기 분석을 알리는 다른 특성과 달리 [DynamicDependency]는 다른 멤버만 유지합니다. 이 특성은 [UnconditionalSuppressMessage]와 함께 사용하여 일부 분석 경고를 해결할 수 있습니다.

Warning

다른 방식을 실행할 수 없는 경우 최후의 수단으로만 [DynamicDependency] 특성을 사용합니다. [RequiresUnreferencedCode] 또는 [DynamicallyAccessedMembers]를 사용하여 반사 동작을 표현하는 것이 좋습니다.

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

DynamicDependency가 없으면 다른 곳에서 참조되지 않는 경우 트리밍이 MyAssembly에서 Helper를 제거하거나 MyAssembly를 완전히 제거하여 런타임에 실패할 수 있음을 나타내는 경고가 생성될 수 있습니다. 이 특성은 Helper가 유지되도록 합니다.

이 특성은 string 또는 DynamicallyAccessedMemberTypes를 통해 유지할 멤버를 지정합니다. 형식과 어셈블리는 특성 컨텍스트에서 암시적이거나 특성에서 명시적으로 지정됩니다(형식 및 어셈블리 이름에 대해 Type 또는 string을 사용).

형식 및 멤버 문자열은 멤버 접두사 없이 C# 설명서 설명 ID 문자열 형식의 변형을 사용합니다. 멤버 문자열은 선언 형식의 이름을 포함하지 않아야 하며 지정된 이름의 모든 멤버를 유지하기 위해 매개 변수를 생략할 수 있습니다. 다음 코드에는 형식의 몇 가지 예가 나와 있습니다.

[DynamicDependency("MyMethod()")]
[DynamicDependency("MyMethod(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType"
                                                 , "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency(
    "MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]

[DynamicDependency] 특성은 DynamicallyAccessedMembersAttribute를 사용해도 분석할 수 없는 리플렉션 패턴이 메서드에 포함된 경우 사용하도록 디자인되어 있습니다.