다음을 통해 공유


변경 내용 검색 및 알림

DbContext 인스턴스는 엔터티 변경 내용을 추적합니다. 이렇게 추적되는 엔터티는 차례로 SaveChanges가 호출될 때 변경 내용을 데이터베이스에 적용합니다. 이 내용은 EF Core의 변경 내용 추적을 다루며, 이 문서에서는 엔터티 상태와 EF Core(Entity Framework Core) 변경 내용 추적의 기본 사항을 이해한다고 가정합니다.

속성 및 관계 변경 내용을 추적하려면 DbContext에서 이러한 변경 내용을 검색할 수 있어야 합니다. 이 문서에서는 이 검색이 발생하는 방법과 속성 알림 또는 변경 내용 추적 프록시를 사용하여 변경 내용을 즉시 검색하는 방법을 설명합니다.

GitHub에서 샘플 코드를 다운로드하여 이 문서의 모든 코드를 실행하고 디버그할 수 있습니다.

스냅샷 변경 내용 추적

기본적으로 EF Core는 DbContext 인스턴스에서 처음 추적할 때 모든 엔터티의 속성 값에 대한 스냅샷을 만듭니다. 이 스냅샷에 저장된 값은 변경된 속성 값을 결정하기 위해 엔터티의 현재 값과 비교됩니다.

이 변경 내용 검색은 데이터베이스에 업데이트를 보내기 전에 변경된 모든 값이 검색되도록 SaveChanges를 호출할 때 발생합니다. 그러나 변경 내용 검색은 애플리케이션이 최신 추적 정보로 작동하는지 확인하기 위해 다른 시간에도 발생합니다. 언제든지 ChangeTracker.DetectChanges()를 호출하여 변경 내용 검색을 강제할 수 있습니다.

변경 검색이 필요한 경우

이 변경을 수행하기 위해 EF Core를 사용하지 않고 속성 또는 탐색이 변경된 경우 변경 내용 검색이 필요합니다. 예를 들어 블로그 및 게시물을 로드한 후 다음 엔터티를 변경하는 것이 좋습니다.

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

ChangeTracker.DetectChanges()를 호출하기 전에 변경 추적기 디버그 보기를 보면 변경 내용이 검색되지 않았으므로 엔터티 상태 및 수정된 속성 데이터에 반영되지 않았다는 것이 나타납니다.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, <not found>]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

특히 블로그 항목의 상태는 여전히 Unchanged이며 새 게시물은 추적된 엔터티로 표시되지 않습니다. (EF Core에서 이러한 변경 내용을 아직 검색하지 않았더라도 속성이 새 값을 보고하는 것을 알 수 있습니다. 디버그 보기가 엔터티 인스턴스에서 직접 현재 값을 읽고 있기 때문입니다.)

DetectChanges를 호출한 후 디버그 보기와 대조합니다.

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

이제 블로그가 Modified로 올바르게 표시되고 새 게시물이 검색되어 Added로 추적됩니다.

이 섹션의 시작 부분에서는 EF Core를 사용하여 변경을 수행하지 않을 때 변경 내용을 검색해야 한다고 명시했습니다. 이는 위의 코드에서 발생하는 일입니다. 즉, 속성 및 탐색에 대한 변경 내용은 EF Core 메서드를 사용하지 않고 엔터티 인스턴스에서 직접 적용됩니다.

동일한 방식으로 엔터티를 수정하지만 이번에는 EF Core 메서드를 사용하는 다음 코드와 대조합니다.

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
context.Entry(blog).Property(e => e.Name).CurrentValue = ".NET Blog (Updated!)";

// Add a new entity to the DbContext
context.Add(
    new Post
    {
        Blog = blog,
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

이 경우 변경 내용 추적기 디버그 보기는 변경 내용 검색이 수행되지 않았더라도 모든 엔터티 상태 및 속성 수정이 알려져 있음을 보여 줍니다. 그 이유는 PropertyEntry.CurrentValue가 EF Core 메서드이기 때문입니다. 즉, EF Core는 이 메서드의 변경 내용을 즉시 알 수 있습니다. 마찬가지로 DbContext.Add를 호출하면 EF Core가 새 엔터티에 대해 즉시 알고 적절하게 추적할 수 있습니다.

항상 EF Core 메서드를 사용하여 엔터티를 변경하여 변경 내용을 검색하지 않도록 시도하지 마세요. 이렇게 하면 일반적으로 엔터티를 변경하는 것보다 더 번거롭고 성능이 떨어집니다. 이 문서의 의도는 변경 내용을 검색해야 하는 시기와 그렇지 않은 경우를 알리는 것입니다. 의도는 변경 감지의 회피를 장려하는 것이 아닙니다.

변경 내용을 자동으로 검색하는 메서드

DetectChanges()는 결과에 영향을 미칠 가능성이 있는 메서드에 의해 자동으로 호출됩니다. 이러한 메서드는 다음과 같습니다.

또한 추적된 엔터티의 전체 그래프가 아닌 단일 엔터티 인스턴스에서만 변경 내용 검색이 발생하는 경우도 있습니다. 이러한 위치는 다음과 같습니다.

  • 엔터티의 상태 및 수정된 속성이 최신 상태인지 확인하기 위해 DbContext.Entry를 사용하는 경우.
  • 속성 수정, 현재 값 등을 최신 상태로 유지하기 위해 Property, Collection, Reference 또는 Member와 같은 EntityEntry 메서드를 사용하는 경우.
  • 필수 관계가 끊어졌기 때문에 종속/자식 엔터티가 삭제되는 경우 이렇게 하면 엔터티가 다시 부모가 되었기 때문에 삭제하지 않아야 하는 시기를 감지합니다.

EntityEntry.DetectChanges()를 호출하여 단일 엔터티에 대한 변경 내용의 로컬 검색을 명시적으로 트리거할 수 있습니다.

참고 항목

로컬 검색 변경 내용이 전체 검색에서 찾을 수 있는 일부 변경 내용을 놓칠 수 있습니다. 이는 검색되지 않은 다른 엔터티 변경으로 인한 연속 작업이 해당 엔터티에 영향을 미칠 때 발생합니다. 이러한 상황에서 애플리케이션은 ChangeTracker.DetectChanges()를 명시적으로 호출하여 모든 엔터티를 강제로 검사해야 할 수 있습니다.

자동 변경 검색을 사용하지 않도록 설정

변경 내용을 검색하는 성능은 대부분의 애플리케이션에 병목 상태가 아닙니다. 그러나 변경 내용을 검색하면 수천 개의 엔터티를 추적하는 일부 애플리케이션의 성능 문제가 될 수 있습니다. (정확한 숫자는 엔터티의 속성 수와 같은 여러 항목에 따라 달라집니다.) 따라서 ChangeTracker.AutoDetectChangesEnabled를 사용하여 변경 내용의 자동 검색을 사용하지 않도록 설정할 수 있습니다. 예를 들어 페이로드와의 다대다 관계에서 조인 엔터티를 처리하는 것이 좋습니다.

public override int SaveChanges()
{
    foreach (var entityEntry in ChangeTracker.Entries<PostTag>()) // Detects changes automatically
    {
        if (entityEntry.State == EntityState.Added)
        {
            entityEntry.Entity.TaggedBy = "ajcvickers";
            entityEntry.Entity.TaggedOn = DateTime.Now;
        }
    }

    try
    {
        ChangeTracker.AutoDetectChangesEnabled = false;
        return base.SaveChanges(); // Avoid automatically detecting changes again here
    }
    finally
    {
        ChangeTracker.AutoDetectChangesEnabled = true;
    }
}

이전 섹션에서 알 수 있듯이 ChangeTracker.Entries<TEntity>()DbContext.SaveChanges 모두 변경 내용을 자동으로 검색합니다. 그러나 Entries를 호출한 후 코드는 엔터티 또는 속성 상태를 변경하지 않습니다. (추가된 엔터티에 일반 속성 값을 설정해도 상태가 변경되지 않습니다.) 따라서 이 코드는 기본 SaveChanges 메서드로 호출할 때 불필요한 자동 변경 검색을 사용하지 않도록 설정합니다. 또한 이 코드는 try/finally 블록을 사용하여 SaveChanges가 실패하더라도 기본 설정이 복원되도록 합니다.

코드가 잘 수행되도록 자동 변경 검색을 사용하지 않도록 설정해야 한다고 가정하지 마세요. 이는 많은 엔터티를 추적하는 애플리케이션을 프로파일링할 때 변경 검색 성능이 문제임을 나타내는 경우에만 필요합니다.

변경 내용 및 값 변환 검색

엔터티 형식으로 스냅샷 변경 내용 추적을 사용하려면 EF Core에서 다음을 수행할 수 있어야 합니다.

  • 엔터티를 추적할 때 각 속성 값의 스냅샷 만들기
  • 이 값을 속성의 현재 값과 비교
  • 값에 대한 해시 코드 생성

이 작업은 데이터베이스에 직접 매핑할 수 있는 형식에 대해 EF Core에서 자동으로 처리됩니다. 그러나 값 변환기를 사용하여 속성을 매핑하는 경우 해당 변환기는 이러한 작업을 수행하는 방법을 지정해야 합니다. 이는 값 비교자를 사용하여 수행되며 값 비교자 설명서에 자세히 설명되어 있습니다.

알림 엔터티

스냅샷 변경 내용 추적은 대부분의 애플리케이션에 권장됩니다. 그러나 많은 엔터티를 추적하거나 해당 엔터티를 많이 변경하는 애플리케이션은 속성 및 탐색 값이 변경되면 EF Core에 자동으로 알리는 엔터티를 구현하는 것이 도움이 될 수 있습니다. 이를 "알림 엔터티"라고 합니다.

알림 엔터티 구현

알림 엔터티는 .NET BCL(기본 클래스 라이브러리)의 일부인 INotifyPropertyChangingINotifyPropertyChanged 인터페이스를 사용합니다. 이러한 인터페이스는 속성 값을 변경하기 전과 후에 발생해야 하는 이벤트를 정의합니다. 예시:

public class Blog : INotifyPropertyChanging, INotifyPropertyChanged
{
    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private int _id;

    public int Id
    {
        get => _id;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Id)));
            _id = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
        }
    }

    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));
            _name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

또한 모든 컬렉션 탐색은 INotifyCollectionChanged를 구현해야 합니다. 위의 예제에서는 게시물의 ObservableCollection<T>를 사용하여 충족됩니다. 또한 EF Core는 안정적인 정렬을 희생하면서 보다 효율적인 조회를 포함하는 ObservableHashSet<T> 구현과 함께 제공합니다.

이 알림 코드의 대부분은 일반적으로 매핑되지 않은 기본 클래스로 이동됩니다. 예시:

public class Blog : NotifyingEntity
{
    private int _id;

    public int Id
    {
        get => _id;
        set => SetWithNotify(value, out _id);
    }

    private string _name;

    public string Name
    {
        get => _name;
        set => SetWithNotify(value, out _name);
    }

    public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public abstract class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged
{
    protected void SetWithNotify<T>(T value, out T field, [CallerMemberName] string propertyName = "")
    {
        NotifyChanging(propertyName);
        field = value;
        NotifyChanged(propertyName);
    }

    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    private void NotifyChanging(string propertyName)
        => PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}

알림 엔터티 구성

EF Core에서 INotifyPropertyChanging 또는 INotifyPropertyChanged가 EF Core와 함께 사용하기 위해 완전히 구현되어 있는지 확인할 수 있는 방법은 없습니다. 특히 이러한 인터페이스의 일부 사용은 EF Core에서 요구하는 대로 모든 속성(탐색 포함)이 아닌 특정 속성에 대해서만 알림을 사용합니다. 이러한 이유로 EF Core는 이러한 이벤트에 자동으로 연결되지 않습니다.

대신 이러한 알림 엔터티를 사용하도록 EF Core를 구성해야 합니다. 이 작업은 일반적으로 ModelBuilder.HasChangeTrackingStrategy를 호출하여 모든 엔터티 형식에 대해 수행됩니다. 예시:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
}

(EntityTypeBuilder.HasChangeTrackingStrategy를 사용하여 다른 엔터티 형식에 대해 전략을 다르게 설정할 수도 있지만 알림 엔터티가 아닌 형식에는 DetectChanges가 여전히 필요하기 때문에 일반적으로 비생산적입니다.)

전체 알림 변경 내용 추적을 사용하려면 INotifyPropertyChangingINotifyPropertyChanged 모두 구현되어야 합니다. 이렇게 하면 속성 값이 변경되기 직전에 원래 값을 저장할 수 있으므로 엔터티를 추적할 때 EF Core에서 스냅샷을 만들 필요가 없습니다. INotifyPropertyChanged만 구현하는 엔터티 형식은 EF Core에서도 사용할 수 있습니다. 이 경우 EF는 원래 값을 추적하기 위해 엔터티를 추적할 때 여전히 스냅샷을 만든 다음, DetectChanges를 호출할 필요 없이 알림을 사용하여 변경 내용을 즉시 검색합니다.

다른 ChangeTrackingStrategy 값은 다음 표에 요약되어 있습니다.

ChangeTrackingStrategy 필요한 인터페이스 DetectChanges 필요 스냅샷 원래 값
스냅샷 없음
ChangedNotifications INotifyPropertyChanged
ChangingAndChangedNotifications INotifyPropertyChanged 및 INotifyPropertyChanging 아니요 아니요
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged 및 INotifyPropertyChanging

알림 엔터티 사용

알림 엔터티는 엔터티 인스턴스를 변경해도 이러한 변경 내용을 검색하기 위해 ChangeTracker.DetectChanges()에 대한 호출이 필요하지 않다는 점을 제외하고 다른 엔터티처럼 동작합니다. 예시:

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

일반 엔터티를 사용하면 변경 추적기 디버그 보기에서 DetectChanges가 호출될 때까지 이러한 변경 내용이 검색되지 않은 것으로 나타났습니다. 알림 엔터티가 사용되는 디버그 보기를 보면 이러한 변경 내용이 즉시 검색되었음을 확인할 수 있습니다.

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
  Id: -2147482643 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

변경 내용 추적 프록시

EF Core는 INotifyPropertyChangingINotifyPropertyChanged를 구현하는 프록시 형식을 동적으로 생성할 수 있습니다. 이렇게 하려면 Microsoft.EntityFrameworkCore.Proxies NuGet 패키지를 설치하고 UseChangeTrackingProxies를 사용하여 변경 내용 추적 프록시를 사용하도록 설정해야 합니다. 예:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseChangeTrackingProxies();

동적 프록시를 만들려면 엔터티 형식에서 상속된 다음 모든 속성 setter를 재정의하는 새 동적 .NET 형식(Castle.Core 프록시 구현 사용)을 만들어야 합니다. 따라서 프록시의 엔터티 형식은 상속할 수 있는 형식이어야 하며 재정의할 수 있는 속성이 있어야 합니다. 또한 명시적으로 만든 컬렉션 탐색은 INotifyCollectionChanged를 구현해야 합니다. 예:

public class Blog
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }

    public virtual IList<Post> Posts { get; } = new ObservableCollection<Post>();
}

public class Post
{
    public virtual int Id { get; set; }
    public virtual string Title { get; set; }
    public virtual string Content { get; set; }

    public virtual int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

변경 내용 추적 프록시의 한 가지 중요한 단점은 EF Core가 항상 프록시 인스턴스를 추적해야 하며 기본 엔터티 형식의 인스턴스는 추적하지 않아야 한다는 것입니다. 이는 기본 엔터티 형식의 인스턴스가 알림을 생성하지 않기 때문입니다. 즉, 이러한 엔터티에 대한 변경 내용이 누락됩니다.

EF Core는 데이터베이스를 쿼리할 때 프록시 인스턴스를 자동으로 만들므로 일반적으로 이 단점은 새 엔터티 인스턴스를 추적하는 것으로 제한됩니다. 이러한 인스턴스는 new를 사용하는 일반적인 방법이 아니라 CreateProxy 확장 메서드를 사용하여 만들어야 합니다. 즉, 이전 예제의 코드는 이제 CreateProxy를 사용해야 합니다.

using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Change a property value
blog.Name = ".NET Blog (Updated!)";

// Add a new entity to a navigation
blog.Posts.Add(
    context.CreateProxy<Post>(
        p =>
        {
            p.Title = "What’s next for System.Text.Json?";
            p.Content = ".NET 5.0 was released recently and has come with many...";
        }));

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

변경 내용 추적 이벤트

EF Core는 엔터티가 처음으로 추적될 때 ChangeTracker.Tracked 이벤트를 발생합니다. 이후 엔터티 상태가 변경되면 ChangeTracker.StateChanged 이벤트가 발생합니다. 자세한 내용은 EF Core의 .NET 이벤트를 참조하세요.

참고 항목

상태가 Detached에서 다른 상태 중 하나로 변경되었더라도 엔터티를 처음 추적할 때 StateChanged 이벤트가 발생하지 않습니다. 모든 관련 알림을 받으려면 StateChangedTracked 이벤트를 모두 수신 대기해야 합니다.