복합 형식

데이터베이스에 저장된 개체는 다음 세 가지 광범위한 범주로 분할할 수 있습니다.

  • 구조화되지 않고 단일 값을 보유하는 개체. 예: int, Guid, string, IPAddress. 이러한 형식은 (다소 느슨하게) 기본 형식이라고 합니다.
  • 여러 값을 보유하도록 구조화되고 개체의 ID가 키 값으로 정의되는 개체. 예: Blog, Post, Customer. 이를 엔터티 형식이라고합니다.
  • 여러 값을 보유하도록 구조화되었지만 개체에 해당 ID를 정의하는 키가 없는 개체입니다. 예: Address, Coordinate, Money. 이러한 개체를 값 개체라고 하며 EF Core는 이러한 개체를 복합 형식으로 매핑합니다.

복합 형식은 여러 속성을 엔터티 형식 내에 포함된 단일 .NET 형식으로 그룹화합니다. 이 형식에는 자체 ID가 없으며 독립적으로 추적하거나 쿼리할 수 없습니다. 이렇게 하면 복합 형식이 값 개체를 모델링하는 자연스러운 방법이 됩니다.

Tip

GitHub 이 문서의 전체 샘플 프로젝트를 실행하고 디버그할 수 있습니다.

메모

복합 형식은 EF Core 8에서 도입되었으며 이후 릴리스에서 크게 확장되었습니다. 기능이 도입된 버전과 함께 아래에 주석이 추가되었습니다.

복합 형식과 소유 엔터티 형식 비교

복합 형식이 존재하기 전에는 소유 엔터티 형식 을 키 속성 없이 개체를 모델링하는 것이 좋습니다. 그러나 소유된 형식은 여전히 백그라운드에서 엔터티 형식 입니다. 숨겨진 키와 ID가 있으므로 참조 의미 체계로 작동합니다. 이렇게 하면 복잡한 형식이 해결하도록 설계된 여러 마찰 지점이 발생합니다.

주요 차이점은 다음과 같습니다.

Aspect 소유된 엔터티 유형 복합 형식
아이덴티티 숨겨진 키 및 ID가 있어야 합니다. ID 없음; 값과 비교
인스턴스 공유 동일한 인스턴스를 두 번 참조할 수 없습니다. 동일한 인스턴스를 여러 속성에 할당할 수 있습니다.
할당 의미 체계 참조 의미 체계 값 의미 체계(속성이 복사됨)
.NET 형식 참조 형식만 참조 또는 값 형식
테이블 매핑 테이블 소유, 테이블 분할 또는 JSON 컨테이너의 테이블(테이블 분할) 또는 JSON
Navigations 다른 엔터티에 대한 탐색을 포함할 수 있습니다. 탐색을 포함할 수 없음
대량 업데이트(ExecuteUpdate) 지원되지 않음 지원됨

예를 들어 고객의 청구 주소를 배송 주소와 동일하게 할당하면 동일한 엔터티 인스턴스를 두 번 이상 참조할 수 없으므로 소유된 엔터티 형식으로 인해 실패합니다.

var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types

복합 형식에는 값 의미 체계가 있으므로 동일한 할당은 단순히 속성을 복사하고 예상대로 작동합니다. 마찬가지로 LINQ 쿼리에서 두 개의 복합 값을 비교하면 콘텐츠가 비교되는 반면, 두 개의 소유 엔터티를 비교하면 해당 ID가 비교됩니다.

이러한 이유로 복합 형식은 일반적으로 테이블 분할 또는 JSON 매핑을 사용하여 값 개체를 모델링하는 데 더 적합합니다. 현재 이러한 시나리오에 대해 소유 엔터티 형식을 사용하는 사용자는 복잡한 형식으로 전환하는 것이 좋습니다.

간단한 예

Address 여러 관련 값을 보유하지만 자체 ID가 없는 형식을 고려합니다.

public record Address
{
    public required string Line1 { get; init; }
    public string? Line2 { get; init; }
    public required string City { get; init; }
    public required string Country { get; init; }
    public required string PostCode { get; init; }
}

Address 는 고객/주문 모델에서 여러 위치에서 사용할 수 있습니다.

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A required (non-nullable) complex property.
    public required Address Address { get; set; }

    // An optional (nullable) complex property.
    public Address? SecondaryAddress { get; set; }

    public List<Order> Orders { get; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

고객 만들기 및 저장은 평소와 같이 작동합니다.

var customer = new Customer
{
    Name = "Willow",
    Address = new Address
    {
        Line1 = "Barking Gate",
        City = "Walpole St Peter",
        Country = "UK",
        PostCode = "PE14 7AV"
    }
};

context.Add(customer);
await context.SaveChangesAsync();

관계형 데이터베이스에서 복합 형식은 자체 테이블을 얻지 못합니다. 대신, 해당 속성은 포함하는 엔터티의 테이블에 추가 열로 인라인으로 저장됩니다( 테이블 분할이라고 함).

INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);

복합 형식에는 값 의미 체계가 있으므로 문제 없이 여러 속성에서 동일한 Address 인스턴스를 공유할 수 있습니다.

// The same Address instance can be assigned to multiple complex properties.
customer.Orders.Add(
    new Order
    {
        Contents = "Tesco Tasty Treats",
        BillingAddress = customer.Address,
        ShippingAddress = customer.Address
    });

await context.SaveChangesAsync();

복합 형식 구성

대부분의 엔터티 형식과 달리 복합 형식은 규칙에 의해 검색되지 않습니다. 형식에 주석을 추가하거나 복합 형식으로 ComplexTypeAttribute매핑되어야 하는 각 속성에 OnModelCreating 대해 Fluent API를 호출 ComplexProperty 하여 명시적으로 구성해야 합니다.

[ComplexType]
public record Address
{
    public required string Line1 { get; init; }
    public string? Line2 { get; init; }
    public required string City { get; init; }
    public required string Country { get; init; }
    public required string PostCode { get; init; }
}

복합 형식 속성의 패싯 구성

중첩된 Property 작성기를 사용하여 엔터티 형식의 속성과 마찬가지로 복합 형식의 스칼라 속성을 구성할 수 있습니다( 예: 열 이름 또는 최대 길이 설정).

modelBuilder.Entity<Order>()
    .ComplexProperty(
        o => o.ShippingAddress,
        b =>
        {
            b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100);
            b.Property(a => a.City).HasColumnName("ShipsToCity");
        });

EF Core 11부터 먼저 복합 형식 작성기를 가져오지 않고 람다에서 멤버 액세스를 연결하여 복합 형식 내에 중첩된 속성을 직접 구성할 수 있습니다.

// EF Core 11 allows configuring a complex-type property directly by chaining
// member access, without first obtaining the complex-type builder.
modelBuilder.Entity<Customer>()
    .Property(c => c.Address.Line1)
    .HasMaxLength(200);

참조 및 값 형식

복합 형식은 .NET 참조 형식(a class 또는) 또는 record값 형식(structrecord structEF Core 10에서 도입된 또는)일 수 있습니다.

가변성

참조 형식 인스턴스는 여러 속성에서 공유할 수 있으므로 해당 속성 중 하나를 변경하면 사용되는 모든 위치에서 값이 변경됩니다. 이것은 일반적으로 당신이 원하는 것이 아닙니다. 값을 변경하려면 새 인스턴스를 만들어야 하므로 복합 형식을 변경할 수 없도록 하는 것이 좋습니다. Address 이 문서 전체에서 사용되는 형식은 변경할 수 없습니다record. 따라서 주소를 변경하는 작업은 식으로 수행 with 됩니다.

// Address is an immutable record, so create a new instance to change a value.
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

완전히 새로운 Address 인스턴스가 할당되더라도 EF는 개별 속성 수준에서 변경 내용을 추적하므로 실제로 값이 변경된 열만 업데이트됩니다.

UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;

불변성(init-only 또는 read-only 속성), a, arecord, 또는 readonly record struct.을 사용하여 변경할 수 없음 classreadonly struct표현할 수 있습니다. 값 형식(struct)에는 복사 의미 체계가 있으므로 값을 할당하면 변경 가능한 경우에도 항상 값을 복사하고 실수로 인한 공유 문제를 방지할 수 있지만 변경 가능한 구조체는 일반적으로 C#에서 권장되지 않으므로 변경할 수 없는 형식을 사용하는 것이 좋습니다.

Tip

여러 엔터티가 실제로 동일한 주소를 관찰하고 변경 시 함께 업데이트해야 하는 경우 주소를 고유한 ID가 있는 엔터티 형식 으로 모델링하고 복잡한 형식을 사용하는 대신 탐색을 통해 참조합니다.

중첩된 복합 형식

복합 형식은 다른 복합 형식의 속성을 포함할 수 있으므로 구조화된 개체를 깊이로 빌드할 수 있습니다. 예를 들어 복합 형식에는 Contact 하나 이상의 PhoneNumber 복합 형식이 포함될 Address 수 있습니다.

public record Address(string Line1, string? Line2, string City, string Country, string PostCode);

public record PhoneNumber(int CountryCode, long Number);

public record Contact
{
    public required Address Address { get; init; }
    public required PhoneNumber HomePhone { get; init; }
    public required PhoneNumber WorkPhone { get; init; }
}

테이블 분할을 통해 매핑되는 경우 중첩된 복합 형식의 열에는 속성의 전체 경로(예 Contact_HomePhone_Number: )가 접두사로 추가됩니다.

선택적 복합 형식

기본적으로 복합 속성이 필요합니다. CLR 속성에는 항상 값이 있어야 하며 null을 허용하지 않는 열에 매핑됩니다. EF Core 10부터는 nullable로 선언하여 복합 속성을 선택적으로 만들 수 있습니다.

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A required (non-nullable) complex property.
    public required Address Address { get; set; }

    // An optional (nullable) complex property.
    public Address? SecondaryAddress { get; set; }

    public List<Order> Orders { get; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

모든 열의 값이 생성 NULL 되는 선택적 복합 속성입니다null.

메모

선택적 복합 형식은 현재 복합 형식에 하나 이상의 필수 속성을 정의해야 합니다. EF는 복합 값을 모든 속성이 있는 복합 값과 구분 null 하기 위해 Null을 허용하지 않는 열이 하나 이상 필요하기 때문입니다 null.

복합 형식에 고유한 필수 속성이 없는 경우 대신 판별자 속성을 구성할 수 있습니다. EF Core는 복합 형식에 대한 상속을 아직 지원하지 않지만 판별자를 기본적으로 필수 섀도 속성 으로 만들어 위의 요구 사항을 충족합니다.

// GeoLocation has no required property, so configure a discriminator. EF creates
// it as a required shadow property, which satisfies the requirement that an
// optional complex type have at least one required property.
modelBuilder.Entity<Place>()
    .ComplexProperty(p => p.Location, b => b.HasDiscriminator());

복합 형식의 컬렉션

EF Core 10부터 속성은 복합 형식 컬렉션을 보유 할 수 있습니다. 관계형 데이터베이스에서는 복합 컬렉션을 사용하여 ToJson 단일 JSON 열에 매핑해야 합니다. 다른 테이블에 매핑할 수 없습니다.

public class Distributor
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A collection of complex types, mapped to a single JSON column.
    public List<Address> ShippingCenters { get; set; } = new();
}
// Collections of complex types must be mapped to JSON on relational providers.
modelBuilder.Entity<Distributor>()
    .ComplexCollection(d => d.ShippingCenters, b => b.ToJson());

컬렉션의 각 요소는 배열 내에 JSON 개체로 저장되고 전체 컬렉션은 하나의 열에 매핑됩니다.

CREATE TABLE [Distributors] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [ShippingCenters] json NOT NULL,
    CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);

메모

값 형식(struct)의 컬렉션은 현재 지원되지 않습니다. 복합 컬렉션 요소에 참조 형식(class 또는 record)을 사용합니다.

복합 형식을 JSON에 매핑

테이블 분할 외에도 EF Core 10을 사용하면 컬렉션이 아닌 복합 속성을 다음을 사용하여 단일 JSON 열 ToJson에 매핑할 수 있습니다.

modelBuilder.Entity<Customer>(b =>
{
    b.ComplexProperty(c => c.Address, c => c.ToJson());
    b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});

그런 다음 각 복합 값이 여러 열에 분산되지 않고 단일 JSON 열로 직렬화됩니다.

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [Address] json NOT NULL,
    [SecondaryAddress] json NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);

SQL Server 2025 및 Azure SQL EF는 기본적으로 네이티브 json 데이터 형식을 사용합니다. 다른 데이터베이스 및 이전 SQL Server 버전에서는 JSON이 텍스트 열에 저장됩니다. 필요한 경우 열 형식 HasColumnType 을 재정의할 수 있습니다.

테이블 분할과 달리 JSON 매핑은 매핑된 형식 의 컬렉션을 허용하며 다른 속성과 마찬가지로 문서 내의 개별 속성을 쿼리하고 업데이트할 수 있습니다. JSON 열 내의 값은 .를 사용하여 대량 ExecuteUpdateAsync으로 효율적으로 업데이트할 수도 있습니다.

복합 형식 속성의 키 및 인덱스

EF Core 11부터 키 및 인덱스는 컬렉션이 아닌 복합 형식 내에 중첩된 스칼라 속성을 대상으로 지정할 수 있습니다. 이 작업은 람다로 수행할 수 있습니다.

// EF Core 11 allows keys and indexes to target scalar properties nested
// inside non-collection complex types.
modelBuilder.Entity<Customer>()
    .HasIndex(c => c.Address.PostCode);

동일한 경로를 이름으로 . 구성하여 복합 속성으로 이동할 수 있습니다.

modelBuilder.Entity<Customer>()
    .HasIndex("Address.PostCode");

관계형 공급자의 경우 인덱스는 JSON 열에 매핑된 복합 형식 내의 경로를 대상으로 지정할 수도 있습니다. 복합 컬렉션 경로는 모든 요소를 참조하거나 특정 요소에 대한 숫자 인덱서를 참조하는 데 사용합니다 [] .

// Index a scalar inside every element of a JSON-mapped complex collection.
// Requires a provider/database with JSON index support, such as SQL Server 2025.
modelBuilder.Entity<Distributor>()
    .HasIndex("ShippingCenters[].City");

메모

JSON 매핑 복합 컬렉션으로 인덱싱하려면 SQL Server 2025와 같은 JSON 인덱스를 지원하는 데이터베이스가 필요합니다.

자세한 내용은 인덱스 및 제약 조건을 참조하세요.

엔터티 상속이 있는 복합 형식

EF Core 11부터 TPT(형식별 테이블) 또는 TPC(테이블별 구체적인 형식)를 사용하는 엔터티 형식에서 복합 형식 및 JSON 열을 사용할 수 있습니다. 이렇게 하면 이러한 상속 전략의 유연성과 복합 형식의 모델링 기능을 결합할 수 있습니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Animal>()
        .UseTptMappingStrategy()
        .ComplexProperty(a => a.Details);
}

변경 사항 추적

EF Core는 복합 형식의 개별 속성에 대한 변경 내용을 추적하므로 호출 SaveChanges할 때 영향을 받는 열만 업데이트됩니다. 변경 추적기를 통해 이 추적 상태를 검사하고 조작할 수 있습니다.

복합 속성에 도달한 다음 스칼라 속성을 드릴인하는 데 사용합니다 EntityEntry.ComplexProperty .

var addressEntry = context.Entry(customer).ComplexProperty(c => c.Address);
Console.WriteLine($"City is currently: {addressEntry.Property(a => a.City).CurrentValue}");
Console.WriteLine($"Address was modified: {addressEntry.Property(a => a.City).IsModified}");

API는 ComplexPropertyEntry 엔터티 EntityEntry API를 미러링합니다. 읽기 및 설정 CurrentValue, 확인 및 설정하고 IsModified중첩된 복합 속성 또는 복합 컬렉션으로 이동할 수 있습니다. 복합 속성은 엔터티의 속성 값 API()를CurrentValues/OriginalValues 통해서도 노출됩니다.

복합 형식 쿼리

복잡한 형식 멤버는 엔터티 자체의 속성과 마찬가지로 LINQ 쿼리에서 사용할 수 있습니다. 이를 필터링하고, 프로젝션하고, 순서를 지정할 수 있습니다.

// Filter and project members of a complex property.
var ukCities = await context.Customers
    .Where(c => c.Address.Country == "UK")
    .Select(c => c.Address.City)
    .ToListAsync();

복합 형식에는 값 의미 체계가 있으므로 쿼리의 전체 복합 값을 비교할 수도 있으며 EF는 모든 속성을 비교합니다.

var ordersToHomeAddress = await context.Orders
    .Where(o => o.ShippingAddress == o.BillingAddress)
    .ToListAsync();

JSON에 매핑된 복합 형식의 경우 EF Core 11은 문서에 지정된 JSON 경로가 있는지 여부를 확인하는 추가 EF.Functions.JsonPathExists합니다.

var withPostCode = await context.Customers
    .Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
    .ToListAsync();

제한점

복합 형식은 값 개체 모델링을 위해 설계되었으며 의도적으로 엔터티 형식의 모든 기능을 지원하지 않습니다. 주요 제한 사항은 다음과 같습니다.

  • 자신의 ID 또는 추적이 없습니다. 복합 형식은 엔터티의 일부로만 존재할 수 있습니다. 복합 형식을 DbSet<T> 가질 수 없으며 독립적으로 추적하거나 쿼리할 수 없습니다.
  • 탐색이 없습니다. 복합 형식은 엔터티 형식에 대한 탐색 속성을 포함할 수 없습니다.
  • 별도의 테이블이 없습니다. 관계형 데이터베이스에서 복합 형식은 항상 컨테이너의 테이블(테이블 분할을 통해) 또는 JSON 열에 저장되며, 자체 테이블에는 저장되지 않습니다.
  • 컬렉션에는 JSON이 필요합니다. 관계형 공급자에서는 복합 컬렉션을 JSON에 매핑해야 하며 ToJson테이블 분할을 통해 매핑할 수 없습니다.
  • 값 형식의 컬렉션은 지원되지 않습니다. 복합 컬렉션 요소는 참조 형식이어야 합니다.
  • 선택적 복합 형식에는 필수 속성이 필요합니다. 선택적(nullable) 복합 형식은 하나 이상의 필수 속성을 정의해야 합니다.

복잡한 형식 지원은 릴리스에서 계속 확대되고 있습니다. 최신 추가에 대한 새로운 페이지를 참조하세요.