블로그로 돌아가기
C#

C# CS8618 경고가 발생하는 이유와 해결 방법

C#에서 non-nullable 프로퍼티를 선언할 때 발생하는 CS8618 경고의 원인을 설명합니다. 생성자 초기화, nullable 타입, required, null!의 차이와 상황별 선택 기준을 정리합니다.

C#.NETCS8618Nullable Reference Typesnullrequired
C# CS8618 경고가 발생하는 이유와 해결 방법

C# CS8618 경고가 발생하는 이유와 해결 방법

다음과 같이 string 프로퍼티를 선언하면 CS8618 경고가 발생할 수 있습니다.

public class User { public string Name { get; set; } }

Namenull을 직접 대입하지 않았는데도 경고가 표시됩니다. 원인은 객체 생성이 끝나는 시점에 Namenull이 아니라고 컴파일러가 확인할 수 없기 때문입니다.

CS8618을 해결할 때는 경고를 없애는 문법부터 선택하면 안 됩니다. 해당 값이 객체 생성 시 반드시 필요한 값인지, 실제로 없을 수 있는 값인지, 프레임워크가 나중에 설정하는 값인지 먼저 구분해야 합니다.

적용 범위

이 글은 다음 조건을 기준으로 설명합니다.

  • nullable 참조 형식이 활성화된 C# 프로젝트
  • nullable 참조 형식을 지원하는 C# 8 이상
  • required를 사용하는 예제는 C# 11 이상
  • 특정 프레임워크에 종속되지 않은 일반 클래스

프로젝트의 nullable 설정은 .csproj에서 확인할 수 있습니다.

<PropertyGroup> <Nullable>enable</Nullable> </PropertyGroup>

CS8618 경고가 발생하는 코드

오류 메시지

프로퍼티 이름에 따라 세부 문구는 달라질 수 있지만, 일반적으로 다음과 같은 경고가 표시됩니다.

CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

최소 재현 코드

#nullable enable public class User { public string Name { get; set; } }

코드에 생성자를 작성하지 않으면 컴파일러가 매개변수 없는 기본 생성자를 제공합니다.

따라서 다음 코드는 정상적으로 작성할 수 있습니다.

var user = new User();

하지만 객체를 생성한 직후 user.Name의 실제 초기값은 null입니다.

Console.WriteLine(user.Name.Length);

컴파일러가 경고하지 않았다면 위 코드는 런타임에서 NullReferenceException을 발생시킬 수 있습니다.

CS8618 경고가 발생하는 원리

string은 null이 아니어야 한다는 계약입니다

nullable 참조 형식이 활성화된 코드에서 stringstring?은 서로 다른 의도를 표현합니다.

string requiredName; string? optionalName;

string은 정상적인 코드 흐름에서 값이 null이 아니어야 한다는 뜻입니다.

string?는 값이 없을 수 있으며, 사용하기 전에 null 상태를 확인해야 한다는 뜻입니다.

두 선언 모두 런타임에서는 System.String을 사용합니다. 차이는 컴파일러가 수행하는 정적 분석에 있습니다.

참조 형식의 기본값은 null입니다

클래스의 참조 형식 필드와 자동 구현 프로퍼티는 별도로 초기화하지 않으면 null로 시작합니다.

public class User { public string Name { get; set; } }

위 코드는 타입 선언으로는 Name이 null이 아니라고 표현합니다. 하지만 실제 객체 생성 직후에는 Namenull일 수 있습니다.

선언된 계약과 객체의 초기 상태가 일치하지 않습니다.

컴파일러는 생성자 종료 시점을 검사합니다

컴파일러는 각 생성자가 끝날 때 non-nullable 멤버가 초기화됐는지 분석합니다.

다음 위치에서 값을 설정하면 컴파일러가 초기화를 확인할 수 있습니다.

  • 필드 또는 프로퍼티 선언부
  • 모든 인스턴스 생성자
  • 컴파일러가 초기화 계약을 이해할 수 있는 메서드

생성자가 끝날 때까지 non-null 값을 확인할 수 없으면 CS8618 경고를 표시합니다.

해결 방법 1: 생성자에서 필수값 받기

객체가 해당 값 없이 존재할 수 없다면 생성자에서 값을 받는 방식이 기본 선택입니다.

public class User { public User(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } public string Name { get; } }

객체를 생성하려면 반드시 이름을 전달해야 합니다.

var user = new User("Kim");

생성자 방식이 적합한 경우

다음 조건에 해당하면 생성자 초기화를 우선합니다.

  • 해당 값이 객체의 필수 상태입니다.
  • 값 없이 생성된 객체는 정상적으로 사용할 수 없습니다.
  • 객체 생성 시 유효성 검사가 필요합니다.
  • 생성 이후 값을 변경할 필요가 없습니다.

필요하다면 생성자에서 빈 문자열이나 공백도 검사할 수 있습니다.

public class User { public User(string name) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException( "이름은 비어 있을 수 없습니다.", nameof(name)); } Name = name; } public string Name { get; } }

생성자는 값의 존재뿐 아니라 도메인 규칙도 검사할 수 있습니다.

해결 방법 2: required로 초기화를 요구하기

객체 초기화 구문을 유지하면서 필수 프로퍼티를 지정하려면 C# 11 이상의 required를 사용할 수 있습니다.

public class User { public required string Name { get; init; } }

객체를 생성할 때 Name을 지정해야 합니다.

var user = new User { Name = "Kim" };

다음 코드는 Name을 설정하지 않았으므로 컴파일 오류가 발생합니다.

var user = new User();

required가 적합한 경우

required는 다음과 같은 데이터 중심 객체에서 검토할 수 있습니다.

  • 프로퍼티 이름을 보면서 객체를 초기화하고 싶습니다.
  • 필수 프로퍼티가 여러 개 있습니다.
  • 객체 초기화 구문을 유지해야 합니다.
  • 프로퍼티를 생성 이후 변경하지 않도록 init을 사용합니다.

다만 required는 멤버를 설정했는지만 검사합니다. 문자열이 공백인지, 숫자가 허용 범위에 있는지 같은 도메인 규칙은 별도로 검사해야 합니다.

해결 방법 3: 값이 선택 사항이면 nullable로 선언하기

값이 실제로 없을 수 있다면 ?를 붙이는 것이 올바른 모델링입니다.

public class UserProfile { public string? Introduction { get; set; } }

사용자가 자기소개를 작성하지 않은 상태도 정상이라면 Introduction은 nullable로 선언해야 합니다.

nullable 프로퍼티를 사용할 때는 null 상태를 확인합니다.

var profile = new UserProfile(); if (profile.Introduction is not null) { Console.WriteLine(profile.Introduction.Length); }

기본 표시값을 사용할 수도 있습니다.

string introduction = profile.Introduction ?? "소개가 없습니다.";

nullable 선언을 선택하는 기준

다음 질문에 답하면 선택하기 쉽습니다.

이 값이 없는 상태도 정상적인 객체 상태인가?

값이 없어도 정상이라면 T?로 선언합니다.

값이 반드시 있어야 한다면 ?로 경고를 이동시키지 말고 생성자나 required를 사용합니다.

해결 방법 4: 유효한 기본값으로 초기화하기

값에 정상적인 기본 상태가 있다면 선언과 동시에 초기화할 수 있습니다.

public class SearchCondition { public string Keyword { get; set; } = string.Empty; }

검색어가 비어 있으면 전체 결과를 조회하는 기능이라면 빈 문자열은 유효한 초기 상태일 수 있습니다.

컬렉션도 빈 컬렉션이 정상 상태라면 선언부에서 초기화할 수 있습니다.

public class Order { public List<string> ProductCodes { get; } = []; }

빈 문자열을 무조건 사용하면 안 되는 이유

다음 코드는 경고를 없애지만 객체의 의미를 흐릴 수 있습니다.

public class User { public string Name { get; set; } = string.Empty; }

사용자 이름이 필수라면 빈 문자열은 정상적인 이름이 아닙니다.

이 경우 null이라는 잘못된 상태를 빈 문자열이라는 다른 잘못된 상태로 바꾼 것뿐입니다. 유효한 기본값이 없다면 생성 시 값을 요구하는 편이 맞습니다.

해결 방법 5: 프레임워크 초기화에 null! 사용하기

일부 객체는 생성자가 아니라 외부 프레임워크가 프로퍼티를 설정합니다.

이런 구조에서는 다음과 같이 null!을 사용하는 코드를 볼 수 있습니다.

public class FrameworkManagedObject { public string Context { get; set; } = null!; }

후위 !는 null-forgiving 연산자입니다.

null!

이 연산자는 런타임 값을 변경하지 않습니다. 실제 초기값은 여전히 null입니다.

!는 컴파일러에 해당 식을 non-null로 취급하라고 알리고 nullable 경고를 억제합니다.

null!을 사용할 수 있는 조건

다음 조건을 모두 확인할 수 있을 때 제한적으로 사용합니다.

  • 외부 프레임워크가 값을 설정합니다.
  • 값이 설정된다는 계약이 문서에 명시되어 있습니다.
  • 실제 사용 전에 값이 들어간다는 보장이 있습니다.
  • 생성자나 required로 구조를 변경하기 어렵습니다.
  • 값이 설정되지 않는 실패 경로를 테스트할 수 있습니다.

일반적인 도메인 모델에서 경고를 없애기 위한 목적으로 사용해서는 안 됩니다.

public class User { public string Name { get; set; } = null!; }

위 코드는 경고만 사라집니다. Name을 설정하지 않고 사용하면 런타임 예외가 발생할 수 있습니다.

var user = new User(); Console.WriteLine(user.Name.Length);

null!은 값을 초기화하는 문법이 아니라 컴파일러 경고를 억제하는 문법입니다.

도우미 메서드에서 초기화할 때 MemberNotNull 사용하기

생성자가 도우미 메서드를 호출해 필드를 초기화하면 컴파일러가 초기화 사실을 충분히 추론하지 못할 수 있습니다.

public class Identifier { private string _value; public Identifier() { Initialize(); } private void Initialize() { _value = Guid.NewGuid().ToString(); } public string Value => _value; }

이 경우 MemberNotNull로 메서드의 초기화 계약을 알릴 수 있습니다.

using System.Diagnostics.CodeAnalysis; public class Identifier { private string _value; public Identifier() { Initialize(); } [MemberNotNull(nameof(_value))] private void Initialize() { _value = Guid.NewGuid().ToString(); } public string Value => _value; }

MemberNotNull은 해당 메서드가 정상적으로 반환되면 지정한 멤버가 null이 아니라고 컴파일러에 알립니다.

특성 자체가 값을 초기화하는 것은 아닙니다. 메서드의 모든 정상 반환 경로에서 실제로 non-null 값을 대입해야 합니다.

해결 방법 비교

방법적합한 상황실제 초기 상태주의사항
생성자객체 생성에 반드시 필요한 값non-null생성자 매개변수가 많아질 수 있음
required객체 초기화 구문으로 필수값 지정호출자가 설정C# 11 이상, 값 검증은 별도
nullable ?값이 없는 상태도 정상null 가능사용할 때 null 검사 필요
선언부 초기화유효한 기본값이 존재함기본값임시 센티널 값을 넣지 않음
null!프레임워크가 생성 후 값을 보장함실제로는 null런타임 안전성을 제공하지 않음
MemberNotNull도우미 메서드가 멤버 초기화구현에 따라 결정특성과 구현이 일치해야 함

어떤 방법을 선택해야 하는가

값이 객체의 필수 조건이라면 생성자에서 받습니다.

public User(string name)

객체 초기화 구문을 유지해야 한다면 C# 11 이상의 required를 검토합니다.

public required string Name { get; init; }

값이 실제로 없어도 정상이라면 nullable로 선언합니다.

public string? Introduction { get; set; }

정상적인 기본값이 있다면 선언과 동시에 초기화합니다.

public List<string> Tags { get; } = [];

외부 프레임워크가 값을 설정하고 그 계약을 확인할 수 있을 때만 null!을 제한적으로 사용합니다.

경고만 없애는 수정이 위험한 이유

다음 세 코드는 모두 서로 다른 의미를 가집니다.

public string Name { get; set; } = string.Empty;

Name의 정상적인 초기값이 빈 문자열이라고 선언합니다.

public string? Name { get; set; }

Name이 없는 상태도 정상이라고 선언합니다.

public string Name { get; set; } = null!;

실제 값은 null이지만 컴파일러 경고를 억제합니다.

세 방법은 서로 바꿔 사용할 수 없습니다. 경고가 사라졌는지가 아니라 클래스가 표현하는 상태가 실제 요구사항과 일치하는지 확인해야 합니다.

생성자에서 값을 설정했는데도 경고가 발생하는 경우

모든 생성자에서 값을 설정했는지 확인합니다.

public class User { public User(string name) { Name = name; } public User() { // Name을 설정하지 않았습니다. } public string Name { get; set; } }

첫 번째 생성자는 Name을 초기화하지만 두 번째 생성자는 초기화하지 않습니다.

모든 생성자에서 값을 설정하거나 생성자 체인을 사용합니다.

public class User { public User() : this("Guest") { } public User(string name) { Name = name; } public string Name { get; } }

"Guest"가 실제로 유효한 기본값일 때만 이 방식을 사용해야 합니다.

nullable 기능을 끄면 해결되는가

다음 설정으로 nullable 경고를 비활성화할 수 있습니다.

<PropertyGroup> <Nullable>disable</Nullable> </PropertyGroup>

이 설정은 프로퍼티를 초기화하지 않습니다. 컴파일러가 nullable 관련 경고를 표시하지 않게 만들 뿐입니다.

기존 프로젝트를 단계적으로 마이그레이션하는 경우가 아니라면 하나의 CS8618 경고를 없애기 위해 프로젝트 전체의 nullable 분석을 끄지 않는 편이 낫습니다.

동작 확인 방법

다음 명령으로 최소 프로젝트를 생성합니다.

dotnet new console -n Cs8618Example cd Cs8618Example

Program.cs에 다음 코드를 작성합니다.

public class User { public string Name { get; set; } }

프로젝트를 빌드합니다.

dotnet build

nullable 참조 형식이 활성화된 환경이라면 Name에 대한 CS8618 경고가 표시되는지 확인합니다.

생성자에서 값을 초기화한 뒤 다시 빌드합니다.

public class User { public User(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } public string Name { get; } }
dotnet build

해당 멤버의 CS8618 경고가 사라졌는지 확인합니다.

이 글은 특정 프로젝트의 실행 결과가 아닌 일반적인 C# 언어 규칙을 기준으로 작성했습니다. 발행 전 실제 사용하는 .NET SDK와 C# 버전에서 예제 코드를 빌드해 확인해야 합니다.

정리

CS8618은 생성자가 끝나는 시점에 non-nullable 멤버가 null이 아니라고 컴파일러가 증명할 수 없을 때 발생합니다.

값이 객체 생성에 반드시 필요하면 생성자에서 받습니다. 객체 초기화 구문이 필요하면 required를 검토합니다. 값이 없어도 정상이라면 nullable로 선언합니다. 유효한 기본값이 있을 때만 선언부에서 초기화합니다.

null!은 실제 값을 초기화하지 않습니다. 외부 프레임워크가 후속 초기화를 보장하는 경우가 아니라면 일반적인 해결책으로 사용하지 않는 편이 안전합니다.