핵심 정리
#nullable enable
string name = "Mina"; // null 아님 — 컴파일러가 null 대입 경고
string? nickname = null; // null 허용
// null 체크 후 사용
if (nickname is not null)
Console.WriteLine(nickname.Length); // 경고 없음 — 컴파일러가 non-null로 추론
// 런타임 검사 (공개 API 경계)
ArgumentNullException.ThrowIfNull(name);문법
어떤 표기가 있나
nullable reference type은 표기만 보면 아래 세 가지가 핵심입니다.
string name = "Mina"; // null 불허
string? nick = null; // null 허용
string safe = nick!; // 경고 억제즉 ?는 null 허용, !는 컴파일러 경고 억제입니다. 둘은 반대 역할이므로 같이 읽어야 실수를 줄일 수 있습니다.
nullable reference type이란
C# 8 이전에는 참조 타입 변수에 항상 null을 대입할 수 있었습니다. nullable reference type(NRT)은 #nullable enable 설정 아래에서 "이 변수가 null이 될 수 있는가"를 타입에 명시하는 기능입니다.
#nullable enable
string name = "Mina"; // null 불허 — null 대입 시 경고 CS8600
string? nick = null; // null 허용
name = null; // ⚠️ CS8600 경고
string len = nick.Length; // ⚠️ CS8602 경고 — nick이 null일 수 있음컴파일 타임 경고입니다. 런타임에 null이 자동으로 사라지지 않으며, 기존 코드와 완전 호환됩니다.
컴파일러의 null flow analysis
컴파일러는 코드 흐름을 분석해 어느 시점에 변수가 null일 수 있는지 추적합니다.
string? value = GetValue();
// if로 null 체크 후 — 컴파일러가 non-null로 추론
if (value != null)
Console.WriteLine(value.Length); // ✅ 경고 없음
// is not null 패턴
if (value is not null)
Console.WriteLine(value.Length); // ✅
// ?? 로 기본값 제공
string safe = value ?? "default"; // safe는 string (non-null)
Console.WriteLine(safe.Length); // ✅
// 삼항
int len = value?.Length ?? 0; // ✅null-forgiving 연산자 !
컴파일러의 null 경고를 억제하고 싶을 때 !(null-forgiving)을 씁니다. 실제 null을 제거하지 않으므로 잘못 쓰면 런타임 NullReferenceException이 발생합니다.
string? maybeNull = GetValue();
string definite = maybeNull!; // 경고 억제 — 개발자가 non-null을 보장
// 테스트 코드에서 자주 사용
var user = _db.Find<User>(id)!; // "테스트 환경에서는 반드시 존재"!는 최소한으로만 사용하세요. 남용하면 NRT의 핵심 이점인 컴파일 타임 null 안전성이 사라집니다.
컴파일러 설정 — 프로젝트 전체 적용
#nullable enable을 파일마다 쓰는 대신 .csproj에서 프로젝트 전체에 적용할 수 있습니다.
<!-- .csproj -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>.NET 6+ 프로젝트 템플릿은 기본으로 활성화되어 있습니다.
런타임 검사와 조합
NRT는 컴파일 타임 도구입니다. 공개 API 경계에서는 런타임 검사도 함께 써야 합니다.
public void Process(string name) // non-null 선언
{
// 컴파일 타임: 경고로 보호
// 런타임: 외부에서 null을 반사(reflection)나 dynamic으로 전달할 수도 있음
ArgumentNullException.ThrowIfNull(name); // .NET 6+
// 또는
if (name is null) throw new ArgumentNullException(nameof(name));
}선택 기준
| 상황 | 선택 |
|---|---|
| null이 절대 허용되지 않는 필수 데이터 | string |
| 외부 입력처럼 비어 있을 수 있는 값 | string? |
| 경고를 임시로 억제 | ! — 원인을 반드시 주석으로 설명 |
string? raw = GetValue();
// ❌ null 가능성을 무시
// Console.WriteLine(raw.Length);
// ✅ 먼저 좁힌다
if (raw is not null)
Console.WriteLine(raw.Length);체크포인트
| 문법 | 의미 |
|---|---|
string | null 불허 (nullable 활성화 시) |
string? | null 허용 |
#nullable enable | 파일 단위 nullable 분석 활성화 |
<Nullable>enable</Nullable> | 프로젝트 전체 활성화 |
value! | null-forgiving — 경고 억제 (런타임 보호 없음) |
주의할 점
! null-forgiving 연산자는 경고만 숨길 뿐 실제 null을 제거하지 않습니다. 원인을 이해하고 최소한으로만 쓰세요. 또 nullable 경고를 전부 !로 무시하면 "설계상의 빈틈을 미리 발견하는 효과"가 사라집니다.
레거시 코드를 NRT로 마이그레이션할 때 한 번에 전체를 바꾸는 것은 위험합니다. 파일 단위로 #nullable enable을 붙여가며 점진적으로 적용하는 편이 안전합니다.
참고 링크
2 sources