Skip to main content

Command Palette

Search for a command to run...

EF Core 6 배우기 - 5. 변경 추적 기능

Published
6 min read

EF Core는 변경 내용을 추적하는 기능을 제공 합니다. 이 기능을 이용해서 DbContext의 SaveChanges가 호출되었을 때 변경된 내용만 데이터베이스에 반영되게 됩니다.

엔터티를 추적하는 방법

EF Core가 변경을 추적하는 유형은 다음과 같습니다.

  • 데이터베이스 쿼리 반환
  • Add, Attach, Update 또는 유사한 메서드를 통해 명시적으로 DbContext에 연결
  • 추적된 기존 엔터티에 연결된 엔터티로 검색할 경우

다음은 추적이 종료되는 경우 입니다.

  • DbContext 삭제
  • 변경 내용 추적기가 제거된 경우
  • 엔터티가 명시적으로 분리된 경우

DbContext는 일시적인 작업 단위로 설계되었으므로 엔터티 추적을 종료하는 가장 일반적인 방법은 DbContext를 삭제하는 것입니다. DbContext의 수명은 일반적으로 다음과 같습니다.

  • DbContext 인스턴스 생성
  • 엔터티 추적
  • 엔터티 변경
  • SaveChanges를 통해 엔터티 추적 변경 내용 반영
  • DbContext 인스턴스 삭제

엔터티 상태

엔터티의 변경을 추적하기 위해 엔터티는 상태를 가집니다. EntityState를 통해 엔터티의 상태가 관리됩니다.

  • Detached는 추적되지 않습니다.
  • Added는 삽입되어야 할 추적 대상이 있음을 의미합니다.
  • Unchanged는 쿼리 시점 이후에 변경되지 않았음을 의미합니다.
  • Modified는 쿼리 후 변경되었음을 의미합니다. 따라서 SaveChanged시 변경된 내용이 업데이트 됩니다.
  • Deleted는 데이터베이스에는 존재하지만 추적에 의해 대상이 삭제되었음을 의미하며 SaveChanged 호출 시 삭제됩니다.

동작에 따른 상태 변화

변경 내용 추적은 쿼리 시점과 반영 시점이 동일한 DbContext 인스턴스를 사용할 때 가장 이상적으로 동작합니다. 변경 내용 추적은 DbContext가 삭제될 때 같이 소멸되기 때문입니다. 쿼리 및 쿼리 대상을 수정하여 업데이트 하는 것 뿐만 아니라 쿼리 후 삽입, 업데이트, 삭제를 순차적으로 했을 경우에도 해당합니다. 엔터티는 내부적으로 EntityState를 이용해 순차적으로 추적 내용을 업데이트 하며 최종 상태를 이용해 SaveChanges시점에서 데이터베이스에 변경 내용을 업데이트 한 후 상태를 Unchanged로 변경합니다.

이를 기존 프로젝트를 이용해 확인해보도록 합시다.

using TodoApp.DbContexts;

using var c = new TodoContext();

var result = c.SaveChanges();
Console.WriteLine(result);

| 결과

0

DBContext 인스턴스를 생성하고 어떠한 작업 없이 SaveChanges()를 호출하면 변경 추적 내용이 없으므로 결과로 0을 반환합니다.

새로운 사용자를 추가해 봅시다.

var newUser = new UserInfo
{
    UserId = "test",
    UserName = "test"
};

newUser는 아직은 DbContext에 연결된 인스턴스가 아니므로 상태를 확인했을 때 Detached임을 알 수 있습니다.

using TodoApp.DbContexts;
using TodoApp.Entities;

using var c = new TodoContext();

var newUser = new UserInfo
{
    UserId = "test",
    UserName = "test"
};

Console.WriteLine(c.Entry(newUser).State);

var result = c.SaveChanges();
Console.WriteLine(result);

| 결과

Detached
0

newUserc.UsersAdd했을 때 상태가 Added로 변경됩니다.

c.Users.Add(newUser);

Console.WriteLine(c.Entry(newUser).State);

| 결과

Added

이제 SaveChanges를 호출해서 상태가 어떻게 변하는지를 확인해 봅시다.

using TodoApp.DbContexts;
using TodoApp.Entities;

using var c = new TodoContext();

var newUser = new UserInfo
{
    UserId = "test",
    UserName = "test"
};

c.Users.Add(newUser);

Console.WriteLine(c.Entry(newUser).State);

var result = c.SaveChanges();
Console.WriteLine(result);

Console.WriteLine(c.Entry(newUser).State);

| 결과

Added
1
Unchanged

SaveChanges 호출에 의해 데이터베이스에 업데이트 되고 반영된 갯수가 1임을 확인할 수 있었고 상태가 Unchanged로 변경되었음을 확인할 수 있습니다.

image.png

이제 저장되어 있는 test 사용자를 찾아 이름을 테스트로 변경해봅시다.

var testUser = c.Users.Find("test")!;
Console.WriteLine(testUser);

Console.WriteLine(c.Entry(testUser).State);

testUser.UserName = "테스트";

Console.WriteLine(c.Entry(testUser).State);

var result = c.SaveChanges();
Console.WriteLine(result);

Console.WriteLine(c.Entry(testUser).State);

| 결과

UserInfo { UserId = test, UserName = test, Todos =  }
Unchanged
Modified
1
Unchanged
UserInfo { UserId = test, UserName = 테스트, Todos =  }

image.png

이제 추가된 사용자를 삭제해봅시다.

var testUser = c.Users.Find("test")!;

Console.WriteLine(c.Entry(testUser).State);

c.Users.Remove(testUser);

Console.WriteLine(c.Entry(testUser).State);

var result = c.SaveChanges();
Console.WriteLine(result);

Console.WriteLine(c.Entry(testUser).State);

testUser = c.Users.Find("test")!;
Console.WriteLine(testUser);

| 결과

Unchanged
Deleted
1
Detached

image.png

각각의 동작에 따라 상태가 Detached, 'Unchanged', 'Added, 'Modified', 'Deleted' 됨을 확인할 수 있습니다.

탐색 속성의 상태 변경 추적

변경에 대한 추적은 일반 엔터티 및 일반 속성에만 해당되지 않습니다. test라는 사용자를 추가 한 후 다음의 코드를 실행해봅시다.

var testUser = c.Users.Find("test")!;
Console.WriteLine(testUser);
Console.WriteLine(testUser.Todos);

| 결과

UserInfo { UserId = test, UserName = 테스트, Todos =  }

탐색 속성인 Todosnull입니다. 질의를 수정해봅시다.

var testUser = c.Users.Include(x => x.Todos).First(x => x.UserId == "test");
Console.WriteLine(testUser);
Console.WriteLine(testUser.Todos);

| 결과

UserInfo { UserId = test, UserName = 테스트, Todos = System.Collections.Generic.HashSet`1[TodoApp.Entities.TodoInfo] }
System.Collections.Generic.HashSet`1[TodoApp.Entities.TodoInfo]

이제 결과가 다르게 나옵니다. Include를 이용해 Join 질의를 했으며 이렇게 탐색 속성과 함께 질의를 했을 때 탐색 속성도 상태 변경 추적의 대상이 됩니다.

할 일과 태그를 동시에 업데이트 하기

다음의 코드를 통해 test사용자의 할 일할 일에 태그를 기록해서 데이터베이스에 업데이트 해봅시다.

var testUser = c.Users.Include(x => x.Todos).First(x => x.UserId == "test");
var newTodo = new TodoInfo
{
    //UserId = "test",
    TodoDate = DateOnly.FromDateTime(DateTime.Now),
    IsComplete = false,
    IsDel = false,
    Memo = "탐색 속성 변경 추적 확인",
    Tags = new TodoTagInfo[] { new() { TagId = "dev", Descption = "개발" }, new() { TagId = "test", Descption = "테스트" } }
};

testUser.Todos.Add(newTodo);

var result = c.SaveChanges();
Console.WriteLine(result);

| 결과

5

| TodoInfo 테이블 image.png

| TodoTagInfo 테이블 image.png

| TodoInfoTodoTagInfo 테이블 (다대다 탐색 속성에 의한 자동 생성 테이블) image.png

코드는 단지 하나의 TodoInfo를 삽입했을 뿐인데 총 5개의 업데이트가 이루어졌는데요, 이것은 탐색 속성 또한 상태 추적의 대상이 되기 때문입니다.

잘 업데이트 되었는지 다음의 코드를 통해 확인해봅시다.

var testUser = c.Users.Include(x => x.Todos).First(x => x.UserId == "test");
foreach (var testTodos in testUser.Todos)
{
    Console.WriteLine(testTodos);
}

| 결과

TodoInfo { Seq = 2, TodoDate = 2022-06-27, CompleteDate = , IsComplete = False, Memo = 탐색 속성 변경 추적 확인, IsDel = False, UserId = test, User = UserInfo { UserId = test, UserName = 테스트, Todos = System.Collections.Generic.HashSet`1[TodoApp.Entities.TodoInfo] }, Histories = , Tags =  }

기대하는 Tags에 아무런 값이 없는데요, Include할 대상을 알아서 넣어주지는 않습니다. 다음처럼 변경해봅시다.

var testUser = c.Users.Include(x => x.Todos).ThenInclude(y => y.Tags).First(x => x.UserId == "test");
foreach (var testTodos in testUser.Todos)
{
    Console.WriteLine(testTodos.Memo);
    foreach (var tag in testTodos.Tags)
        Console.WriteLine($"Tags : {tag.TagId} - {tag.Descption}");
}

데이터 지연 로드

질의 시 필요한 데이터를 모두 질의 하는 방법도 있지만 지연로드를 이용해 필요한 시점에서 (속성이 읽혀지는 시점에서) 추가 질의를 통해 데이터를 가져올 수도 있습니다. 이를 활성화 하려면 아래의 지침을 따릅니다.

  • Microsoft.EntityFrameworkCore.Proxies 패키지 추가
  • DbContextOnConfiguring().UserLazyLoadingProxies() 추가
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
      => optionsBuilder
          .UseLazyLoadingProxies()
          .UseSqlServer(myConnectionString);
    
  • 또는 AddDbContext를 사용하는 경우:
    .AddDbContext<BloggingContext>(
      b => b.UseLazyLoadingProxies()
            .UseSqlServer(myConnectionString));
    
    이를 통해 virtual 탐색 속성은 지연된 로드를 사용하게 됩니다. 또는 ILazyLoader 서비스 참조를 통해 지연로드를 수동으로 사용할 수 도 있습니다.

연결되지 않은 엔터티 추적

DbContext 인스턴스를 생성해서 수행하는 추적 기능은 상당히 유용하지만 대부분의 HTTP 요청 웹 서비스의 경우 서버와 클라이언트의 데이터의 주고 받음에 의해 DbContext와의 연결이 끊어진 상태가 됩니다. 이때 끊어진 추적을 시작하려면 다음과 같이 수행합니다.

context.Attach(
    new Blog { Id = 1, Name = ".NET Blog", });

위의 예시는 단순성을 위해 new를 사용하였지만 실제로는 클라이언트에서 수신된 데이터를 이용하게 됩니다.

변경 내용 추적기 디버깅

변경 내용에 대한 추적 내용은 ChangeTracker을 통해 얻을 수 있습니다. Tracked 이벤트를 통해 추적된 내용이 변경되었을 때 이벤트 수신을 통해 내용을 확인할 수 있습니다. 또한 디버그 모드에 유용한 ChangeTracker.DebugView를 통해 추적된 이력을 확인할 수 있습니다.

정리

오늘은 EF Core의 변경 내용 추적에 대해 살펴보았습니다. EF Core 문서 변경 내용 추적을 통해 좀 더 상세한 내용을 살펴보실 수 있습니다. 다음 시간에는 EF Core가 생성하는 쿼리를 로그를 통해 확인하는 방법을 살펴보도록 하겠습니다.

https://github.com/dimohy/efcore-learning/tree/main/5.%20%EB%B3%80%EA%B2%BD%20%EC%B6%94%EC%A0%81%20%EA%B8%B0%EB%8A%A5/TodoApp

More from this blog

개발, 테스트, 운영에서의 도커 활용

핵심 원칙: "한 번 빌드하고, 어디서든 실행한다 (Build once, run anywhere)" 도커의 가장 큰 장점은 환경 일관성입니다. 동일한 도커 이미지를 사용하여 개발, 테스트, 운영 환경을 구성함으로써 "제 PC에서는 됐는데..." 하는 문제를 최소화할 수 있습니다. 1. 개발 단계 (Development) 목표: 빠른 코드 변경 반영, 쉬운 디버깅, 실제 운영 환경과 유사한 환경 구성. Docker 사용 방안: Dockerf...

May 9, 20256 min read15

[EF Core] 데이터 삭제 시 소프트 삭제 적용

DB에서 데이터를 삭제하면 일반적으로 복구할 수 없습니다. 또한 관계에 따라 영구 삭제 자체가 어려울 수도 있습니다. 그래서 데이터를 영구 삭제하는 대신 IsDeleted 속성을 true로 주고 IsDeleted 속성을 필터링해서 조회하는 방법을 사용하기도 합니다. 이를 소프트 삭제라고 합니다. 그런데 EF에서 알아서 데이터 삭제 시 소프트 삭제를 하고 쿼리시 IsDeleted 속성을 체크해서 삭제한 데이터를 제외한 데이터만 쿼리하게 하는 ...

Mar 18, 20243 min read19

[EF Core] ValueConverter를 이용해서 엔터티 속성의 도메인 관리

EF Core를 사용하면서 문자열 길이 등의 특성을 일일이 지정하는 것은 번거롭습니다. ... [MaxLength(32)] public string? 제목 { get; set; } 엔터티가 한 개일 때는 상관이 없으나 제목 유형이 여러 엔터티에 사용될 경우 유형을 지정하기 번거롭습니다. 속성 유형을 도메인으로 관리하면 참 편할텐데요, ValueConverter를 이용할 수 있습니다. 그런데 이것을 인터페이스 정적 추상를 사용해서 다음처럼 ...

Mar 16, 20242 min read8

디모이 블로그

154 posts

.NET 관련 기술을 선호하고 새로운 언어를 배우는데 관심이 있습니다.