C# Nullable Reference Types 완전 정복
Nullable Reference Types(NRT)는 C# 8.0에서 도입된 기능으로, 참조 타입의 null 가능성을 컴파일 타임에 분석하여 NullReferenceException을 사전에 방지합니다. 기존의 int?(Nullable Value Type)와 달리, 런타임 동작을 변경하지 않고 컴파일러 정적 분석만 추가합니다.
1. 활성화
섹션 제목: “1. 활성화”<!-- .csproj: 전체 프로젝트 활성화 --><PropertyGroup> <Nullable>enable</Nullable></PropertyGroup>// 파일 단위 제어#nullable enable // 이 파일에서 NRT 활성화#nullable disable // 이 파일에서 비활성화#nullable restore // 프로젝트 설정으로 복원2. 기본 어노테이션
섹션 제목: “2. 기본 어노테이션”// 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일 수 있는 참조의 역참조3. null 분석 흐름 (Flow Analysis)
섹션 제목: “3. null 분석 흐름 (Flow Analysis)”컴파일러는 코드 흐름을 추적하여 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 }}4. 메서드 매개변수와 반환값
섹션 제목: “4. 메서드 매개변수와 반환값”// 매개변수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 참조 반환}5. 제네릭과 NRT
섹션 제목: “5. 제네릭과 NRT”// 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;6. 어노테이션 어트리뷰트
섹션 제목: “6. 어노테이션 어트리뷰트”컴파일러에 추가적인 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-nullpublic 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-nullclass 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 }}7. null-forgiving 연산자 (!)
섹션 제목: “7. null-forgiving 연산자 (!)”컴파일러의 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!; // 모델 바인더가 채워줌8. 마이그레이션 전략
섹션 제목: “8. 마이그레이션 전략”<!-- 단계적 마이그레이션: warnings만 먼저 --><Nullable>warnings</Nullable>
<!-- 또는 annotations만 (경고 없이 어노테이션 문법 허용) --><Nullable>annotations</Nullable>// 파일별 점진적 적용// 안정화된 파일부터 enable#nullable enablepublic class StableClass{ public string Name { get; } // non-nullable public string? Description { get; } // nullable}9. 컬렉션 패턴
섹션 제목: “9. 컬렉션 패턴”// Dictionary 접근Dictionary<string, string> dict = new();
// TryGetValue 사용 (권장)if (dict.TryGetValue("key", out string? value)){ Console.WriteLine(value.Length); // value는 non-null}
// LINQstring[] 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>();10. 레코드와 NRT
섹션 제목: “10. 레코드와 NRT”// record: 기본 생성자 매개변수에 NRT 적용record Person(string Name, string? Email);
var alice = new Person("Alice", null); // OKNRT는 코드 품질을 높이는 강력한 도구지만, 일시에 전환하면 수백 개의 경고가 쏟아질 수 있습니다. Nullable=warnings로 시작해 파일 단위로 #nullable enable을 적용하고, [NotNull], [MaybeNull], [NotNullWhen] 어트리뷰트로 null 계약을 명확히 표현하는 것이 성공적인 마이그레이션의 핵심입니다.