콘텐츠로 이동

C# Nullable Reference Types 완전 정복

Nullable Reference Types(NRT)는 C# 8.0에서 도입된 기능으로, 참조 타입의 null 가능성을 컴파일 타임에 분석하여 NullReferenceException을 사전에 방지합니다. 기존의 int?(Nullable Value Type)와 달리, 런타임 동작을 변경하지 않고 컴파일러 정적 분석만 추가합니다.


<!-- .csproj: 전체 프로젝트 활성화 -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// 파일 단위 제어
#nullable enable // 이 파일에서 NRT 활성화
#nullable disable // 이 파일에서 비활성화
#nullable restore // 프로젝트 설정으로 복원

// nullable enable 상태에서:
string name = "Alice"; // null 불허 (non-nullable)
string? title = null; // null 허용 (nullable)
// null 불허 타입에 null 할당 시 경고
string name2 = null; // CS8600: 'null' 리터럴 또는 가능한 null 값을 null 허용하지 않는 형식에 변환
// null 허용 타입을 역참조하면 경고
int len = title.Length; // CS8602: null일 수 있는 참조의 역참조

컴파일러는 코드 흐름을 추적하여 null 안전성을 판단합니다.

string? GetName() => null;
void Example()
{
string? name = GetName();
// 직접 사용: 경고
Console.WriteLine(name.Length); // CS8602
// null 체크 후: 경고 없음
if (name != null)
{
Console.WriteLine(name.Length); // OK
}
// null-conditional 연산자
int? len = name?.Length; // OK (len은 null 가능)
// null-coalescing
string display = name ?? "Unknown"; // OK (display는 non-nullable)
// 패턴 매칭
if (name is string s)
{
Console.WriteLine(s.Length); // OK (s는 non-nullable string)
}
// is not null
if (name is not null)
{
Console.WriteLine(name.Length); // OK
}
}

// 매개변수
void Greet(string name) // name은 null 불허
void GreetOptional(string? name) // name은 null 허용
// 반환값
string GetName() // 항상 non-null 반환 보장
string? FindName() // null 반환 가능
// 구현 시 계약 이행
string GetName()
{
return "Alice"; // OK
// return null; // CS8603: 가능한 null 참조 반환
}

// T가 참조 타입일 때 null 허용
T? GetFirst<T>(IList<T> list) where T : class
=> list.Count > 0 ? list[0] : null;
// T가 값 타입일 때 Nullable<T> 반환
T? GetFirst<T>(IList<T> list) where T : struct
=> list.Count > 0 ? list[0] : null;
// T가 둘 다일 때 (unconstrained)
// T?는 T가 참조 타입이면 T?, 값 타입이면 Nullable<T>
T? GetFirst<T>(IList<T> list)
=> list.Count > 0 ? list[0] : default;

컴파일러에 추가적인 null 정보를 제공하는 어트리뷰트입니다.

using System.Diagnostics.CodeAnalysis;
// [NotNull]: 반환 시 null이 아님을 보장
public static string GetOrThrow([NotNull] string? value)
{
ArgumentNullException.ThrowIfNull(value);
return value; // 이후 non-null
}
// [MaybeNull]: 반환값이 null일 수 있음
[return: MaybeNull]
public T GetValue<T>(string key)
=> _dict.TryGetValue(key, out var val) ? val : default;
// [NotNullWhen]: 반환이 true일 때 매개변수가 non-null
public static bool TryParse(string? s, [NotNullWhen(true)] out User? user)
{
if (string.IsNullOrEmpty(s)) { user = null; return false; }
user = new User(s);
return true;
}
// 사용
if (TryParse(input, out var user))
{
Console.WriteLine(user.Name); // user는 non-null (경고 없음)
}
// [MemberNotNull]: 메서드 호출 후 멤버가 non-null
class Config
{
public string? ConnectionString { get; private set; }
[MemberNotNull(nameof(ConnectionString))]
public void Initialize(string connStr)
{
ConnectionString = connStr;
}
public void Connect()
{
Initialize("Server=localhost;");
// ConnectionString은 이제 non-null
Console.WriteLine(ConnectionString.Length); // OK
}
}

컴파일러의 null 경고를 억제할 때 사용합니다. 반드시 확실한 경우에만 사용하세요.

string? name = GetName();
// ! 연산자: "나는 이게 null이 아님을 안다"
string definitelyNotNull = name!;
// 초기화 순서 문제 해결
class Service
{
private readonly IRepository _repo = null!; // DI 주입 예정
public Service(IRepository repo)
{
_repo = repo;
}
}
// ASP.NET Core에서 자주 사용
[Required]
public string Name { get; set; } = null!; // 모델 바인더가 채워줌

<!-- 단계적 마이그레이션: warnings만 먼저 -->
<Nullable>warnings</Nullable>
<!-- 또는 annotations만 (경고 없이 어노테이션 문법 허용) -->
<Nullable>annotations</Nullable>
// 파일별 점진적 적용
// 안정화된 파일부터 enable
#nullable enable
public class StableClass
{
public string Name { get; } // non-nullable
public string? Description { get; } // nullable
}

// Dictionary 접근
Dictionary<string, string> dict = new();
// TryGetValue 사용 (권장)
if (dict.TryGetValue("key", out string? value))
{
Console.WriteLine(value.Length); // value는 non-null
}
// LINQ
string[] names = { "Alice", null!, "Bob" };
// Where로 null 필터 (컴파일러는 여전히 string? 추론)
IEnumerable<string?> filtered = names.Where(n => n != null);
// OfType으로 null 제거 (non-nullable 결과)
IEnumerable<string> nonNull = names.OfType<string>();

// record: 기본 생성자 매개변수에 NRT 적용
record Person(string Name, string? Email);
var alice = new Person("Alice", null); // OK
var bob = new Person(null!, "[email protected]"); // 경고 없음 (null-forgiving)

NRT는 코드 품질을 높이는 강력한 도구지만, 일시에 전환하면 수백 개의 경고가 쏟아질 수 있습니다. Nullable=warnings로 시작해 파일 단위로 #nullable enable을 적용하고, [NotNull], [MaybeNull], [NotNullWhen] 어트리뷰트로 null 계약을 명확히 표현하는 것이 성공적인 마이그레이션의 핵심입니다.