Null 허용 여부 이해

완료됨

.NET 개발자라면 System.NullReferenceException이 발생한 경험이 있을 것입니다. 이는 런타임에 null이(가) 역참조되는 경우, 즉 런타임에 변수가 계산되지만 변수가 null을(를) 참조하는 경우 발생합니다. 이는 .NET 에코시스템 내에서 가장 일반적으로 발생하는 예외입니다. null을(를) 개발한 Tony Hoare는 null을(를) "수십억 달러짜리 실수"라고 합니다.

다음 예제에서는 FooBar 변수가 null에 할당되고 즉시 역참조되므로 문제가 발생합니다.

// Declare variable and assign it as null.
FooBar fooBar = null;

// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();

// The FooBar type definition.
record FooBar(int Id, string Name);

앱의 크기와 복잡성이 증가하면 개발자가 문제를 파악하기가 훨씬 더 어려워집니다. 이와 같은 잠재적 오류를 발견하는 것은 도구를 사용해야 하는 작업이며 C# 컴파일러가 도움이 될 수 있습니다.

Null 안전성 정의

null 안전성이라는 용어는 가능한 NullReferenceException 발생 횟수를 줄이는 데 도움이 되는 nullable 형식과 관련된 기능 집합을 정의합니다.

위의 FooBar 예제에서는 역참조 전에 fooBar 변수가 null인지 여부를 확인하면 NullReferenceException을(를) 방지할 수 있습니다.

// Declare variable and assign it as null.
FooBar fooBar = null;

// Check for null
if (fooBar is not null)
{
    _ = fooBar.ToString();
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

이와 같은 시나리오를 식별하는 데 도움이 되도록 컴파일러는 코드의 의도를 유추하고 의도된 동작을 적용할 수 있습니다. 그러나 이는 null 허용 컨텍스트를 사용하는 경우에만 해당됩니다. Null 허용 컨텍스트에 대해 논의하기 전에 가능한 null 허용 형식을 설명하겠습니다.

Nullable 유형

C# 2.0 이전에는 참조 형식만 null을 허용했습니다. int 또는 DateTime과 같은 값 형식은 null이 될 수 없습니다. 이러한 형식이 값 없이 초기화되면 해당 default 값으로 대체됩니다. int의 경우에는 0입니다. DateTime의 경우에는 DateTime.MinValue입니다.

초기 값 없이 인스턴스화된 참조 형식은 다르게 작동합니다. 모든 참조 형식의 default 값은 null입니다.

다음 C# 코드 조각을 살펴보겠습니다.

string first;                  // first is null
string second = string.Empty   // second is not null, instead it's an empty string ""
int third;                     // third is 0 because int is a value type
DateTime date;                 // date is DateTime.MinValue

앞의 예제에서:

  • firstnull입니다. 참조 형식 string이 선언되었지만 할당이 없기 때문입니다.
  • second는 선언될 때 string.Empty가 할당됩니다. 이 개체에는 null 할당이 없었습니다.
  • third은(는) 할당되지 않았음에도 불구하고 0입니다. 이 개체는 struct(값 형식)이며 default 값은 0입니다.
  • date은(는) 초기화되지 않았지만 default 값은 System.DateTime.MinValue입니다.

C# 2.0부터 Nullable<T>(또는 줄여서 T?)를 사용하여 null 허용 값 형식을 정의할 수 있습니다. 이렇게 하면 값 형식이 null을 허용할 수 있습니다. 다음 C# 코드 조각을 살펴보겠습니다.

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null
int? third = default;  // third is null as the default value for Nullable<Int32> is null
int? fourth = new();    // fourth is 0, since new calls the nullable constructor

앞의 예제에서:

  • firstnull입니다. null 허용 값 형식은 초기화되지 않기 때문입니다.
  • second는 선언될 때 null가 할당됩니다.
  • thirdnull입니다. Nullable<int>default 값이 null이기 때문입니다.
  • fourth0입니다. new() 식은 Nullable<int> 생성자를 호출하며 기본적으로 int0이기 때문입니다.

C# 8.0에서 null 허용 참조 형식이 도입되었습니다. 이를 통해 참조 형식이 null일 수 있음 또는 항상 null이 아님이라는 의도를 표현할 수 있습니다. "모든 참조 유형이 null 허용이라고 생각했어요!"라고 생각하실 수 있습니다. 여러분의 생각은 틀리지 않았습니다. 이 기능을 사용하면 의도를 표현할 수 있으며, 그러면 컴파일러가 해당 의도를 적용하려고 합니다. 동일한 T? 구문은 참조 형식이 null 허용임을 표현합니다.

다음 C# 코드 조각을 살펴보겠습니다.

#nullable enable

string first = string.Empty;
string second;
string? third;

위의 예제에서 컴파일러는 다음과 같이 의도를 다음과 같이 유추합니다.

  • first는 절대로 null이 아닙니다. 반드시 값이 할당되기 때문입니다.
  • second는 처음에 null이더라도 절대로 null이면 안 됩니다. 값을 할당하기 전에 second를 계산하면 초기화되지 않았기 때문에 컴파일러 경고가 발생합니다.
  • thirdnull일 수 있습니다. 예를 들어 System.String을 가리킬 수도 있지만 null을 가리킬 수도 있습니다. 이러한 변형은 모두 허용됩니다. 컴파일러는 null이 아닌지 먼저 확인하지 않고 third을 역참조하는 경우 경고하여 도움을 줍니다.

중요

위와 같이 null 허용 참조 형식 기능을 사용하려면 null 허용 컨텍스트 내에 있어야 합니다. 이 내용은 다음 섹션에서 자세히 설명합니다.

Null 허용 컨텍스트

Nullable 컨텍스트를 통해 컴파일러가 참조 형식 변수를 해석하는 방식을 미세하게 제어할 수 있습니다. 다음 네 가지 null 허용 컨텍스트가 가능합니다.

  • disable: 컴파일러가 C# 7.3 이하와 유사하게 동작합니다.
  • enable: 컴파일러가 모든 null 참조 분석 및 모든 언어 기능을 사용하도록 설정합니다.
  • warnings: 컴파일러가 모든 null 분석을 수행하고 코드가 null을 역참조할 수 있는 경우 경고를 내보냅니다.
  • annotations: 컴파일러가 null 분석을 수행하지도 않고 코드가 null을 역참조할 수 있는 경우 경고를 내보내지도 않지만 개발자가 null 허용 참조 형식 ? 및 null 허용 연산자(!)를 사용하여 코드에 주석을 달 수 있습니다.

이 모듈에서는 disable 또는 enable null 허용 컨텍스트만 다룹니다. 자세한 내용은 Null 허용 참조 형식: Null 허용 컨텍스트를 참조하세요.

Null 허용 참조 형식 사용

C# 프로젝트 파일(.csproj)에서 자식 <Nullable> 노드를 <Project> 요소에 추가하거나 기존 <PropertyGroup>에 추가합니다. 이렇게 하면 전체 프로젝트에 enable null 허용 컨텍스트가 적용됩니다.

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

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <!-- Omitted for brevity -->

</Project>

또는 컴파일러 지시문을 사용하여 null 허용 컨텍스트의 범위를 C# 파일로 지정할 수 있습니다.

#nullable enable

위의 C# 컴파일러 지시문은 프로젝트 구성과 기능적으로 동일하지만 해당 지시문이 있는 파일로 범위가 지정됩니다. 자세한 내용은 Null 허용 참조 형식: Null 허용 컨텍스트(문서)를 참조하세요.

중요

Null 허용 컨텍스트는 .NET 6.0 이상의 모든 C# 프로젝트 템플릿에서 기본적으로 .csproj 파일에서 사용하도록 설정됩니다.

Null 허용 컨텍스트를 사용하도록 설정하면 새 경고가 표시됩니다. 위의 FooBar 예제를 보면 null 허용 컨텍스트에서 분석될 때 다음 두 가지 경고가 발생합니다.

  1. FooBar fooBar = null; 줄에는 null 할당에 대한 다음과 같은 경고가 있습니다. “C# 경고 CS8600: Null 리터럴 또는 가능한 null 값을 null을 허용하지 않은 형식으로 변환하는 중입니다”.

    C# Warning CS8600 스크린샷: Null 리터럴 또는 가능한 null 값을 null을 허용하지 않은 형식으로 변환하는 중입니다.

  2. _ = fooBar.ToString(); 줄에도 경고가 있습니다. 이번에는 컴파일러가 fooBar가 null일 수 있다고 우려합니다. “C# 경고 CS8602: 가능한 null 참조에 대한 역참조입니다”.

    C# 경고 CS8602 스크린샷: 가능한 null 참조에 대한 역참조입니다.

중요

모든 경고에 대응하고 제거하더라도 null 안전성은 보장되지 않습니다. 컴파일러의 분석을 전달하지만 런타임 NullReferenceException을(를) 초래하는 몇 가지 제한된 시나리오가 있습니다.

요약

이 단원에서는 C#에서 null 허용 컨텍스트를 사용하도록 설정하여 NullReferenceException을 방어하는 방법을 알아보았습니다. 다음 단원에서는 null 허용 컨텍스트에서 의도를 명시적으로 표현하는 방법을 자세히 알아봅니다.