nullable 참조 형식 사용

C# 8에서는 NRT(nullable Reference Types)라는 새로운 기능을 도입하여 참조 형식에 주석을 추가할 수 있도록 하여 null을 포함하는 것이 유효한지 여부를 나타냅니다. 이 기능을 접하는 경우 C# 문서를 읽어 익숙해지는 것이 좋습니다. Nullable 참조 형식은 새 프로젝트 템플릿에서 기본적으로 사용하도록 설정되지만 명시적으로 옵트인하지 않는 한 기존 프로젝트에서는 사용하지 않도록 유지됩니다.

이 페이지에서는 nullable 참조 형식에 대한 EF Core의 지원을 소개하고 이를 사용하기 위한 모범 사례를 설명합니다.

필수 및 선택적 속성

필수 및 선택적 속성 및 nullable 참조 형식과의 상호 작용에 대한 기본 설명서는 필수 및 선택적 속성 페이지입니다. 먼저 해당 페이지를 읽어 시작하는 것이 좋습니다.

참고 항목

기존 프로젝트에서 nullable 참조 형식을 사용하도록 설정할 때 주의해야 합니다. 이전에 선택 사항으로 구성되었던 참조 형식 속성은 이제 null 허용으로 명시적으로 주석을 추가하지 않는 한 필요에 따라 구성됩니다. 관계형 데이터베이스 스키마를 관리할 때 마이그레이션이 생성되어 데이터베이스 열의 Null 허용 여부를 변경할 수 있습니다.

nullable이 아닌 속성 및 초기화

nullable 참조 형식을 사용하도록 설정하면 C# 컴파일러는 null을 포함하므로 초기화되지 않은 nullable이 아닌 속성에 대한 경고를 내보낸다. 따라서 다음과 같은 일반적인 엔터티 형식 작성 방법을 사용할 수 없습니다.

public class Customer
{
    public int Id { get; set; }

    // Generates CS8618, uninitialized non-nullable property:
    public string Name { get; set; }
}

C# 11 이상을 사용하는 경우 필수 멤버는 이 문제에 대한 완벽한 솔루션을 제공합니다.

public required string Name { get; set; }

이제 컴파일러는 코드가 Customer를 인스턴스화할 때 항상 Name 속성을 초기화하도록 보장합니다. 또한 속성에 매핑된 데이터베이스 열은 null을 허용하지 않으므로 EF에서 로드하는 모든 인스턴스에는 항상 null이 아닌 이름도 포함됩니다.

이전 버전의 C#을 사용하는 경우 생성자 바인딩은 nullable이 아닌 속성이 초기화되도록 하는 대체 기술입니다.

public class CustomerWithConstructorBinding
{
    public int Id { get; set; }
    public string Name { get; set; }

    public CustomerWithConstructorBinding(string name)
    {
        Name = name;
    }
}

아쉽게도 일부 시나리오에서는 생성자 바인딩이 옵션이 아닙니다. 예를 들어 탐색 속성은 이러한 방식으로 초기화할 수 없습니다. 이러한 경우 null 용서 연산자의 도움을 받아 속성을 null로 초기화할 수 있습니다(자세한 내용은 아래 참조).

public Product Product { get; set; } = null!;

탐색 속성 추가

필수 탐색 속성은 추가적인 난이도를 제공합니다. 지정된 보안 주체에 대한 종속성이 항상 존재하지만 프로그램의 해당 시점에 필요한 사항에 따라 특정 쿼리에 의해 로드되거나 로드되지 않을 수 있습니다(데이터 로드를 위한 다양한 패턴 참조). 이와 동시에, 그렇게 하면 탐색이 로드되는 것으로 알려져 있어 null일 수 없는 경우에도 모든 액세스 권한이 null을 확인하도록 강제하기 때문에 이러한 속성을 null 허용으로 만드는 것이 바람직하지 않을 수 있습니다.

반드시 문제가 되는 것은 아닙니다. 필수 종속 항목이 제대로 로드되는 한(예: Include를 통해) 탐색 속성에 액세스하면 항상 null이 아닌 값이 반환됩니다. 반면에 애플리케이션은 탐색이 null인지 여부를 확인하여 관계가 로드되는지 여부를 확인할 수 있습니다. 이러한 경우 탐색을 null 허용으로 만드는 것이 합리적입니다. 즉, 보안 주체에 종속된 탐색이 필요합니다.

  • 프로그래머 오류가 로드되지 않은 경우 탐색에 액세스하는 것으로 간주되는 경우 null을 허용하지 않아야 합니다.
  • 애플리케이션 코드가 탐색을 확인하여 관계가 로드되는지 여부를 확인하는 것이 허용되는 경우 null을 허용해야 합니다.

더 엄격한 접근 방식을 원하는 경우 nullable 지원 필드가 있는 nullable이 아닌 속성을 사용할 수 있습니다.

private Address? _shippingAddress;

public Address ShippingAddress
{
    set => _shippingAddress = value;
    get => _shippingAddress
           ?? throw new InvalidOperationException("Uninitialized property: " + nameof(ShippingAddress));
}

탐색이 제대로 로드되면 속성을 통해 종속에 액세스할 수 있습니다. 그러나 먼저 관련 엔터티를 제대로 로드하지 않고 속성에 액세스하는 경우 API 계약이 잘못 사용되었기 때문에 InvalidOperationException이 throw됩니다.

참고 항목

여러 관련 엔터티에 대한 참조를 포함하는 컬렉션 탐색은 항상 null을 허용하지 않아야 합니다. 빈 컬렉션은 관련 엔터티가 없음을 의미하지만 목록 자체는 null이 아니어야 합니다.

DbContext 및 DbSet

EF를 사용하면 컨텍스트 형식에 초기화되지 않은 DbSet 속성을 사용하는 것이 일반적입니다.

public class MyContext : DbContext
{
    public DbSet<Customer> Customers { get; set;}
}

일반적으로 컴파일러 경고가 발생하지만 EF Core 7.0 이상에서는 이 경고를 표시하지 않습니다. EF는 리플렉션을 통해 이러한 속성을 자동으로 초기화하기 때문에 이 경고는 표시되지 않습니다.

이전 버전의 EF Core에서는 다음과 같이 이 문제를 해결할 수 있습니다.

public class MyContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
}

또 다른 전략은 null을 허용하지 않는 자동 속성을 사용하지만 null-forgiving 연산자(!)를 사용하여 컴파일러 경고를 무음으로 설정하여 null로 초기화하는 것입니다. DbContext 기본 생성자는 모든 DbSet 속성이 초기화되고 null이 관찰되지 않도록 합니다.

선택적 관계를 처리할 때 실제 null 참조 예외가 불가능한 컴파일러 경고가 발생할 수 있습니다. LINQ 쿼리를 번역하고 실행할 때 EF Core는 선택적 관련 엔터티가 없는 경우 해당 엔터티에 대한 탐색이 throw하는 대신 무시되도록 보장합니다. 그러나 컴파일러는 이 EF Core 보장을 인식하지 못하고 LINQ 쿼리가 메모리에서 실행된 것처럼 LINQ to Objects 경고를 생성합니다. 따라서 null 용서 연산자(!)를 사용하여 컴파일러에 실제 null 값을 사용할 수 없음을 알려야 합니다.

var order = context.Orders
    .Where(o => o.OptionalInfo!.SomeProperty == "foo")
    .ToList();

선택적 탐색에서 여러 수준의 관계를 포함할 때도 비슷한 문제가 발생합니다.

var order = context.Orders
    .Include(o => o.OptionalInfo!)
    .ThenInclude(op => op.ExtraAdditionalInfo)
    .Single();

이 작업을 많이 수행하고 해당 엔터티 형식이 EF Core 쿼리에서 주로(또는 독점적으로) 사용되는 경우 탐색 속성을 null 허용이 아닌 상태로 만들고 Fluent API 또는 데이터 주석을 통해 선택 사항으로 구성하는 것이 좋습니다. 이렇게 하면 관계를 선택적으로 유지하면서 모든 컴파일러 경고가 제거됩니다. 그러나 엔터티가 EF Core 외부에서 트래버스되는 경우 속성에 null 허용이 아닌 주석이 추가되어도 null 값이 관찰될 수 있습니다.

이전 버전의 제한 사항

EF Core 6.0 이전에는 다음과 같은 제한 사항이 적용되었습니다.

  • 공용 API 표면은 null 허용 여부를 위해 주석이 지정되지 않았으므로(공용 API가 "null-oblivious"였음) NRT 기능이 켜져 있을 때 사용하기가 불편할 수 있습니다. 특히 EF Core에서 노출하는 비동기 LINQ 연산자(예: FirstOrDefaultAsync)가 포함됩니다. 공용 API는 EF Core 6.0부터 null 허용 여부를 위해 완전히 주석이 추가됩니다.
  • 리버스 엔지니어링은 C# 8 NRT(nullable 참조 형식)를 지원하지 않았습니다. EF Core는 항상 기능이 꺼져 있다고 가정하는 C# 코드를 생성했습니다. 예를 들어 nullable 텍스트 열은 속성 필요 여부를 구성하는 데 사용하는 Fluent API나 데이터 주석을 사용하여 string? 대신 string 형식으로 스캐폴드되었습니다. 이전 버전의 EF Core를 사용하는 경우에도 스캐폴드된 코드를 편집하고 C# Null 허용 여부 주석으로 바꿀 수 있습니다.