트리밍 경고 소개

개념적으로 트리밍은 간단합니다. 애플리케이션을 게시하면 .NET SDK가 전체 애플리케이션을 분석하고 사용되지 않는 코드를 모두 제거합니다. 그러나 사용되지 않는 코드를 확인하는 것은 어려울 수 있습니다.

애플리케이션을 자를 때 동작이 변경되지 않도록 하기 위해 .NET SDK는 자르기 경고를 통해 자르기 호환성에 대한 정적 분석을 제공합니다. 트리머는 트리밍과 호환되지 않을 수 있는 코드를 발견하면 자르기 경고를 생성합니다. 자르기 호환이 아닌 코드는 잘린 후 애플리케이션에서 동작 변경 또는 충돌을 일으킬 수 있습니다. 이상적으로는 트리밍을 사용하는 모든 애플리케이션이 자르기 경고를 생성하지 않아야 합니다. 트리밍 경고가 있는 경우에는 동작이 변경되지 않도록 트리밍 후 애플리케이션을 철저히 테스트해야 합니다.

이 문서는 일부 패턴에서 자르기 경고가 생성되는 이유와 이러한 경고를 해결하는 방법을 이해하는 데 도움이 됩니다.

트리밍 경고의 예

대부분의 C# 코드는 사용되는 코드와 사용되지 않는 코드를 결정하는 것이 간단합니다. 트리머는 메서드 호출, 필드 및 속성 참조 등을 탐색하고 어느 코드에 액세스하는지 확인할 수 있습니다. 아쉽게도 리플렉션 같은 일부 기능에는 상당한 문제가 있습니다. 다음 코드를 살펴보세요.

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

이 예제에서는 GetType()이 알 수 없는 이름의 형식을 동적으로 요청하고 모든 메서드의 이름을 출력합니다. 게시 시간에서 어떤 형식 이름을 사용해야 하는지 알 수 없기 때문에 출력에서 보존할 형식을 결정할 수 있는 방법은 없습니다. 이 코드는 자르기 전에 작동했을 가능성이 높습니다(입력이 대상 프레임워크에 존재하는 것으로 알려진 것일 경우). 그러나 유형을 찾을 수 없으면 Type.GetType이 null을 반환하므로 자른 후 null 참조 예외가 발생할 수 있습니다.

이 경우 트리머는 Type.GetType에 대한 호출에 대해 경고를 발생시켜 애플리케이션에서 사용할 유형을 결정할 수 없음을 나타냅니다.

트리밍 경고 대응

트리밍 경고는 트리밍에 대한 예측 가능성을 제공하기 위한 것입니다. 표시될 수 있는 경고에는 크게 두 가지 범주가 있습니다.

  1. 기능이 트리밍과 호환되지 않음
  2. 기능에는 자르기 호환을 위한 입력에 대한 특정 요구 사항이 있습니다.

트리밍과 호환되지 않는 기능

이러한 메서드는 일반적으로 전혀 작동하지 않거나 일부 경우 잘린 애플리케이션에서 사용되는 경우 손상될 수 있습니다. 좋은 예는 이전 예의 Type.GetType 메서드입니다. 잘린 앱에서는 작동할 수 있지만 보장할 수는 없습니다. 이러한 API는 RequiresUnreferencedCodeAttribute로 표시됩니다.

RequiresUnreferencedCodeAttribute는 간단하고 광범위합니다. 멤버가 트리밍과 호환되지 않는 주석을 달았음을 의미하는 특성입니다. 이 특성은 코드가 기본적으로 트리밍 호환되지 않는 경우 또는 트리밍 종속성이 너무 복잡하여 트리머에게 설명할 수 없는 경우에 사용됩니다. 이는 LoadFrom(String) 등을 통해 코드를 동적으로 로드하거나, GetType() 등을 통해 애플리케이션이나 어셈블리의 모든 유형을 열거 또는 검색하거나, C# dynamic 키워드를 사용하거나, 다른 런타임 코드 생성 기술을 사용하는 메서드의 경우 종종 해당됩니다. 예를 들면 다음과 같습니다.

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad()
{
    ...
    Assembly.LoadFrom(...);
    ...
}

void TestMethod()
{
    // IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
    // can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
    MethodWithAssemblyLoad();
}

RequiresUnreferencedCode 해결 방법은 많지 않습니다. 가장 좋은 방법은 트리밍할 때 해당 메서드를 호출하지 않고 트리밍 호환되는 다른 메서드를 사용하는 것입니다.

기능을 트리밍과 호환되지 않는 것으로 표시

라이브러리를 작성 중이고 호환되지 않는 기능을 사용할지 여부를 제어할 수 없는 경우 RequiresUnreferencedCode로 표시할 수 있습니다. 이는 트리밍과 호환되지 않는 방법으로 주석을 표시합니다. RequiresUnreferencedCode를 사용하면 지정된 메서드의 모든 자르기 경고가 무음으로 표시되지만 다른 사람이 이를 호출할 때마다 경고가 생성됩니다.

RequiresUnreferencedCodeAttribute를 사용하려면 Message를 지정해야 합니다. 메시지는 표시된 메서드를 호출하는 개발자에게 보고되는 경고의 일부로 표시됩니다. 예시:

IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>

위의 예에서 특정 메서드에 대한 경고는 다음과 같습니다.

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.

이러한 API를 호출하는 개발자는 일반적으로 영향을 받는 API의 특정 사항이나 트리밍과 관련된 특정 사항에 관심이 없습니다.

좋은 메시지는 트리밍과 호환되지 않는 기능을 명시하고 개발자에게 잠재적인 다음 단계를 안내해야 합니다. 다른 기능을 사용하거나 해당 기능이 사용되는 방식을 변경하도록 제안할 수도 있습니다. 또한 명확한 대체 없이 해당 기능이 아직 트리밍과 호환되지 않는다고 간단히 명시할 수도 있습니다.

개발자를 위한 지침이 경고 메시지에 포함되기에는 너무 길어지면 RequiresUnreferencedCodeAttribute에 선택적 Url을 추가하여 개발자에게 문제와 가능한 솔루션을 더 자세히 설명하는 웹 페이지를 알려줄 수 있습니다.

예시:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }

그러면 다음과 같은 경고가 발생합니다.

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. https://site/trimming-and-method

같은 이유로 RequiresUnreferencedCode를 사용하면 더 많은 메서드를 표시하게 되는 경우가 많습니다. 이는 트리밍과 호환되지 않는 하위 수준 메서드를 호출하기 때문에 개략적인 메서드가 트리밍과 호환되지 않는 경우에 일반적입니다. 공용 API에 대한 경고를 "버블링"합니다. RequiresUnreferencedCode를 사용할 때마다 메시지가 필요하며 이러한 경우 메시지는 동일할 가능성이 높습니다. 문자열 중복을 방지하고 유지 관리를 더 쉽게 만들려면 상수 문자열 필드를 사용하여 메시지를 저장합니다.

class Functionality
{
    const string IncompatibleWithTrimmingMessage = "This functionality is not compatible with trimming. Use 'FunctionalityFriendlyToTrimming' instead";

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    private void ImplementationOfAssemblyLoading()
    {
        ...
    }

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    public void MethodWithAssemblyLoad()
    {
        ImplementationOfAssemblyLoading();
    }
}

입력 요구 사항이 있는 기능

트리밍은 자르기 호환 코드로 이어지는 메서드 및 기타 멤버에 대한 입력에 대한 더 많은 요구 사항을 지정하는 API를 제공합니다. 이러한 요구 사항은 일반적으로 형식에 대한 특정 멤버 또는 작업에 액세스하는 기능과 리플렉션에 관한 것입니다. 이러한 요구 사항은 DynamicallyAccessedMembersAttribute를 사용하여 지정됩니다.

RequiresUnreferencedCode와 달리 리플렉션은 올바른 주석이 추가된 경우 때때로 트리머가 인식할 수 있습니다. 원래 예제를 다시 살펴보겠습니다.

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

이전 예에서 실제 문제는 Console.ReadLine()입니다. 모든 형식을 읽을 수 있기 때문에 트리머는 System.DateTime 또는 System.Guid 또는 기타 형식에서 메서드가 필요한지 여부를 알 수 있는 방법이 없습니다. 반면에 다음 코드는 괜찮습니다.

Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

여기서는 트리머가 참조되는 정확한 형식, 즉 System.DateTime을 알 수 있습니다. 이제 흐름 분석을 사용하여 System.DateTime에서 모든 공용 메서드를 유지해야 하는지 결정할 수 있습니다. 그렇다면 DynamicallyAccessMembers는 어디에서 제공되나요? 리플렉션을 여러 메서드 간에 분할하는 경우 다음 코드에서는 System.DateTime 형식이 System.DateTime의 메서드에 액세스하기 위해 리플렉션이 사용되는 Method3으로 흘러가는 것을 볼 수 있습니다.

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(Type type)
{
    var methods = type.GetMethods();
    ...
}

이전 코드를 컴파일하면 다음 경고가 생성됩니다.

IL2070: Program.Method3(Type): 'this' 인수는 'System.Type.GetMethods()' 호출에서 'DynamicallyAccessedMemberTypes.PublicMethods'를 충족하지 않습니다. 'Program.Method3(Type)' 메서드의 'type' 매개 변수에 일치하는 주석이 없습니다. 원본 값은 최소한 할당되는 대상 위치에 선언된 것과 동일한 요구 사항을 선언해야 합니다.

성능 및 안정성 흐름 분석은 메서드 간에 수행되지 않으므로 리플렉션 호출(GetMethods)에서 Type의 원본으로 메서드 간에 정보를 전달하는 데 주석이 필요합니다. 이전 예에서 트리머 경고는 GetMethodsPublicMethods 주석을 갖기 위해 호출된 Type 개체 인스턴스가 필요하지만 type 변수에는 동일한 요구 사항이 없다는 것을 의미합니다. 즉, 요구 사항을 GetMethods에서 호출자에게 전달해야 합니다.

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

매개 변수 type에 주석을 추가하면 원래 경고가 사라지지만 다른 경고가 나타납니다.

IL2087: 'type' 인수는 'Program.Method3(Type)' 호출에서 'DynamicallyAccessedMemberTypes.PublicMethods'를 충족하지 않습니다. 'Program.Method2<T>()'의 일반 매개 변수 'T'에 일치하는 주석이 없습니다.

Method3의 매개 변수 type까지 주석을 전파했습니다. Method2에도 비슷한 문제가 있습니다. 트리머는 typeof에 대한 호출을 통해 흐르고 지역 변수 t에 할당되고 Method3에 전달되는 T 값을 추적할 수 있습니다. 이 시점에서 매개 변수 type에는 PublicMethods가 필요하지만 T에는 요구 사항이 없음을 확인하고 새로운 경고를 생성합니다. 이 문제를 해결하려면 정적으로 알려진 형식(예: System.DateTime 또는 System.Tuple) 또는 다른 주석이 달린 값에 도달할 때까지 호출 체인 전체에 주석을 적용하여 "주석을 달고 전파"해야 합니다. 이 경우 Method2의 형식 매개 변수 T에 주석을 달아야 합니다.

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

이제 트리머는 런타임 리플렉션(공용 메서드)을 통해 액세스할 수 있는 멤버와 형식(System.DateTime)을 알고 이를 유지하므로 경고가 없습니다. 트리머가 무엇을 보존해야 할지 알 수 있도록 주석을 추가하는 것이 가장 좋습니다.

영향을 받는 코드가 RequiresUnreferencedCode가 있는 메서드에 있는 경우 이러한 추가 요구 사항으로 인해 생성된 경고는 자동으로 억제됩니다.

단순히 비호환성을 보고하는 RequiresUnreferencedCode와 달리 DynamicallyAccessedMembers를 추가하면 코드가 트리밍과 호환됩니다.

트리머 경고 억제

호출이 안전하고 필요한 코드가 하나도 트리밍되지 않음을 확인할 수 있는 경우 UnconditionalSuppressMessageAttribute를 사용하여 경고를 표시하지 않을 수도 있습니다. 예시:

[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
    Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
void TestMethod()
{
    InitializeEverything();

    MethodWithAssemblyLoad(); // Warning suppressed

    ReportResults();
}

Warning

자르기 경고를 억제할 때는 매우 주의해야 합니다. 현재 호출이 자르기 호환 가능하지만 변경될 수 있는 코드를 변경하면 모든 제거 사항을 검토하는 것을 잊어버릴 수도 있습니다.

UnconditionalSuppressMessageSuppressMessage와 유사하지만 publish 및 다른 빌드 후 도구에서 볼 수 있습니다.

Important

트리머 경고를 표시하지 않으려면 SuppressMessage 또는 #pragma warning disable을 사용하지 마세요. 이는 컴파일러에서만 작동하지만 컴파일된 어셈블리에서는 유지되지 않습니다. 트리머는 컴파일된 어셈블리에서 작동하며 제거를 볼 수 없습니다.

제거는 전체 메서드 본문에 적용됩니다. 따라서 위 샘플에서는 메서드의 모든 IL2026 경고를 억제합니다. 주석을 추가하지 않는 한 어떤 방법이 문제가 있는지 명확하지 않기 때문에 이해하기가 더 어렵습니다. 더 중요한 것은 ReportResults도 자르기와 호환되지 않는 경우와 같이 나중에 코드가 변경되는 경우 이 메서드 호출에 대해 경고가 보고되지 않는다는 것입니다.

문제가 있는 메서드 호출을 별도의 메서드나 로컬 함수로 리팩터링한 다음 해당 메서드에만 제거를 적용하면 이 문제를 해결할 수 있습니다.

void TestMethod()
{
    InitializeEverything();

    CallMethodWithAssemblyLoad();

    ReportResults();

    [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
        Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
    void CallMethodWithAssemblyLoad()
    {
        MethodWIthAssemblyLoad(); // Warning suppressed
    }
}