ASP.NET Core 3.1 · 3부. 데이터 접근

Entity Framework Core 소개

객체와 표를 잇는 방식입니다.

예상 학습 시간 18분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 중급
개념 설명

형식으로 적고 SQL 은 맡깁니다

지금까지 할일 목록을 List<Todo> 에 담아 두었습니다. 프로그램을 끄면 사라집니다. 남기려면 데이터베이스에 담아야 하는데, 그러자면 표를 만들고 SQL 을 적어야 합니다.

Entity Framework Core 는 그 사이를 잇습니다. 우리는 C# 형식과 LINQ 로 적고, EF Core 가 그것을 표와 SQL 로 바꿉니다. 이런 것을 ORM(Object-Relational Mapping)이라고 부릅니다.

모든 프로젝트가 ORM 을 사용하는 것은 아닙니다. 저장 프로시저를 직접 적고 그 결과를 형식에 담는 방식도 널리 사용합니다. 질의를 손으로 다듬어야 하거나, 데이터베이스 쪽에 이미 규칙이 들어 있는 경우입니다. 이 트랙은 EF Core 를 다루지만, 그것이 유일한 방법은 아니라는 것을 알아 두십시오.

최소 예제

형식 하나와 DbContext 하나

담을 형식을 보통의 클래스로 적고, 그것을 다루는 자리를 DbContext 로 만듭니다.

Todo.cs · TodoContext.cs
public class Todo {
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public bool Done { get; set; }
    public int Priority { get; set; }
}

public class TodoContext(string path) : DbContext {
    public DbSet<Todo> Todos => Set<Todo>();

    protected override void OnConfiguring(DbContextOptionsBuilder options) =>
        options.UseSqlite($"Data Source={path}");
}
EF Core 가 만든 표
CREATE TABLE "Todos" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_Todos" PRIMARY KEY AUTOINCREMENT, "Title" TEXT NOT NULL, "Done" INTEGER NOT NULL, "Priority" INTEGER NOT NULL );

이 코드는 데이터베이스가 있어야 하므로 브라우저에서 실행할 수 없습니다. 아래에서 질의가 언제 실행되는지만 따로 실행해 볼 수 있습니다.

표를 적지 않았는데 만들어졌습니다. 규약을 따랐기 때문입니다.

  • Id 라는 이름은 기본 키로 봅니다. TodoId 도 같습니다.
  • 정수 기본 키는 자동으로 번호가 매겨집니다.
  • 물음표 없는 stringNOT NULL 입니다. 2.3에서 본 규칙이 여기에도 적용됩니다.
  • DbSet 속성 이름이 표 이름이 됩니다.

규약과 다르게 두려면 따로 적습니다. 3.2에서 봅니다.

상세 사용법

LINQ 가 SQL 이 됩니다

읽을 때는 SQL 대신 LINQ 로 적습니다. 우리가 적은 것이 어떤 SQL 이 되는지 ToQueryString() 으로 볼 수 있습니다.

Program.cs
var 남은것 = db.Todos.Where(t => !t.Done).OrderByDescending(t => t.Priority);

Console.WriteLine(남은것.ToQueryString());

foreach (var t in 남은것) Console.WriteLine($"{t.Id} {t.Title} ({t.Priority})");
출력
SELECT "t"."Id", "t"."Done", "t"."Priority", "t"."Title" FROM "Todos" AS "t" WHERE NOT ("t"."Done") ORDER BY "t"."Priority" DESC 3 세차 (5) 1 우유 사기 (3)

거르기와 정렬이 SQL 안으로 들어갔습니다. 세 줄을 모두 가져와 C# 에서 거른 것이 아닙니다. 데이터베이스가 두 줄만 돌려주었습니다. 줄이 수십만 개일 때 이 차이가 큽니다.

처음에는 ToQueryString() 을 자주 찍어 보십시오. LINQ 를 어떻게 적느냐에 따라 SQL 이 달라집니다. 어떤 것은 SQL 로 바뀌지 못해 자료를 모두 가져온 뒤 C# 에서 거르게 되는데, 그때도 오류는 나지 않습니다.

상세 사용법

질의는 순회할 때 실행됩니다

db.Todos.Where(…) 라고 적은 순간에는 아무 일도 일어나지 않습니다. 질의를 담아 두었을 뿐이고, 순회할 때 SQL 이 나갑니다. 그래서 두 번 순회하면 두 번 나갑니다.

Program.cs
var 질의 = db.Todos.Where(t => !t.Done);   // 여기서는 나가지 않습니다

foreach (var t in 질의) { }
foreach (var t in 질의) { }

var 담아둠 = 질의.ToList();               // 여기서 한 번 나갑니다
foreach (var t in 담아둠) { }
foreach (var t in 담아둠) { }
나간 SQL 횟수
질의를 만든 뒤 : 0 한 번 순회한 뒤 : 1 두 번 순회한 뒤 : 2 ToList 뒤 두 번 : 0

같은 것을 보통의 List 로도 볼 수 있습니다. 아래를 실행해 언제 살펴보는지 확인해보십시오.

Program.cs
var 원본 = new List<Todo> {
    new("우유 사기", false),
    new("책 반납", true),
    new("세차", false)
};

var 질의 = 원본.Where(t => { Console.WriteLine("  살펴봅니다 : " + t.Title); return !t.Done; });

Console.WriteLine("질의를 만들었습니다.");

Console.WriteLine("첫 번째 순회");
foreach (var t in 질의) Console.WriteLine("  남음 : " + t.Title);

Console.WriteLine("두 번째 순회");
foreach (var t in 질의) Console.WriteLine("  남음 : " + t.Title);

Console.WriteLine("담아 둔 뒤 두 번 순회");
var 담아둠 = 질의.ToList();
foreach (var t in 담아둠) { }
foreach (var t in 담아둠) { }
Console.WriteLine("  (담을 때 한 번만 살펴봅니다)");

record Todo(string Title, bool Done);
출력
질의를 만들었습니다. 첫 번째 순회 살펴봅니다 : 우유 사기 남음 : 우유 사기 살펴봅니다 : 책 반납 살펴봅니다 : 세차 남음 : 세차 두 번째 순회 살펴봅니다 : 우유 사기 남음 : 우유 사기 살펴봅니다 : 책 반납 살펴봅니다 : 세차 남음 : 세차 담아 둔 뒤 두 번 순회 살펴봅니다 : 우유 사기 살펴봅니다 : 책 반납 살펴봅니다 : 세차 (담을 때 한 번만 살펴봅니다)

코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.

"살펴봅니다" 와 "남음" 이 번갈아 나옵니다. 하나를 찾을 때마다 돌려주고 다음을 찾으러 갑니다. 미리 다 탐색해 두는 것이 아닙니다. EF Core 의 질의도 모양이 같고, 다만 살펴보는 일을 데이터베이스가 대신합니다.

상세 사용법

가져온 것을 지켜보고 있습니다

읽어 온 객체의 속성을 바꾸면 SaveChanges()바뀐 것만 골라 UPDATE 를 만듭니다. 무엇을 바꿨는지 우리가 적지 않아도 됩니다. EF Core 가 가져온 것을 기억해 두고 비교하기 때문입니다.

Program.cs
var 하나 = db.Todos.First(t => t.Title == "세차");
하나.Done = true;

Console.WriteLine("SaveChanges 전 : " + db.Entry(하나).State);
db.SaveChanges();
Console.WriteLine("SaveChanges 후 : " + db.Entry(하나).State);

// 고칠 일이 없다면 지켜보지 않게 할 수 있습니다
var 안함 = db.Todos.AsNoTracking().First();
Console.WriteLine("AsNoTracking   : " + db.Entry(안함).State);
출력
SaveChanges 전 : Modified SaveChanges 후 : Unchanged AsNoTracking : Detached

지켜보는 데에도 값이 듭니다. 가져온 것을 모두 기억해 두어야 하기 때문입니다. 화면에 보여 주기만 하고 고치지 않는 조회라면 AsNoTracking() 을 붙이는 편이 빠르고 메모리도 덜 사용합니다.

버전 배지

EF Core 와 EF 6 는 다릅니다

EF Core 1.0 부터

이름이 비슷한 Entity Framework 6 은 .NET Framework 시절의 것으로, EF Core 와 별개입니다. 1.1에서 본 ASP.NET 과 ASP.NET Core 의 관계와 같습니다. 인터넷 예제에서 System.Data.Entity 를 사용하는 것은 옛것입니다.

EF Core 의 버전은 .NET 을 따라갑니다. .NET 10 이면 EF Core 10 이고, 함께 나옵니다.

실습 문제

직접 해보기

1. 살펴보는 횟수 줄이기 난이도 하

위 실행 예제에서 첫 번째와 두 번째 순회가 같은 일을 되풀이합니다. 한 번만 살펴보도록 고쳐보세요.

순회하기 전에 결과를 담아 두면 됩니다. 마지막 묶음이 그렇게 하고 있습니다.
var 질의 = 원본.Where(…).ToList(); // 여기서 한 번만 살펴봅니다 foreach (var t in 질의) … foreach (var t in 질의) … // EF Core 에서는 이것이 SQL 을 한 번만 보내는 것과 같습니다.
2. 규약과 다르게 두기 난이도 중

표 이름을 Todos 가 아니라 TODO_LIST 로, 기본 키 속성 이름을 Id 가 아니라 Num 으로 두고 싶습니다. 어떻게 할지 짐작해 적어보세요(3.2에서 확인합니다).

2.3의 검사 규칙처럼 어트리뷰트로 적는 방법이 있습니다.
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; [Table("TODO_LIST")] public class Todo { [Key] public int Num { get; set; } } // OnModelCreating 에서 코드로 적는 방법도 있습니다. 3.2에서 봅니다.
3. 조용히 느려지는 자리 난이도 상

아래 두 줄은 같은 결과를 냅니다. 그런데 하나는 줄이 늘수록 크게 느려집니다. 어느 쪽이고 왜 그런지 적어보세요.

① db.Todos.Where(t => !t.Done).Count()
② db.Todos.ToList().Where(t => !t.Done).Count()
ToList() 를 부른 자리에서 SQL 이 나갑니다. 그 뒤의 것은 무엇 위에서 도는 것입니까.
// ② 입니다. // ① SELECT COUNT(*) FROM "Todos" WHERE NOT "Done" // 숫자 하나만 돌아옵니다. // ② SELECT * FROM "Todos" // 줄을 모두 가져와 메모리에 담은 뒤 C# 에서 셉니다. // 줄이 백만 개면 백만 개를 모두 가져옵니다. // 오류가 나지 않아 알아채기 어렵습니다. ToQueryString() 으로 // 확인하거나, ToList() 를 어디서 부르는지 살펴야 합니다.
요약
  • 형식과 DbContext 를 적으면 EF Core 가 표와 SQL 을 만듭니다.
  • Id·물음표 없는 string 같은 규약이 표의 모양을 정합니다.
  • 질의는 적을 때가 아니라 순회할 때 실행됩니다. 두 번 순회하면 두 번 나갑니다.
  • 가져온 것을 지켜보고 있어 SaveChanges() 가 바뀐 것만 고칩니다. 고칠 일이 없으면 AsNoTracking() 을 붙입니다.
  • ToQueryString() 으로 어떤 SQL 이 되는지 확인하십시오. 잘못 적어도 오류가 나지 않습니다.