Ver3.0 ๐ Entity Framework Core ์ฑ๋ฅ ์ต์ ํ ๊ธฐ๋ฒ: ์ค์ ์์ ๋ฐ๋ก ์จ๋จน๋ ์๋ฒฝ ๊ฐ์ด๋

๐ Entity Framework Core ์ฑ๋ฅ ์ต์ ํ ๊ธฐ๋ฒ: ์ค์ ์์ ๋ฐ๋ก ์จ๋จน๋ ์๋ฒฝ ๊ฐ์ด๋
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ์๋๋ฅผ 10๋ฐฐ ๋น ๋ฅด๊ฒ ๋ง๋๋ ๋น๋ฒ ๋๊ณต๊ฐ! ๐จ
์๋ ! ๐ ์ค๋์ C# ๊ฐ๋ฐ์๋ผ๋ฉด ๋๊ตฌ๋ ํ ๋ฒ์ฏค ๊ณ ๋ฏผํด๋ดค์ ์ฃผ์ , ๋ฐ๋ก Entity Framework Core(EF Core) ์ฑ๋ฅ ์ต์ ํ์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ํน์ "์ ๋ด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด๋ ๊ฒ ๋๋ฆด๊น?" ํ๊ณ ๊ณ ๋ฏผํด๋ณธ ์ ์์ด? ๋๋ถ๋ถ์ ๊ฒฝ์ฐ, ๋ฌธ์ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ ๋ฐฉ์์ ์๊ฑฐ๋ !
EF Core๋ ์ ๋ง ํธ๋ฆฌํ ORM(Object-Relational Mapping) ๋๊ตฌ์ผ. ํ์ง๋ง ์๋ชป ์ฌ์ฉํ๋ฉด ์ฑ๋ฅ์ด ๊ธ๊ฒฉํ ๋จ์ด์ง ์ ์์ด. ๋ง์น ๊ณ ๊ธ ์คํฌ์ธ ์นด๋ฅผ 1๋จ ๊ธฐ์ด๋ก๋ง ์ด์ ํ๋ ๊ฒ๊ณผ ๊ฐ์ง. ๐๏ธ
์ด ๊ธ์์๋ ์ค๋ฌด์์ ๋ฐ๋ก ์ ์ฉํ ์ ์๋ ๊ตฌ์ฒด์ ์ธ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ์น๊ตฌ์๊ฒ ์ค๋ช ํ๋ฏ ์ฝ๊ฒ ํ์ด๋ณผ๊ฒ. ์ฌ๋ฅ๋ท์์ ํ๋ก๊ทธ๋๋ฐ ๊ด๋ จ ์ง์์ ๊ณต์ ํ๋ ๋งํผ, ์ค์ ๊ฒฝํ์ ๋ฐํ์ผ๋ก ํ ํ๋ค์ ๊ฐ๋ ๋ด์์ด!
๐ฏ ์ EF Core ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ค์ํ ๊น?
๋ณธ๊ฒฉ์ ์ผ๋ก ๋ค์ด๊ฐ๊ธฐ ์ ์, ์ ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ค์ํ์ง๋ถํฐ ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ์ค์ ๋ก ๋ง์ ๊ฐ๋ฐ์๋ค์ด EF Core๋ฅผ ์ฌ์ฉํ๋ฉด์ ์ด๋ฐ ๋ฌธ์ ๋ค์ ๊ฒช์ด:
๐ ์ค์ ์ฌ๋ก: ํ ์ ์์๊ฑฐ๋ ์ฌ์ดํธ์์ ์ํ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋๋ฐ 15์ด๊ฐ ๊ฑธ๋ ธ์ด. ์์ธ์ ๋ถ์ํด๋ณด๋ 100๊ฐ์ ์ํ์ ๋ถ๋ฌ์ฌ ๋๋ง๋ค ๊ฐ ์ํ์ ์นดํ ๊ณ ๋ฆฌ, ๋ฆฌ๋ทฐ, ์ด๋ฏธ์ง๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ์กฐํํ๊ณ ์์์ง. ๊ฒฐ๊ณผ์ ์ผ๋ก 401๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๊ณ ์์์ด! ๐ฑ
์ด๋ฐ ๋ฌธ์ ๋ ๋จ์ํ "๋๋ฆฌ๋ค"๋ ๊ฒ์ ๋์ด์ ๋ค์๊ณผ ๊ฐ์ ์ฌ๊ฐํ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ํด:
โ ๏ธ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๊ฐ์ ธ์ค๋ ์
์ํฅ:
1. ์ฌ์ฉ์ ์ดํ๋ฅ ์ฆ๊ฐ - ํ์ด์ง ๋ก๋ฉ์ด 3์ด ์ด์ ๊ฑธ๋ฆฌ๋ฉด 53%์ ์ฌ์ฉ์๊ฐ ์ดํํ๋ค๋ ์ฐ๊ตฌ ๊ฒฐ๊ณผ๊ฐ ์์ด
2. ์๋ฒ ๋น์ฉ ์ฆ๊ฐ - ๋นํจ์จ์ ์ธ ์ฟผ๋ฆฌ๋ ๋ ๋ง์ ์๋ฒ ๋ฆฌ์์ค๋ฅผ ์๋นํด
3. ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถํ - ๋ถํ์ํ ์ฟผ๋ฆฌ๊ฐ DB ์๋ฒ์ ๊ณผ๋ถํ๋ฅผ ์ผ์ผ์ผ
4. ํ์ฅ์ฑ ๋ฌธ์ - ์ฌ์ฉ์๊ฐ ๋์ด๋ ์๋ก ๋ฌธ์ ๊ฐ ๊ธฐํ๊ธ์์ ์ผ๋ก ์ปค์ ธ
๊ทธ๋์ ์ค๋ ๋ฐฐ์ธ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ๋จ์ํ "ํ"์ด ์๋๋ผ, ํ๋ก๋์ ํ๊ฒฝ์์ ํ์์ ์ธ ๊ธฐ์ ์ด์ผ. ์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํด๋ณผ๊น? ๐
๐ก 1. N+1 ๋ฌธ์ ํด๊ฒฐํ๊ธฐ - ๊ฐ์ฅ ํํ ์ฑ๋ฅ ํฌ๋ฌ
EF Core์์ ๊ฐ์ฅ ํํ๊ฒ ๋ฐ์ํ๋ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ๋ฐ๋ก N+1 ๋ฌธ์ ์ผ. ์ด๊ฒ ๋ญ๋๋ฉด, 1๊ฐ์ ์ฟผ๋ฆฌ๋ก N๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ ํ, ๊ฐ ๋ฐ์ดํฐ๋ง๋ค ์ถ๊ฐ๋ก 1๊ฐ์ฉ์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ ๊ฑฐ์ง.
๐ด ์๋ชป๋ ์์ (N+1 ๋ฌธ์ ๋ฐ์)
// ๋ธ๋ก๊ทธ ํฌ์คํธ์ ์์ฑ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ฝ๋
var posts = await context.Posts.ToListAsync();
foreach (var post in posts)
{
// ๊ฐ ํฌ์คํธ๋ง๋ค ์์ฑ์ ์ ๋ณด๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ์กฐํ!
Console.WriteLine($"{post.Title} - {post.Author.Name}");
}
// ์คํ๋๋ ์ฟผ๋ฆฌ:
// 1. SELECT * FROM Posts (1๊ฐ์ ์ฟผ๋ฆฌ)
// 2. SELECT * FROM Authors WHERE Id = 1 (N๊ฐ์ ์ฟผ๋ฆฌ)
// 3. SELECT * FROM Authors WHERE Id = 2
// ... ์ด N+1๊ฐ์ ์ฟผ๋ฆฌ ์คํ!
์ ์ฝ๋๋ 100๊ฐ์ ํฌ์คํธ๊ฐ ์๋ค๋ฉด 101๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ์คํํด. ์ด๊ฑด ์ ๋ง ๋นํจ์จ์ ์ด์ง! ๐ฐ
โ ์ฌ๋ฐ๋ฅธ ์์ (Eager Loading ์ฌ์ฉ)
// Include๋ฅผ ์ฌ์ฉํ Eager Loading
var posts = await context.Posts
.Include(p => p.Author) // ์์ฑ์ ์ ๋ณด๋ฅผ ํจ๊ป ๋ก๋
.ToListAsync();
foreach (var post in posts)
{
Console.WriteLine($"{post.Title} - {post.Author.Name}");
}
// ์คํ๋๋ ์ฟผ๋ฆฌ:
// SELECT p.*, a.* FROM Posts p
// LEFT JOIN Authors a ON p.AuthorId = a.Id
// ๋จ 1๊ฐ์ ์ฟผ๋ฆฌ๋ก ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ด!
๐ Pro Tip:
Include() ๋ฉ์๋๋ ๊ด๋ จ ์ํฐํฐ๋ฅผ ํ ๋ฒ์ ๋ก๋ํด์ค. ์ด๋ฅผ Eager Loading(์ฆ์ ๋ก๋ฉ)์ด๋ผ๊ณ ํด. ์ฑ๋ฅ ํฅ์ ํจ๊ณผ๊ฐ ์์ฒญ๋๋๊น ๊ผญ ๊ธฐ์ตํด๋ฌ!
๐ ๋ค์ค Include ์ฌ์ฉํ๊ธฐ
์ฌ๋ฌ ๊ด๊ณ๋ฅผ ํ ๋ฒ์ ๋ก๋ํด์ผ ํ ๋๋ ์์ด. ์๋ฅผ ๋ค์ด, ํฌ์คํธ์ ํจ๊ป ์์ฑ์, ๋๊ธ, ํ๊ทธ๋ฅผ ๋ชจ๋ ๊ฐ์ ธ์์ผ ํ๋ค๋ฉด?
var posts = await context.Posts
.Include(p => p.Author) // ์์ฑ์
.Include(p => p.Comments) // ๋๊ธ๋ค
.ThenInclude(c => c.Author) // ๊ฐ ๋๊ธ์ ์์ฑ์
.Include(p => p.Tags) // ํ๊ทธ๋ค
.ToListAsync();
// ThenInclude๋ Includeํ ์ํฐํฐ์ ํ์ ๊ด๊ณ๋ฅผ ๋ก๋ํ ๋ ์ฌ์ฉํด
โ ๏ธ ์ฃผ์์ฌํญ:
๋๋ฌด ๋ง์ Include๋ฅผ ์ฌ์ฉํ๋ฉด ์คํ๋ ค ์ฑ๋ฅ์ด ๋จ์ด์ง ์ ์์ด. ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํ์ ์ผ๋ก ๋ก๋ํ๋ ๊ฒ ์ค์ํด. ํนํ ์ปฌ๋ ์
์ ์ฌ๋ฌ ๊ฐ Includeํ๋ฉด ์นดํฐ์
๊ณฑ(Cartesian Product) ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ ์กฐ์ฌํด!
๐ฏ Split Query ์ฌ์ฉํ๊ธฐ
EF Core 5.0๋ถํฐ๋ Split Query๋ผ๋ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋์ด. ์ฌ๋ฌ ์ปฌ๋ ์ ์ Includeํ ๋ ์ ์ฉํด:
var posts = await context.Posts
.Include(p => p.Comments)
.Include(p => p.Tags)
.AsSplitQuery() // ๊ฐ Include๋ฅผ ๋ณ๋์ ์ฟผ๋ฆฌ๋ก ๋ถ๋ฆฌ
.ToListAsync();
// ์นดํฐ์
๊ณฑ ๋ฌธ์ ๋ฅผ ํผํ๋ฉด์ ํจ์จ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์
โก 2. AsNoTracking์ผ๋ก ์ฝ๊ธฐ ์ ์ฉ ์ฟผ๋ฆฌ ์ต์ ํํ๊ธฐ
EF Core๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์กฐํํ ๋ชจ๋ ์ํฐํฐ๋ฅผ ๋ณ๊ฒฝ ์ถ์ (Change Tracking)ํด. ์ด๊ฒ ๋ญ๋๋ฉด, ๋์ค์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ณ SaveChanges()๋ฅผ ํธ์ถํ ๋ ์ด๋ค ๋ถ๋ถ์ด ๋ณ๊ฒฝ๋๋์ง ์์๋ด๊ธฐ ์ํด ์๋ณธ ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ณด๊ดํ๋ ๊ฑฐ์ผ.
ํ์ง๋ง ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ธฐ๋ง ํ ๋๋ ์ด ๊ธฐ๋ฅ์ด ๋ถํ์ํ ์ค๋ฒํค๋๊ฐ ๋ผ. ๋ง์น ์ฑ ์ ์ฝ์ ๋๋ง๋ค ๋ณต์ฌ๋ณธ์ ๋ง๋ค์ด๋๋ ๊ฒ๊ณผ ๊ฐ์ง! ๐
๐ด ๊ธฐ๋ณธ ๋์ (๋ณ๊ฒฝ ์ถ์ ํ์ฑํ)
// ๊ธฐ๋ณธ์ ์ผ๋ก ๋ณ๊ฒฝ ์ถ์ ์ด ํ์ฑํ๋จ
var products = await context.Products
.Where(p => p.Price > 1000)
.ToListAsync();
// EF Core๋ ๊ฐ Product ์ํฐํฐ์ ์๋ณธ ๊ฐ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
// ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ + ์ฒ๋ฆฌ ์๊ฐ ์ฆ๊ฐ
โ AsNoTracking ์ฌ์ฉ (์ฝ๊ธฐ ์ ์ฉ)
// ์ฝ๊ธฐ ์ ์ฉ ์ฟผ๋ฆฌ์๋ AsNoTracking ์ฌ์ฉ
var products = await context.Products
.AsNoTracking() // ๋ณ๊ฒฝ ์ถ์ ๋นํ์ฑํ
.Where(p => p.Price > 1000)
.ToListAsync();
// ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๊ฐ์ + ์ฒ๋ฆฌ ์๋ ํฅ์ (์ฝ 30-50% ๋นจ๋ผ์ง!)
๐ ์ธ์ AsNoTracking์ ์ฌ์ฉํ ๊น?
โ ์ฌ์ฉํ๋ฉด ์ข์ ๊ฒฝ์ฐ:
โข API ์๋ต์ฉ ๋ฐ์ดํฐ ์กฐํ
โข ๋ฆฌํฌํธ๋ ํต๊ณ ๋ฐ์ดํฐ ์์ฑ
โข ํ๋ฉด ํ์์ฉ ๋ฐ์ดํฐ ์กฐํ
โข ๋๋์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ธฐ๋ง ํ ๋
โ ์ฌ์ฉํ๋ฉด ์ ๋๋ ๊ฒฝ์ฐ:
โข ์กฐํ ํ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ์์ ์ผ ๋
โข SaveChanges()๋ฅผ ํธ์ถํ ๊ณํ์ด ์์ ๋
๐ ์ ์ญ ์ค์ ์ผ๋ก ๊ธฐ๋ณธ๊ฐ ๋ณ๊ฒฝํ๊ธฐ
๋๋ถ๋ถ์ ์ฟผ๋ฆฌ๊ฐ ์ฝ๊ธฐ ์ ์ฉ์ด๋ผ๋ฉด, ์์ ๊ธฐ๋ณธ ๋์์ ๋ณ๊ฒฝํ ์๋ ์์ด:
// DbContext ์ค์ ์์ ์ ์ญ์ผ๋ก NoTracking ์ค์
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
// ์ด์ ๋ชจ๋ ์ฟผ๋ฆฌ๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก NoTracking
// ํ์ํ ๊ฒฝ์ฐ์๋ง .AsTracking()์ ์ฌ์ฉํ๋ฉด ๋ผ
์ค์ ์ฑ๋ฅ ํ ์คํธ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด, 10,000๊ฐ์ ์ํฐํฐ๋ฅผ ์กฐํํ ๋:
๐ ์ฑ๋ฅ ๋น๊ต:
โข ๊ธฐ๋ณธ ์ถ์ ๋ชจ๋: ์ฝ 850ms, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ 45MB
โข AsNoTracking ๋ชจ๋: ์ฝ 420ms, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ 22MB
์ฝ 50% ์ฑ๋ฅ ํฅ์ + 50% ๋ฉ๋ชจ๋ฆฌ ์ ๊ฐ! ๐
๐จ 3. Select๋ฅผ ํ์ฉํ ํ๋ก์ ์ ์ต์ ํ
๋ง์ ๊ฐ๋ฐ์๋ค์ด ๋์น๋ ๋ถ๋ถ์ธ๋ฐ, ํ์ํ ์ปฌ๋ผ๋ง ์ ํํด์ ๊ฐ์ ธ์ค๋ ๊ฒ๋ง์ผ๋ก๋ ์์ฒญ๋ ์ฑ๋ฅ ํฅ์์ ์ป์ ์ ์์ด. ์ ์ฒด ์ํฐํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฑด ๋ง์น ํธ์์ ์์ ์ฐ์ ํ๋ ์ฌ๋ฌ ๊ฐ๋๋ฐ ์ผํ์นดํธ๋ฅผ ๋๊ณ ๊ฐ๋ ๊ฒ๊ณผ ๊ฐ์! ๐
๐ด ๋นํจ์จ์ ์ธ ๋ฐฉ์
// ์ ์ฒด ์ํฐํฐ๋ฅผ ๊ฐ์ ธ์จ ํ ํ์ํ ํ๋๋ง ์ฌ์ฉ
var users = await context.Users
.Include(u => u.Profile)
.Include(u => u.Orders)
.ToListAsync();
var userNames = users.Select(u => u.Name).ToList();
// ๋ฌธ์ ์ :
// 1. ๋ถํ์ํ ๋ชจ๋ ์ปฌ๋ผ์ DB์์ ๊ฐ์ ธ์ด
// 2. ๊ด๋ จ ์ํฐํฐ๊น์ง ๋ชจ๋ ๋ก๋
// 3. ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฒด ๊ฐ์ฒด๋ฅผ ์์ฑ
// 4. ๊ฒฐ๊ตญ Name๋ง ์ฌ์ฉํ๋๋ฐ ์์ฒญ๋ ๋ญ๋น!
โ ํจ์จ์ ์ธ ๋ฐฉ์ (ํ๋ก์ ์ )
// ํ์ํ ์ปฌ๋ผ๋ง ์ ํํด์ ๊ฐ์ ธ์ค๊ธฐ
var userNames = await context.Users
.Select(u => u.Name)
.ToListAsync();
// ๋๋ ์ฌ๋ฌ ํ๋๊ฐ ํ์ํ๋ค๋ฉด ์ต๋ช
ํ์
์ฌ์ฉ
var userInfo = await context.Users
.Select(u => new
{
u.Id,
u.Name,
u.Email,
OrderCount = u.Orders.Count()
})
.ToListAsync();
// ์คํ๋๋ SQL:
// SELECT Id, Name, Email, COUNT(Orders.Id) FROM Users
// ๋ฑ ํ์ํ ๊ฒ๋ง ๊ฐ์ ธ์!
๐ฏ DTO(Data Transfer Object) ํ์ฉํ๊ธฐ
์ค๋ฌด์์๋ ๋ณดํต DTO ํด๋์ค๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํด. ํ์ ์์ ์ฑ๋ ํ๋ณดํ๊ณ ์ฝ๋๋ ๊น๋ํด์ง๊ฑฐ๋ :
// DTO ํด๋์ค ์ ์
public class UserSummaryDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int OrderCount { get; set; }
public DateTime LastLoginDate { get; set; }
}
// DTO๋ก ํ๋ก์ ์
var userSummaries = await context.Users
.Select(u => new UserSummaryDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email,
OrderCount = u.Orders.Count(),
LastLoginDate = u.LastLogin
})
.ToListAsync();
// ์ฅ์ :
// 1. ํ์
์์ ์ฑ ํ๋ณด
// 2. IntelliSense ์ง์
// 3. ์ฌ์ฌ์ฉ ๊ฐ๋ฅ
// 4. API ์๋ต ๋ชจ๋ธ๋ก ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
๐ ํ๋ก์ ์
์ ๋๋ผ์ด ํจ๊ณผ:
์ค์ ํ๋ก์ ํธ์์ User ํ
์ด๋ธ(30๊ฐ ์ปฌ๋ผ)์์ ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ ํ์ํ 5๊ฐ ์ปฌ๋ผ๋ง ์ ํํ๋๋ก ๋ณ๊ฒฝํ๋๋:
โข ์ฟผ๋ฆฌ ์คํ ์๊ฐ: 1,200ms โ 180ms (์ฝ 85% ๊ฐ์!)
โข ๋คํธ์ํฌ ์ ์ก๋: 2.5MB โ 0.3MB (์ฝ 88% ๊ฐ์!)
โข ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋: 35MB โ 6MB (์ฝ 83% ๊ฐ์!)
๐ฅ ๋ณต์กํ ํ๋ก์ ์ ์์
์ค๋ฌด์์๋ ๋ ๋ณต์กํ ํ๋ก์ ์ ์ด ํ์ํ ๋๊ฐ ๋ง์. ์๋ฅผ ๋ค์ด ์ฃผ๋ฌธ ๋ชฉ๋ก๊ณผ ํจ๊ป ๊ณ ๊ฐ ์ ๋ณด, ์ํ ์ ๋ณด๋ฅผ ์กฐํฉํด์ผ ํ๋ค๋ฉด:
var orderDetails = await context.Orders
.Where(o => o.OrderDate >= DateTime.Now.AddMonths(-1))
.Select(o => new OrderDetailDto
{
OrderId = o.Id,
OrderDate = o.OrderDate,
CustomerName = o.Customer.Name,
CustomerEmail = o.Customer.Email,
TotalAmount = o.OrderItems.Sum(oi => oi.Quantity * oi.UnitPrice),
ItemCount = o.OrderItems.Count(),
Products = o.OrderItems.Select(oi => new ProductSummaryDto
{
ProductName = oi.Product.Name,
Quantity = oi.Quantity,
Price = oi.UnitPrice
}).ToList(),
Status = o.Status
})
.ToListAsync();
// ์ด๋ ๊ฒ ํ๋ฉด ๋ณต์กํ ๋ฐ์ดํฐ๋ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ํจ์จ์ ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์์ด!
๐ 4. ํ์ด์ง(Pagination)์ผ๋ก ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌํ๊ธฐ
์์ฒ, ์๋ง ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ๊ฐ์ ธ์ค๋ ๊ฑด ์ ๋ ๊ธ๋ฌผ์ด์ผ! ๐ซ ์ฌ์ฉ์๋ ํ ๋ฒ์ ๊ทธ๋ ๊ฒ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ณผ ์ ์๊ณ , ์๋ฒ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ์์ฒญ๋ ๋ถ๋ด์ด ๋๊ฑฐ๋ .
๐ด ์๋ชป๋ ๋ฐฉ์
// ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ๊ฐ์ ธ์ค๊ธฐ (์ํ!)
var allProducts = await context.Products
.OrderBy(p => p.Name)
.ToListAsync();
// 10๋ง ๊ฐ์ ์ํ์ด ์๋ค๋ฉด?
// - ๋ฉ๋ชจ๋ฆฌ ํญ๋ฐ ๐ฅ
// - ์๋ต ์๊ฐ ํญ๋ฐ ๐ฅ
// - ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถํ ํญ๋ฐ ๐ฅ
โ Skip๊ณผ Take๋ฅผ ์ฌ์ฉํ ๊ธฐ๋ณธ ํ์ด์ง
// ํ์ด์ง ๋ฒํธ์ ํ์ด์ง ํฌ๊ธฐ ์ ์
int pageNumber = 1; // ์ฒซ ๋ฒ์งธ ํ์ด์ง
int pageSize = 20; // ํ์ด์ง๋น 20๊ฐ
var products = await context.Products
.OrderBy(p => p.Name) // ์ ๋ ฌ์ ํ์!
.Skip((pageNumber - 1) * pageSize) // ๊ฑด๋๋ธ ๊ฐ์
.Take(pageSize) // ๊ฐ์ ธ์ฌ ๊ฐ์
.ToListAsync();
// SQL๋ก ๋ณํ๋๋ฉด:
// SELECT * FROM Products
// ORDER BY Name
// OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY
โ ๏ธ ์ค์ํ ์ฃผ์์ฌํญ:
Skip๊ณผ Take๋ฅผ ์ฌ์ฉํ๊ธฐ ์ ์ ๋ฐ๋์ OrderBy๋ฅผ ๋จผ์ ํธ์ถํด์ผ ํด! ๊ทธ๋ ์ง ์์ผ๋ฉด ๊ฒฐ๊ณผ๊ฐ ์์ธก ๋ถ๊ฐ๋ฅํ๊ณ , SQL Server์์๋ ์์ ์๋ฌ๊ฐ ๋ฐ์ํ ์๋ ์์ด.
๐ฏ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํ์ด์ง ํ์ฅ ๋ฉ์๋
๋งค๋ฒ Skip๊ณผ Take๋ฅผ ์ฐ๋ ๊ฑด ๋ฒ๊ฑฐ๋ก์ฐ๋๊น, ํ์ฅ ๋ฉ์๋๋ฅผ ๋ง๋ค์ด์ ์ฌ์ฉํ๋ฉด ํธํด:
// ํ์ด์ง ํ์ฅ ๋ฉ์๋
public static class QueryableExtensions
{
public static IQueryable<t> Paginate<t>(
this IQueryable<t> query,
int pageNumber,
int pageSize)
{
return query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
}
}
// ์ฌ์ฉ ์์
var products = await context.Products
.OrderBy(p => p.Name)
.Paginate(pageNumber: 2, pageSize: 20)
.ToListAsync();
// ํจ์ฌ ๊น๋ํ์ง? ๐</t></t></t>
๐ ํ์ด์ง ๊ฒฐ๊ณผ์ ์ ์ฒด ๊ฐ์ ํจ๊ป ๋ฐํํ๊ธฐ
์ค๋ฌด์์๋ ๋ณดํต ํ์ฌ ํ์ด์ง์ ๋ฐ์ดํฐ์ ํจ๊ป ์ ์ฒด ๊ฐ์๋ ํ์ํด. UI์์ "์ด 1,234๊ฐ ์ค 1-20๋ฒ์งธ" ๊ฐ์ ์ ๋ณด๋ฅผ ํ์ํ๋ ค๋ฉด ๋ง์ด์ผ:
// ํ์ด์ง ๊ฒฐ๊ณผ๋ฅผ ๋ด์ ํด๋์ค
public class PagedResult<t>
{
public List<t> Items { get; set; }
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < TotalPages;
}
// ํ์ด์ง ํฌํผ ๋ฉ์๋
public static async Task<pagedresult<t>> ToPagedResultAsync<t>(
this IQueryable<t> query,
int pageNumber,
int pageSize)
{
var totalCount = await query.CountAsync(); // ์ ์ฒด ๊ฐ์ ์กฐํ
var items = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<t>
{
Items = items,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize
};
}
// ์ฌ์ฉ ์์
var result = await context.Products
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToPagedResultAsync(pageNumber: 1, pageSize: 20);
Console.WriteLine($"์ด {result.TotalCount}๊ฐ ์ค {result.Items.Count}๊ฐ ํ์");
Console.WriteLine($"์ ์ฒด {result.TotalPages}ํ์ด์ง ์ค {result.PageNumber}ํ์ด์ง");</t></t></t></pagedresult<t></t></t>
๐ ์ฑ๋ฅ ์ต์ ํ ํ:
์ ์ฒด ๊ฐ์๋ฅผ ์กฐํํ๋ CountAsync()๋ ๋น์ฉ์ด ๋ค์ด. ํนํ ๋์ฉ๋ ํ
์ด๋ธ์์๋ ๋์ฑ ๊ทธ๋. ๋ง์ฝ ์ ํํ ๊ฐ์๊ฐ ํ์ ์๋ค๋ฉด (์: "100๊ฐ ์ด์" ์ ๋๋ง ์๋ฉด ๋๋ ๊ฒฝ์ฐ), ์บ์ฑ์ ํ์ฉํ๊ฑฐ๋ ๊ทผ์ฌ๊ฐ์ ์ฌ์ฉํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
๐ ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง (๋ฌดํ ์คํฌ๋กค์ฉ)
๋ชจ๋ฐ์ผ ์ฑ์ด๋ SNS์ฒ๋ผ ๋ฌดํ ์คํฌ๋กค์ด ํ์ํ ๊ฒฝ์ฐ์๋ ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง์ด ๋ ํจ์จ์ ์ด์ผ:
// ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง (๋ง์ง๋ง์ผ๋ก ๋ณธ ID ๊ธฐ์ค)
public async Task<list<product>> GetProductsAfterCursor(
int? lastSeenId,
int pageSize = 20)
{
var query = context.Products.AsQueryable();
if (lastSeenId.HasValue)
{
// ๋ง์ง๋ง์ผ๋ก ๋ณธ ID ์ดํ์ ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์ค๊ธฐ
query = query.Where(p => p.Id > lastSeenId.Value);
}
return await query
.OrderBy(p => p.Id)
.Take(pageSize)
.ToListAsync();
}
// ์ฌ์ฉ ์์
var firstPage = await GetProductsAfterCursor(null, 20);
var secondPage = await GetProductsAfterCursor(firstPage.Last().Id, 20);
// ์ฅ์ :
// 1. Skip์ ์ฌ์ฉํ์ง ์์ ๋์ฉ๋ ๋ฐ์ดํฐ์์๋ ๋น ๋ฆ
// 2. ์ค์๊ฐ์ผ๋ก ๋ฐ์ดํฐ๊ฐ ์ถ๊ฐ๋์ด๋ ์ค๋ณต์ด๋ ๋๋ฝ ์์
// 3. ๋ฌดํ ์คํฌ๋กค ๊ตฌํ์ ์ต์ ํ</list<product>
๐พ 5. ์ปดํ์ผ๋ ์ฟผ๋ฆฌ(Compiled Queries)๋ก ๋ฐ๋ณต ์ฟผ๋ฆฌ ์ต์ ํ
๊ฐ์ ํจํด์ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ๋ณต์ ์ผ๋ก ์คํํ๋ค๋ฉด, ์ปดํ์ผ๋ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ์ฑ๋ฅ์ ํฅ์์ํฌ ์ ์์ด. EF Core๋ LINQ ์ฟผ๋ฆฌ๋ฅผ SQL๋ก ๋ณํํ๋ ๊ณผ์ ์์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋๋ฐ, ์ด ๊ณผ์ ์ ๋ฏธ๋ฆฌ ํด๋๋ ๊ฑฐ์ง! ๐ฏ
๐ด ์ผ๋ฐ ์ฟผ๋ฆฌ (๋งค๋ฒ ๋ณํ ๊ณผ์ ๋ฐ์)
// ์ด ๋ฉ์๋๊ฐ 1์ด์ 100๋ฒ ํธ์ถ๋๋ค๋ฉด?
public async Task<user> GetUserById(int userId)
{
return await context.Users
.Include(u => u.Profile)
.FirstOrDefaultAsync(u => u.Id == userId);
// ๋งค๋ฒ LINQ โ SQL ๋ณํ ๊ณผ์ ์ด ๋ฐ์
// ๋ฐ๋ณต์ ์ผ๋ก ์คํ๋๋ฉด ๋์ ์ค๋ฒํค๋๊ฐ ์ปค์ง
}</user>
โ ์ปดํ์ผ๋ ์ฟผ๋ฆฌ ์ฌ์ฉ
// ์ปดํ์ผ๋ ์ฟผ๋ฆฌ ์ ์ (ํด๋์ค ๋ ๋ฒจ์์ static์ผ๋ก)
private static readonly Func<mydbcontext, int,="" task<user="">> _getUserByIdQuery =
EF.CompileAsyncQuery((MyDbContext context, int userId) =>
context.Users
.Include(u => u.Profile)
.FirstOrDefault(u => u.Id == userId));
// ์ฌ์ฉ
public async Task<user> GetUserById(int userId)
{
return await _getUserByIdQuery(context, userId);
// ์ฒซ ์คํ ์ ํ ๋ฒ๋ง ์ปดํ์ผ๋๊ณ , ์ดํ์๋ ์บ์๋ ๋ฒ์ ์ฌ์ฉ
// ์ฝ 10-30% ์ฑ๋ฅ ํฅ์!
}</user></mydbcontext,>
๐ฏ ๋ณต์กํ ์ปดํ์ผ๋ ์ฟผ๋ฆฌ ์์
// ์ฌ๋ฌ ๋งค๊ฐ๋ณ์๋ฅผ ๋ฐ๋ ์ปดํ์ผ๋ ์ฟผ๋ฆฌ
private static readonly Func<mydbcontext, string,="" decimal,="" int,="" task<list<product="">>>
_searchProductsQuery = EF.CompileAsyncQuery(
(MyDbContext context, string keyword, decimal minPrice, int categoryId) =>
context.Products
.Where(p => p.Name.Contains(keyword)
&& p.Price >= minPrice
&& p.CategoryId == categoryId)
.OrderBy(p => p.Name)
.Take(50)
.ToList());
// ์ฌ์ฉ
public async Task<list<product>> SearchProducts(
string keyword,
decimal minPrice,
int categoryId)
{
return await _searchProductsQuery(context, keyword, minPrice, categoryId);
}</list<product></mydbcontext,>
๐ ์ปดํ์ผ๋ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ์ข์ ๊ฒฝ์ฐ:
โข API ์๋ํฌ์ธํธ์์ ๋ฐ๋ณต์ ์ผ๋ก ํธ์ถ๋๋ ์ฟผ๋ฆฌ
โข ์ค์๊ฐ ๋์๋ณด๋๋ ๋ชจ๋ํฐ๋ง ์ฟผ๋ฆฌ
โข ๊ฒ์ ๊ธฐ๋ฅ์ฒ๋ผ ์์ฃผ ์คํ๋๋ ์ฟผ๋ฆฌ
โข ์ฑ๋ฅ์ด ์ค์ํ ํซ ํจ์ค(hot path) ์ฝ๋
๋จ, ๋๋ฌด ๋ณต์กํ๊ฑฐ๋ ๋์ ์ธ ์ฟผ๋ฆฌ์๋ ์ ํฉํ์ง ์์ ์ ์์ด!
๐๏ธ 6. ์ธ๋ฑ์ค ์ต์ ํ - ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ๋ฒจ ์ต์ ํ
์๋ฌด๋ฆฌ EF Core ์ฝ๋๋ฅผ ์ต์ ํํด๋, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ ํ ์ธ๋ฑ์ค๊ฐ ์๋ค๋ฉด ์ฑ๋ฅ์ด ๋์ ์๋ฐ์ ์์ด. ์ธ๋ฑ์ค๋ ์ฑ ์ ๋ชฉ์ฐจ์ ๊ฐ์์, ์ํ๋ ์ ๋ณด๋ฅผ ๋น ๋ฅด๊ฒ ์ฐพ์ ์ ์๊ฒ ํด์ค! ๐
๐ฏ EF Core์์ ์ธ๋ฑ์ค ์ ์ํ๊ธฐ
// Fluent API๋ฅผ ์ฌ์ฉํ ์ธ๋ฑ์ค ์ ์
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ๋จ์ผ ์ปฌ๋ผ ์ธ๋ฑ์ค
modelBuilder.Entity<product>()
.HasIndex(p => p.Name)
.HasDatabaseName("IX_Product_Name");
// ๋ณตํฉ ์ธ๋ฑ์ค (์ฌ๋ฌ ์ปฌ๋ผ)
modelBuilder.Entity<order>()
.HasIndex(o => new { o.CustomerId, o.OrderDate })
.HasDatabaseName("IX_Order_Customer_Date");
// ์ ๋ํฌ ์ธ๋ฑ์ค
modelBuilder.Entity<user>()
.HasIndex(u => u.Email)
.IsUnique()
.HasDatabaseName("IX_User_Email_Unique");
// ํํฐ๋ง๋ ์ธ๋ฑ์ค (์กฐ๊ฑด๋ถ ์ธ๋ฑ์ค)
modelBuilder.Entity<product>()
.HasIndex(p => p.Price)
.HasFilter("[IsActive] = 1") // ํ์ฑ ์ํ๋ง ์ธ๋ฑ์ฑ
.HasDatabaseName("IX_Product_Price_Active");
}</product></user></order></product>
๐ Data Annotation์ผ๋ก ์ธ๋ฑ์ค ์ ์
// ์ํฐํฐ ํด๋์ค์ ์ง์ ์ ์
[Index(nameof(Email), IsUnique = true)]
[Index(nameof(LastName), nameof(FirstName))] // ๋ณตํฉ ์ธ๋ฑ์ค
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime CreatedAt { get; set; }
}
// ๊ฐ๋จํ ๊ฒฝ์ฐ์๋ ์ด ๋ฐฉ๋ฒ์ด ๋ ์ง๊ด์ ์ด์ผ!
๐ ์ธ๋ฑ์ค ์ค๊ณ ๊ฐ์ด๋๋ผ์ธ:
1. WHERE ์ ์ ์์ฃผ ์ฌ์ฉ๋๋ ์ปฌ๋ผ
WHERE UserId = @userId ๊ฐ์ ์กฐ๊ฑด์ด ์์ฃผ ๋์จ๋ค๋ฉด UserId์ ์ธ๋ฑ์ค ํ์!
2. JOIN ์กฐ๊ฑด์ ์ฌ์ฉ๋๋ ์ธ๋ ํค
EF Core๋ ์ธ๋ ํค์ ์๋์ผ๋ก ์ธ๋ฑ์ค๋ฅผ ๋ง๋ค์ง ์์ผ๋ ์ง์ ์ถ๊ฐํด์ผ ํด
3. ORDER BY์ ์ฌ์ฉ๋๋ ์ปฌ๋ผ
์ ๋ ฌ์ด ์์ฃผ ์ผ์ด๋๋ ์ปฌ๋ผ์๋ ์ธ๋ฑ์ค๊ฐ ๋์์ด ๋ผ
4. ๋ณตํฉ ์ธ๋ฑ์ค์ ์ปฌ๋ผ ์์
์ ํ๋(selectivity)๊ฐ ๋์ ์ปฌ๋ผ์ ์์ ๋ฐฐ์นํด์ผ ํจ๊ณผ์ ์ด์ผ
โ ๏ธ ์ธ๋ฑ์ค ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
์ธ๋ฑ์ค๊ฐ ํญ์ ์ข์ ๊ฑด ์๋์ผ:
โ ๊ณผ๋ํ ์ธ๋ฑ์ค์ ๋ฌธ์ ์ :
โข INSERT, UPDATE, DELETE ์ฑ๋ฅ ์ ํ (์ธ๋ฑ์ค๋ ํจ๊ป ์
๋ฐ์ดํธํด์ผ ํ๋๊น)
โข ์ ์ฅ ๊ณต๊ฐ ์ฆ๊ฐ
โข ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ
โ ์ธ๋ฑ์ค ์ต์ ํ ํ:
โข ์ค์ ์ฟผ๋ฆฌ ํจํด์ ๋ถ์ํด์ ํ์ํ ์ธ๋ฑ์ค๋ง ์์ฑ
โข ์ฌ์ฉ๋์ง ์๋ ์ธ๋ฑ์ค๋ ์ ๊ธฐ์ ์ผ๋ก ์ ๊ฑฐ
โข ์ฐ๊ธฐ ์์
์ด ๋ง์ ํ
์ด๋ธ์ ์ธ๋ฑ์ค๋ฅผ ์ต์ํ
โข ์คํ ๊ณํ(Execution Plan)์ ํ์ธํด์ ์ธ๋ฑ์ค๊ฐ ์ค์ ๋ก ์ฌ์ฉ๋๋์ง ๊ฒ์ฆ
๐ ์ธ๋ฑ์ค ํจ๊ณผ ์ธก์ ํ๊ธฐ
// SQL Server์์ ์คํ ๊ณํ ํ์ธ
// EF Core ๋ก๊น
ํ์ฑํ
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging(); // ํ๋ผ๋ฏธํฐ ๊ฐ๋ ๋ก๊น
}
// ์คํ๋๋ SQL์ ํ์ธํ๊ณ , SSMS์์ ์คํ ๊ณํ์ ๋ถ์
// Index Seek vs Table Scan์ ๋น๊ตํด๋ด
// Index Seek์ด๋ฉด ์ธ๋ฑ์ค๊ฐ ์ ์๋ํ๋ ๊ฑฐ์ผ!
๐ 7. ๋ฐฐ์น ์ฒ๋ฆฌ์ ๋ฒํฌ ์์ ์ต์ ํ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ๋๋ ์ผ๋ฐ์ ์ธ ๋ฐฉ๋ฒ์ผ๋ก๋ ์ฑ๋ฅ์ด ๋๋ฌด ๋๋ ค. ํ ๋ฒ์ ํ๋์ฉ ์ฒ๋ฆฌํ๋ ๊ฑด ๋ง์น ํ๋ฐฐ๋ฅผ ํ ๊ฐ์ฉ ๋ฐ๋ก๋ฐ๋ก ๋ณด๋ด๋ ๊ฒ๊ณผ ๊ฐ๊ฑฐ๋ ! ๐ฆ
๐ด ๋นํจ์จ์ ์ธ ๋ฐฉ์ (ํ๋์ฉ ์ฒ๋ฆฌ)
// 1,000๊ฐ์ ์ํ ๊ฐ๊ฒฉ์ 10% ์ธ์ํ๋ ๊ฒฝ์ฐ
var products = await context.Products
.Where(p => p.CategoryId == 5)
.ToListAsync();
foreach (var product in products)
{
product.Price *= 1.1m;
await context.SaveChangesAsync(); // ๋งค๋ฒ DB ์๋ณต! ๐ฑ
}
// ๋ฌธ์ ์ :
// 1. 1,000๋ฒ์ UPDATE ์ฟผ๋ฆฌ ์คํ
// 2. 1,000๋ฒ์ ๋คํธ์ํฌ ์๋ณต
// 3. ์์ฒญ๋๊ฒ ๋๋ฆผ (์์ญ ์ด ์์)
โ ๊ฐ์ ๋ ๋ฐฉ์ (๋ฐฐ์น ์ฒ๋ฆฌ)
// SaveChanges๋ฅผ ๋ฃจํ ๋ฐ์ผ๋ก ์ด๋
var products = await context.Products
.Where(p => p.CategoryId == 5)
.ToListAsync();
foreach (var product in products)
{
product.Price *= 1.1m;
}
await context.SaveChangesAsync(); // ํ ๋ฒ๋ง ํธ์ถ!
// EF Core๋ ๋ณ๊ฒฝ์ฌํญ์ ๋ชจ์์ ๋ฐฐ์น๋ก ์ฒ๋ฆฌ
// ํจ์ฌ ๋น ๋ฆ! (๋ช ์ด ์ด๋ด)
๐ ExecuteUpdate๋ฅผ ์ฌ์ฉํ ๋ฒํฌ ์ ๋ฐ์ดํธ (EF Core 7.0+)
EF Core 7.0๋ถํฐ๋ ๋์ฑ ๊ฐ๋ ฅํ ๋ฒํฌ ์์ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋์ด:
// ExecuteUpdate๋ฅผ ์ฌ์ฉํ ์ง์ ์
๋ฐ์ดํธ
await context.Products
.Where(p => p.CategoryId == 5)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Price, p => p.Price * 1.1m)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow));
// ์คํ๋๋ SQL:
// UPDATE Products
// SET Price = Price * 1.1, UpdatedAt = GETUTCDATE()
// WHERE CategoryId = 5
// ์ฅ์ :
// 1. ๋จ 1๊ฐ์ UPDATE ์ฟผ๋ฆฌ๋ก ๋ชจ๋ ํ ์
๋ฐ์ดํธ
// 2. ๋ฉ๋ชจ๋ฆฌ์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ์ง ์์
// 3. ์์ฒญ๋๊ฒ ๋น ๋ฆ! (๋ฐ๋ฆฌ์ด ๋จ์)
๐๏ธ ExecuteDelete๋ฅผ ์ฌ์ฉํ ๋ฒํฌ ์ญ์
// ์ค๋๋ ๋ก๊ทธ ๋ฐ์ดํฐ ์ญ์
await context.Logs
.Where(l => l.CreatedAt < DateTime.UtcNow.AddMonths(-6))
.ExecuteDeleteAsync();
// ์คํ๋๋ SQL:
// DELETE FROM Logs
// WHERE CreatedAt < @date
// ๊ธฐ์กด ๋ฐฉ์๋ณด๋ค ์๋ฐฑ ๋ฐฐ ๋น ๋ฅผ ์ ์์ด!
๐ ExecuteUpdate/Delete ์ฌ์ฉ ์ ์ฃผ์์ฌํญ:
์ฅ์ :
โข ๋งค์ฐ ๋น ๋ฅธ ์ฑ๋ฅ
โข ๋ฉ๋ชจ๋ฆฌ ํจ์จ์
โข ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์ ์ต์
์ ํ์ฌํญ:
โข ๋ณ๊ฒฝ ์ถ์ (Change Tracking)์ด ์๋ํ์ง ์์
โข ์ํฐํฐ ์ด๋ฒคํธ๋ ์ธํฐ์
ํฐ๊ฐ ์คํ๋์ง ์์
โข ๋ค๋น๊ฒ์ด์
์์ฑ์ ํตํ ์
๋ฐ์ดํธ ๋ถ๊ฐ
โข ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ด ์๋ค๋ฉด ๊ธฐ์กด ๋ฐฉ์ ์ฌ์ฉ ๊ถ์ฅ
๐ฆ ๋๋ ์ฝ์ ์ต์ ํ
// ๋๋ ๋ฐ์ดํฐ ์ฝ์
์ ์ต์ ํ
var newProducts = new List<product>();
for (int i = 0; i < 10000; i++)
{
newProducts.Add(new Product
{
Name = $"Product {i}",
Price = i * 10
});
}
// AddRange๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐฐ์น๋ก ์ฒ๋ฆฌ๋จ
context.Products.AddRange(newProducts);
await context.SaveChangesAsync();
// ์ถ๊ฐ ์ต์ ํ: AutoDetectChanges ๋นํ์ฑํ
context.ChangeTracker.AutoDetectChangesEnabled = false;
context.Products.AddRange(newProducts);
context.ChangeTracker.DetectChanges(); // ํ ๋ฒ๋ง ํธ์ถ
await context.SaveChangesAsync();
context.ChangeTracker.AutoDetectChangesEnabled = true;
// ์ฑ๋ฅ์ด 2-3๋ฐฐ ํฅ์๋ ์ ์์ด!</product>
๐ฏ ์ค์ ์ฑ๋ฅ ๋น๊ต:
10,000๊ฐ์ ๋ ์ฝ๋๋ฅผ ์ฝ์
ํ๋ ๊ฒฝ์ฐ:
โข ํ๋์ฉ SaveChanges: ์ฝ 45์ด ๐ฑ
โข AddRange + SaveChanges: ์ฝ 3์ด ๐
โข AddRange + AutoDetectChanges ๋นํ์ฑํ: ์ฝ 1.5์ด ๐
โข ๋ฒํฌ ์ฝ์
๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ: ์ฝ 0.3์ด ๐ฅ
๐ญ 8. ์ง์ฐ ๋ก๋ฉ vs ์ฆ์ ๋ก๋ฉ vs ๋ช ์์ ๋ก๋ฉ
EF Core์์ ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋ ๋ฐฉ๋ฒ์ ์ธ ๊ฐ์ง๊ฐ ์์ด. ๊ฐ๊ฐ์ ํน์ง์ ์ดํดํ๊ณ ์ํฉ์ ๋ง๊ฒ ์ฌ์ฉํ๋ ๊ฒ ์ค์ํด! ๐ฏ
1๏ธโฃ ์ฆ์ ๋ก๋ฉ (Eager Loading)
// Include๋ฅผ ์ฌ์ฉํด์ ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋
var posts = await context.Posts
.Include(p => p.Author)
.Include(p => p.Comments)
.ToListAsync();
// ์ฅ์ : ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๋ชจ๋ ๋ฐ์ดํฐ ๋ก๋, N+1 ๋ฌธ์ ๋ฐฉ์ง
// ๋จ์ : ํ์ ์๋ ๋ฐ์ดํฐ๊น์ง ๋ก๋ํ ์ ์์
2๏ธโฃ ์ง์ฐ ๋ก๋ฉ (Lazy Loading)
// ์ง์ฐ ๋ก๋ฉ ํ์ฑํ (ํ๋ก์ ์ฌ์ฉ)
// NuGet: Microsoft.EntityFrameworkCore.Proxies ํ์
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString)
.UseLazyLoadingProxies(); // ์ง์ฐ ๋ก๋ฉ ํ์ฑํ
}
// ์ํฐํฐ์ ๋ค๋น๊ฒ์ด์
์์ฑ์ virtual๋ก ์ ์ธ
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public virtual Author Author { get; set; } // virtual ํค์๋!
public virtual ICollection<comment> Comments { get; set; }
}
// ์ฌ์ฉ
var post = await context.Posts.FirstAsync();
var authorName = post.Author.Name; // ์ด ์์ ์ Author ๋ก๋!
// ์ฅ์ : ํ์ํ ๋๋ง ๋ก๋, ์ฝ๋๊ฐ ๊ฐ๋จ
// ๋จ์ : N+1 ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ, ์์ธกํ๊ธฐ ์ด๋ ค์</comment>
โ ๏ธ ์ง์ฐ ๋ก๋ฉ ์ฌ์ฉ ์ ์ฃผ์์ฌํญ: ์ง์ฐ ๋ก๋ฉ์ ํธ๋ฆฌํ์ง๋ง N+1 ๋ฌธ์ ๋ฅผ ์ผ์ผํค๊ธฐ ์ฌ์. ํนํ ๋ฃจํ ์์์ ๋ค๋น๊ฒ์ด์ ์์ฑ์ ์ ๊ทผํ๋ฉด ๋งค๋ฒ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ผ. ์ค๋ฌด์์๋ ์ฆ์ ๋ก๋ฉ์ด๋ ๋ช ์์ ๋ก๋ฉ์ ๋ ๋ง์ด ์ฌ์ฉํ๋ ํธ์ด์ผ!
3๏ธโฃ ๋ช ์์ ๋ก๋ฉ (Explicit Loading)
// ํ์ํ ์์ ์ ๋ช
์์ ์ผ๋ก ๋ก๋
var post = await context.Posts.FirstAsync();
// ๋์ค์ Author๊ฐ ํ์ํด์ก์ ๋
await context.Entry(post)
.Reference(p => p.Author) // ๋จ์ผ ์ฐธ์กฐ
.LoadAsync();
// ์ปฌ๋ ์
๋ก๋
await context.Entry(post)
.Collection(p => p.Comments) // ์ปฌ๋ ์
.LoadAsync();
// ํํฐ๋งํ๋ฉด์ ๋ก๋
await context.Entry(post)
.Collection(p => p.Comments)
.Query()
.Where(c => c.IsApproved)
.LoadAsync();
// ์ฅ์ : ์ธ๋ฐํ ์ ์ด ๊ฐ๋ฅ, ์กฐ๊ฑด๋ถ ๋ก๋ฉ ๊ฐ๋ฅ
// ๋จ์ : ์ฝ๋๊ฐ ๋ณต์กํด์ง ์ ์์
๐ฏ ์ด๋ค ๋ฐฉ์์ ์ ํํด์ผ ํ ๊น?
์ฆ์ ๋ก๋ฉ (Include) ์ฌ์ฉ:
โข ๊ด๋ จ ๋ฐ์ดํฐ๊ฐ ํญ์ ํ์ํ ๊ฒฝ์ฐ
โข ํ ๋ฒ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ ํจ์จ์ ์ธ ๊ฒฝ์ฐ
โข API ์๋ต์ ๊ด๋ จ ๋ฐ์ดํฐ๊ฐ ํฌํจ๋์ด์ผ ํ๋ ๊ฒฝ์ฐ
๋ช
์์ ๋ก๋ฉ ์ฌ์ฉ:
โข ์กฐ๊ฑด์ ๋ฐ๋ผ ๊ด๋ จ ๋ฐ์ดํฐ๊ฐ ํ์ํ ๊ฒฝ์ฐ
โข ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ํํฐ๋งํด์ ๊ฐ์ ธ์์ผ ํ๋ ๊ฒฝ์ฐ
โข ์ธ๋ฐํ ์ ์ด๊ฐ ํ์ํ ๊ฒฝ์ฐ
์ง์ฐ ๋ก๋ฉ ์ฌ์ฉ:
โข ํ๋กํ ํ์
์ด๋ ๊ฐ๋จํ ์ ํ๋ฆฌ์ผ์ด์
โข ์ฑ๋ฅ์ด ํฌ๊ฒ ์ค์ํ์ง ์์ ๊ฒฝ์ฐ
โข (์ค๋ฌด์์๋ ๊ฐ๊ธ์ ํผํ๋ ๊ฒ ์ข์!)
๐ง 9. ์ฟผ๋ฆฌ ํํฐ์ ๊ธ๋ก๋ฒ ์ฟผ๋ฆฌ ํํฐ
๋ชจ๋ ์ฟผ๋ฆฌ์ ๊ณตํต์ ์ผ๋ก ์ ์ฉ๋์ด์ผ ํ๋ ์กฐ๊ฑด์ด ์๋ค๋ฉด, ๊ธ๋ก๋ฒ ์ฟผ๋ฆฌ ํํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ํธ๋ฆฌํ๊ณ ์์ ํด! ์๋ฅผ ๋ค์ด ์ํํธ ์ญ์ (Soft Delete)๋ ๋ฉํฐ ํ ๋์(Multi-tenancy) ๊ตฌํ์ ์ ์ฉํ์ง. ๐ก๏ธ
๐ฏ ๊ธ๋ก๋ฒ ์ฟผ๋ฆฌ ํํฐ ์ค์
// ์ํฐํฐ์ IsDeleted ์์ฑ ์ถ๊ฐ
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsDeleted { get; set; } // ์ํํธ ์ญ์ ํ๋๊ทธ
}
// OnModelCreating์์ ๊ธ๋ก๋ฒ ํํฐ ์ค์
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ์ญ์ ๋์ง ์์ ์ํ๋ง ์กฐํํ๋๋ก ํํฐ ์ค์
modelBuilder.Entity<product>()
.HasQueryFilter(p => !p.IsDeleted);
// ์ด์ ๋ชจ๋ ์ฟผ๋ฆฌ์ ์๋์ผ๋ก ์ ์ฉ๋จ!
}
// ์ฌ์ฉ
var products = await context.Products.ToListAsync();
// ์คํ๋๋ SQL: SELECT * FROM Products WHERE IsDeleted = 0
// ํํฐ๋ฅผ ๋ฌด์ํ๊ณ ์ถ์ ๋
var allProducts = await context.Products
.IgnoreQueryFilters() // ํํฐ ๋ฌด์
.ToListAsync();</product>
๐ข ๋ฉํฐ ํ ๋์ ๊ตฌํ ์์
// ํ์ฌ ํ
๋ํธ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ์๋น์ค
public interface ITenantProvider
{
int GetCurrentTenantId();
}
// DbContext์์ ์ฌ์ฉ
public class MyDbContext : DbContext
{
private readonly ITenantProvider _tenantProvider;
public MyDbContext(
DbContextOptions<mydbcontext> options,
ITenantProvider tenantProvider) : base(options)
{
_tenantProvider = tenantProvider;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ํ์ฌ ํ
๋ํธ์ ๋ฐ์ดํฐ๋ง ์กฐํํ๋๋ก ํํฐ ์ค์
modelBuilder.Entity<order>()
.HasQueryFilter(o => o.TenantId == _tenantProvider.GetCurrentTenantId());
modelBuilder.Entity<customer>()
.HasQueryFilter(c => c.TenantId == _tenantProvider.GetCurrentTenantId());
}
}
// ์ด์ ๋ชจ๋ ์ฟผ๋ฆฌ๊ฐ ์๋์ผ๋ก ํ์ฌ ํ
๋ํธ์ ๋ฐ์ดํฐ๋ง ์กฐํ!
// ์ค์๋ก ๋ค๋ฅธ ํ
๋ํธ์ ๋ฐ์ดํฐ์ ์ ๊ทผํ๋ ๊ฒ์ ๋ฐฉ์ง</customer></order></mydbcontext>
๐ ๊ธ๋ก๋ฒ ์ฟผ๋ฆฌ ํํฐ์ ์ฅ์ :
โข ๋ชจ๋ ์ฟผ๋ฆฌ์ ์๋ ์ ์ฉ๋์ด ์ค์ ๋ฐฉ์ง
โข ์ฝ๋ ์ค๋ณต ์ ๊ฑฐ
โข ๋ณด์ ๊ฐํ (ํนํ ๋ฉํฐ ํ
๋์)
โข ์ ์ง๋ณด์ ์ฉ์ด
๋จ, ํํฐ๊ฐ ์ ์ฉ๋๊ณ ์๋ค๋ ๊ฑธ ํญ์ ์ธ์งํ๊ณ ์์ด์ผ ํด. ๋๋ฒ๊น
ํ ๋ ํท๊ฐ๋ฆด ์ ์๊ฑฐ๋ !
๐ 10. ์ฟผ๋ฆฌ ๋ถํ ๊ณผ AsSplitQuery
์ฌ๋ฌ ์ปฌ๋ ์ ์ Includeํ ๋ ๋ฐ์ํ๋ ์นดํฐ์ ๊ณฑ(Cartesian Product) ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ด์ผ. ์ด๊ฒ ๋ญ๋๋ฉด, ์กฐ์ธ์ด ๋ง์์ง์๋ก ๊ฒฐ๊ณผ ํ ์๊ฐ ๊ธฐํ๊ธ์์ ์ผ๋ก ๋์ด๋๋ ํ์์ด์ง! ๐ต
๐ด ๋ฌธ์ ์ํฉ
// ๋ธ๋ก๊ทธ ํฌ์คํธ์ ํจ๊ป ๋๊ธ(10๊ฐ), ํ๊ทธ(5๊ฐ)๋ฅผ ๋ก๋
var posts = await context.Posts
.Include(p => p.Comments) // 10๊ฐ
.Include(p => p.Tags) // 5๊ฐ
.ToListAsync();
// ์คํ๋๋ SQL:
// SELECT * FROM Posts p
// LEFT JOIN Comments c ON p.Id = c.PostId
// LEFT JOIN Tags t ON p.Id = t.PostId
// ๋ฌธ์ : ๊ฒฐ๊ณผ ํ ์๊ฐ 1 * 10 * 5 = 50ํ!
// ์ค์ ๋ก๋ 1๊ฐ์ ํฌ์คํธ์ธ๋ฐ 50ํ์ ์ ์กํ๊ณ ์ฒ๋ฆฌํด์ผ ํจ
// ๋คํธ์ํฌ ํธ๋ํฝ ์ฆ๊ฐ + ๋ฉ๋ชจ๋ฆฌ ๋ญ๋น
โ AsSplitQuery๋ก ํด๊ฒฐ
// ์ฟผ๋ฆฌ๋ฅผ ๋ถํ ํด์ ์คํ
var posts = await context.Posts
.Include(p => p.Comments)
.Include(p => p.Tags)
.AsSplitQuery() // ์ฟผ๋ฆฌ ๋ถํ !
.ToListAsync();
// ์คํ๋๋ SQL (3๊ฐ์ ์ฟผ๋ฆฌ๋ก ๋ถํ ):
// 1. SELECT * FROM Posts
// 2. SELECT * FROM Comments WHERE PostId IN (...)
// 3. SELECT * FROM Tags WHERE PostId IN (...)
// ๊ฐ ์ฟผ๋ฆฌ๊ฐ ๋
๋ฆฝ์ ์ผ๋ก ์คํ๋์ด ์นดํฐ์
๊ณฑ ๋ฌธ์ ํด๊ฒฐ!
// ์ ์ก๋๋ ๋ฐ์ดํฐ: 1 + 10 + 5 = 16ํ (50ํ โ 16ํ)
โ๏ธ AsSplitQuery vs ๋จ์ผ ์ฟผ๋ฆฌ
๐ฏ ์ธ์ AsSplitQuery๋ฅผ ์ฌ์ฉํ ๊น?
AsSplitQuery ์ฌ์ฉ ๊ถ์ฅ:
โข ์ฌ๋ฌ ์ปฌ๋ ์
์ Includeํ ๋
โข ์ปฌ๋ ์
์ ํฌ๊ธฐ๊ฐ ํด ๋
โข ๋คํธ์ํฌ ํธ๋ํฝ์ด ์ค์ํ ๊ฒฝ์ฐ
โข ์นดํฐ์
๊ณฑ์ผ๋ก ์ธํ ์ค๋ณต ๋ฐ์ดํฐ๊ฐ ๋ง์ ๋
๋จ์ผ ์ฟผ๋ฆฌ ์ฌ์ฉ ๊ถ์ฅ:
โข ์ปฌ๋ ์
์ด 1๊ฐ์ด๊ฑฐ๋ ํฌ๊ธฐ๊ฐ ์์ ๋
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋ณต์ ์ต์ํํ๊ณ ์ถ์ ๋
โข ํธ๋์ญ์
์ผ๊ด์ฑ์ด ์ค์ํ ๊ฒฝ์ฐ
๐ ์ ์ญ ์ค์
// DbContext ์ค์ ์์ ๊ธฐ๋ณธ๊ฐ์ SplitQuery๋ก ๋ณ๊ฒฝ
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString)
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
}
// ์ด์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฟผ๋ฆฌ๊ฐ ๋ถํ ๋จ
// ํ์ํ ๊ฒฝ์ฐ .AsSingleQuery()๋ก ๋จ์ผ ์ฟผ๋ฆฌ ์ฌ์ฉ ๊ฐ๋ฅ
โ ๏ธ AsSplitQuery ์ฃผ์์ฌํญ:
โข ์ฌ๋ฌ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ฏ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋ณต ํ์ ์ฆ๊ฐ
โข ์ฟผ๋ฆฌ ์ฌ์ด์ ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ์ ์์ (์ผ๊ด์ฑ ๋ฌธ์ )
โข ํธ๋์ญ์
์์์ ์ฌ์ฉํ๋ ๊ฒ ์์
โข ์ฑ๋ฅ ํ
์คํธ๋ฅผ ํตํด ์ค์ ๋ก ๊ฐ์ ๋๋์ง ํ์ธ ํ์
๐ช 11. ์ฐ๊ฒฐ ํ๋ง๊ณผ DbContext ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ
DbContext์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ ๋๋ก ๊ด๋ฆฌํ์ง ์์ผ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์๋ ์ฑ๋ฅ ์ ํ๊ฐ ๋ฐ์ํ ์ ์์ด. ํนํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๋์ฑ ์ค์ํ์ง! ๐
โ ASP.NET Core์์์ ์ฌ๋ฐ๋ฅธ ์ฌ์ฉ
// Startup.cs ๋๋ Program.cs์์ DI ๋ฑ๋ก
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<mydbcontext>(options =>
options.UseSqlServer(connectionString));
// DbContext๋ ๊ธฐ๋ณธ์ ์ผ๋ก Scoped ์๋ช
์ฃผ๊ธฐ
// ๊ฐ HTTP ์์ฒญ๋ง๋ค ์๋ก์ด ์ธ์คํด์ค๊ฐ ์์ฑ๋๊ณ ์์ฒญ ์ข
๋ฃ ์ ์๋ ํด์
}
// ์ปจํธ๋กค๋ฌ์์ ์ฌ์ฉ
public class ProductsController : ControllerBase
{
private readonly MyDbContext _context;
// ์์ฑ์ ์ฃผ์
public ProductsController(MyDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<iactionresult> GetProducts()
{
var products = await _context.Products
.AsNoTracking()
.ToListAsync();
return Ok(products);
}
// ์์ฒญ์ด ๋๋๋ฉด ์๋์ผ๋ก Dispose๋จ!
}</iactionresult></mydbcontext>
๐ด ํผํด์ผ ํ ์ํฐํจํด
// โ ์๋ชป๋ ์์ 1: DbContext๋ฅผ Singleton์ผ๋ก ๋ฑ๋ก
services.AddDbContext<mydbcontext>(
options => options.UseSqlServer(connectionString),
ServiceLifetime.Singleton); // ์ ๋ ์ ๋ผ!
// ๋ฌธ์ ์ :
// - DbContext๋ ์ค๋ ๋ ์์ ํ์ง ์์
// - ๋ณ๊ฒฝ ์ถ์ ๋ฐ์ดํฐ๊ฐ ๊ณ์ ์์ฌ์ ๋ฉ๋ชจ๋ฆฌ ๋์
// - ๋์์ฑ ๋ฌธ์ ๋ฐ์
// โ ์๋ชป๋ ์์ 2: DbContext๋ฅผ ํ๋์ ์ ์ฅ
public class ProductService
{
private readonly MyDbContext _context; // ์ํ!
public ProductService(MyDbContext context)
{
_context = context; // ์ฅ์๊ฐ ๋ณด๊ดํ๋ฉด ์ ๋ผ
}
// ์ฌ๋ฌ ๋ฉ์๋์์ ๊ฐ์ ์ธ์คํด์ค ์ฌ์ฌ์ฉ
// ๋ณ๊ฒฝ ์ถ์ ๋ฐ์ดํฐ๊ฐ ๊ณ์ ์์
}
// โ ์๋ชป๋ ์์ 3: ์๋์ผ๋ก DbContext ์์ฑ
public void SomeMethod()
{
var context = new MyDbContext(); // using ์์ด ์์ฑ
var products = context.Products.ToList();
// Dispose ํธ์ถ ์ ํจ โ ์ฐ๊ฒฐ ๋์!
}</mydbcontext>
โ ์ฌ๋ฐ๋ฅธ ํจํด
// ํจํด 1: DI๋ฅผ ํตํ ์๋ ๊ด๋ฆฌ (๊ถ์ฅ)
public class ProductService
{
private readonly IDbContextFactory<mydbcontext> _contextFactory;
public ProductService(IDbContextFactory<mydbcontext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<list<product>> GetProductsAsync()
{
// ํ์ํ ๋๋ง๋ค ์๋ก์ด ์ปจํ
์คํธ ์์ฑ
await using var context = await _contextFactory.CreateDbContextAsync();
return await context.Products.ToListAsync();
// using์ผ๋ก ์๋ Dispose
}
}
// DbContextFactory ๋ฑ๋ก
services.AddDbContextFactory<mydbcontext>(options =>
options.UseSqlServer(connectionString));
// ํจํด 2: ๋ช
์์ using (์ฝ์ ์ฑ ๋ฑ)
public async Task ProcessData()
{
await using var context = new MyDbContext();
var products = await context.Products.ToListAsync();
// ๋ธ๋ก ์ข
๋ฃ ์ ์๋ Dispose
}</mydbcontext></list<product></mydbcontext></mydbcontext>
๐ DbContext ์๋ช
์ฃผ๊ธฐ ๋ฒ ์คํธ ํ๋ํฐ์ค:
โ ํด์ผ ํ ๊ฒ:
โข ์งง์ ์๋ช
์ฃผ๊ธฐ ์ ์ง (์์
๋จ์๋น ํ๋)
โข DI๋ฅผ ํตํ ์๋ ๊ด๋ฆฌ ํ์ฉ
โข using ๋ฌธ์ผ๋ก ๋ช
์์ ํด์
โข ์น ์ฑ์์๋ Scoped ์๋ช
์ฃผ๊ธฐ ์ฌ์ฉ
โ ํ์ง ๋ง์์ผ ํ ๊ฒ:
โข Singleton์ผ๋ก ๋ฑ๋ก
โข ์ฅ์๊ฐ ๋ณด๊ด
โข ์ฌ๋ฌ ์ค๋ ๋์์ ๊ณต์
โข Dispose ํธ์ถ ๋๋ฝ
๐ ์ฐ๊ฒฐ ํ๋ง ์ต์ ํ
// ์ฐ๊ฒฐ ๋ฌธ์์ด์ ํ๋ง ์ต์
์ค์
var connectionString =
"Server=localhost;" +
"Database=MyDb;" +
"User Id=sa;" +
"Password=pass;" +
"Pooling=true;" + // ํ๋ง ํ์ฑํ (๊ธฐ๋ณธ๊ฐ)
"Min Pool Size=5;" + // ์ต์ ์ฐ๊ฒฐ ์
"Max Pool Size=100;" + // ์ต๋ ์ฐ๊ฒฐ ์
"Connection Lifetime=300;" + // ์ฐ๊ฒฐ ์๋ช
(์ด)
"Connection Timeout=30;"; // ์ฐ๊ฒฐ ํ์์์
// ์ฐ๊ฒฐ ํ๋ง์ ์๋์ผ๋ก ๊ด๋ฆฌ๋๋ฏ๋ก ํน๋ณํ ์ฝ๋ ๋ถํ์
// DbContext๋ฅผ ์ ๋๋ก Disposeํ๋ฉด ์ฐ๊ฒฐ์ด ํ๋ก ๋ฐํ๋จ
๐ฌ 12. ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง๊ณผ ํ๋กํ์ผ๋ง
์ต์ ํ๋ฅผ ํ๋ค๋ฉด ์ค์ ๋ก ํจ๊ณผ๊ฐ ์๋์ง ์ธก์ ํด์ผ ํด! "์ธก์ ํ ์ ์์ผ๋ฉด ๊ฐ์ ํ ์ ์๋ค"๋ ๋ง์ด ์์์? ๐
๐ EF Core ๋ก๊น ํ์ฑํ
// ๊ฐ๋ฐ ํ๊ฒฝ์์ SQL ๋ก๊น
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information) // ์ฝ์ ์ถ๋ ฅ
.EnableSensitiveDataLogging() // ํ๋ผ๋ฏธํฐ ๊ฐ๋ ํ์
.EnableDetailedErrors(); // ์์ธ ์๋ฌ ์ ๋ณด
}
// ๋๋ ASP.NET Core์ ๋ก๊น
์์คํ
์ฌ์ฉ
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<mydbcontext>(options =>
options
.UseSqlServer(connectionString)
.LogTo(
Console.WriteLine,
new[] { DbLoggerCategory.Database.Command.Name },
LogLevel.Information));
}
// ์คํ๋๋ SQL๊ณผ ์คํ ์๊ฐ์ด ๋ก๊ทธ์ ์ถ๋ ฅ๋จ
// Executed DbCommand (45ms) [Parameters=[@p0='?' (DbType = Int32)],
// CommandType='Text', CommandTimeout='30']
// SELECT [p].[Id], [p].[Name] FROM [Products] AS [p] WHERE [p].[Id] = @p0</mydbcontext>
โฑ๏ธ ์ฟผ๋ฆฌ ์คํ ์๊ฐ ์ธก์
// Stopwatch๋ฅผ ์ฌ์ฉํ ๊ฐ๋จํ ์ธก์
using System.Diagnostics;
var stopwatch = Stopwatch.StartNew();
var products = await context.Products
.Include(p => p.Category)
.ToListAsync();
stopwatch.Stop();
Console.WriteLine($"์ฟผ๋ฆฌ ์คํ ์๊ฐ: {stopwatch.ElapsedMilliseconds}ms");
// ๋ ์ ๊ตํ ์ธก์ ์ ์ํ ํฌํผ ๋ฉ์๋
public static async Task<(T Result, long ElapsedMs)> MeasureAsync<t>(
Func<task<t>> action)
{
var sw = Stopwatch.StartNew();
var result = await action();
sw.Stop();
return (result, sw.ElapsedMilliseconds);
}
// ์ฌ์ฉ
var (products, elapsed) = await MeasureAsync(async () =>
await context.Products.ToListAsync());
Console.WriteLine($"์คํ ์๊ฐ: {elapsed}ms, ๊ฒฐ๊ณผ ๊ฐ์: {products.Count}");</task<t></t>
๐ MiniProfiler ์ฌ์ฉํ๊ธฐ
// NuGet ํจํค์ง ์ค์น
// MiniProfiler.AspNetCore.Mvc
// MiniProfiler.EntityFrameworkCore
// Startup.cs์์ ์ค์
public void ConfigureServices(IServiceCollection services)
{
services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
}).AddEntityFramework();
services.AddDbContext<mydbcontext>(options =>
options.UseSqlServer(connectionString));
}
public void Configure(IApplicationBuilder app)
{
app.UseMiniProfiler();
// ...
}
// ์ด์ /profiler/results ์์ ์์ธํ ์ฑ๋ฅ ์ ๋ณด ํ์ธ ๊ฐ๋ฅ
// - ๊ฐ ์ฟผ๋ฆฌ์ ์คํ ์๊ฐ
// - N+1 ๋ฌธ์ ๊ฐ์ง
// - ์ค๋ณต ์ฟผ๋ฆฌ ๊ฐ์ง
// - ์๊ฐ์ ์ธ ํ์๋ผ์ธ</mydbcontext>
๐ฏ ์ฑ๋ฅ ์ธก์ ์ฒดํฌ๋ฆฌ์คํธ:
1. ์ฟผ๋ฆฌ ์คํ ์๊ฐ
โข ๊ฐ ์ฟผ๋ฆฌ๊ฐ ์ผ๋ง๋ ๊ฑธ๋ฆฌ๋์ง ์ธก์
โข ๋ชฉํ: ๋๋ถ๋ถ์ ์ฟผ๋ฆฌ๊ฐ 100ms ์ด๋ด
2. ์คํ๋๋ ์ฟผ๋ฆฌ ๊ฐ์
โข N+1 ๋ฌธ์ ๊ฐ ์๋์ง ํ์ธ
โข ๋ชฉํ: ํ ํ๋ฉด์ 5๊ฐ ์ดํ์ ์ฟผ๋ฆฌ
3. ์ ์ก๋๋ ๋ฐ์ดํฐ ์
โข ๋ถํ์ํ ์ปฌ๋ผ์ ๊ฐ์ ธ์ค์ง ์๋์ง ํ์ธ
โข ๋ชฉํ: ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํ์ ์ผ๋ก ์กฐํ
4. ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋
โข ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐ ํ์ธ
โข ๋ชฉํ: ํ์ด์ง์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ ์ ํ
๐ญ ์คํ ๊ณํ ๋ถ์
// SQL Server Management Studio์์ ์คํ ๊ณํ ํ์ธ
// 1. EF Core ๋ก๊ทธ์์ ์์ฑ๋ SQL ๋ณต์ฌ
// 2. SSMS์์ "์ค์ ์คํ ๊ณํ ํฌํจ" ํ์ฑํ
// 3. ์ฟผ๋ฆฌ ์คํ ํ ์คํ ๊ณํ ํญ ํ์ธ
// ์ฃผ์ํด์ ๋ด์ผ ํ ๊ฒ๋ค:
// - Table Scan vs Index Seek
// โ Index Seek์ด ํจ์ฌ ๋น ๋ฆ
// - Key Lookup
// โ ์ธ๋ฑ์ค์ ํ์ํ ์ปฌ๋ผ์ด ์์ด์ ์ถ๊ฐ ์กฐํ ๋ฐ์
// - Nested Loops vs Hash Match
// โ ๋ฐ์ดํฐ ์์ ๋ฐ๋ผ ์ ์ ํ ์กฐ์ธ ๋ฐฉ์ ์ ํ
// - ๊ฒฝ๊ณ ํ์
// โ ์๋ฌต์ ๋ณํ, ๋๋ฝ๋ ์ธ๋ฑ์ค ๋ฑ
๐ 13. ๊ณ ๊ธ ์ต์ ํ ๊ธฐ๋ฒ
๊ธฐ๋ณธ์ ์ธ ์ต์ ํ๋ฅผ ๋ง์คํฐํ๋ค๋ฉด, ์ด์ ๋ ๊ณ ๊ธ ๊ธฐ๋ฒ๋ค์ ์์๋ณผ ์ฐจ๋ก์ผ! ์ด๋ฐ ๊ธฐ๋ฒ๋ค์ ํน์ ์ํฉ์์ ์์ฒญ๋ ์ฑ๋ฅ ํฅ์์ ๊ฐ์ ธ์ฌ ์ ์์ด. ๐ฏ
1๏ธโฃ ์์ SQL ์ฟผ๋ฆฌ ์ฌ์ฉ
๋๋ก๋ LINQ๋ก ํํํ๊ธฐ ์ด๋ ต๊ฑฐ๋ ๋นํจ์จ์ ์ธ ์ฟผ๋ฆฌ๊ฐ ์์ด. ๊ทธ๋ด ๋๋ ์์ SQL์ ์ง์ ์ฌ์ฉํ๋ ๊ฒ ๋์ ์ ์์ง:
// FromSqlRaw๋ฅผ ์ฌ์ฉํ ์์ SQL
var products = await context.Products
.FromSqlRaw(@"
SELECT p.*
FROM Products p
INNER JOIN Categories c ON p.CategoryId = c.Id
WHERE c.Name = {0} AND p.Price > {1}
ORDER BY p.SalesCount DESC",
categoryName, minPrice)
.ToListAsync();
// ์ฅ์ :
// 1. ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์ ์ด
// 2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ํนํ ๊ธฐ๋ฅ ์ฌ์ฉ ๊ฐ๋ฅ
// 3. ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ฌ์
// ์ฃผ์: SQL ์ธ์ ์
๋ฐฉ์ง๋ฅผ ์ํด ํ๋ผ๋ฏธํฐ ์ฌ์ฉ ํ์!
// ์ ์ฅ ํ๋ก์์ ํธ์ถ
var result = await context.Products
.FromSqlRaw("EXEC GetTopSellingProducts @CategoryId = {0}", categoryId)
.ToListAsync();
// ExecuteSqlRaw๋ก ์
๋ฐ์ดํธ/์ญ์ (๋ฐํ๊ฐ ์์)
var affectedRows = await context.Database
.ExecuteSqlRawAsync(@"
UPDATE Products
SET Price = Price * 1.1
WHERE CategoryId = {0}", categoryId);
2๏ธโฃ ๋ทฐ(View) ํ์ฉ
// ๋ณต์กํ ์กฐ์ธ์ด ํ์ํ ๊ฒฝ์ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ทฐ ์์ฑ
// SQL:
// CREATE VIEW vw_ProductSummary AS
// SELECT
// p.Id, p.Name, p.Price,
// c.Name AS CategoryName,
// COUNT(r.Id) AS ReviewCount,
// AVG(r.Rating) AS AverageRating
// FROM Products p
// LEFT JOIN Categories c ON p.CategoryId = c.Id
// LEFT JOIN Reviews r ON p.Id = r.ProductId
// GROUP BY p.Id, p.Name, p.Price, c.Name
// EF Core์์ ๋ทฐ๋ฅผ ์ํฐํฐ๋ก ๋งคํ
[Keyless] // ๋ทฐ๋ ๋ณดํต ๊ธฐ๋ณธ ํค๊ฐ ์์
public class ProductSummary
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string CategoryName { get; set; }
public int ReviewCount { get; set; }
public double AverageRating { get; set; }
}
// OnModelCreating์์ ๋ทฐ ๋งคํ
modelBuilder.Entity<productsummary>()
.ToView("vw_ProductSummary")
.HasNoKey();
// ์ฌ์ฉ - ๋ณต์กํ ์กฐ์ธ ์์ด ๊ฐ๋จํ๊ฒ!
var summaries = await context.Set<productsummary>()
.Where(s => s.AverageRating >= 4.0)
.ToListAsync();</productsummary></productsummary>
3๏ธโฃ ๊ณ์ฐ๋ ์ปฌ๋ผ(Computed Columns)
// ์์ฃผ ๊ณ์ฐ๋๋ ๊ฐ์ ๊ณ์ฐ๋ ์ปฌ๋ผ์ผ๋ก ์ ์ฅ
public class Order
{
public int Id { get; set; }
public decimal Subtotal { get; set; }
public decimal TaxRate { get; set; }
// ๊ณ์ฐ๋ ์ปฌ๋ผ (๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์๋ ๊ณ์ฐ)
public decimal TotalAmount { get; set; }
}
// OnModelCreating์์ ์ค์
modelBuilder.Entity<order>()
.Property(o => o.TotalAmount)
.HasComputedColumnSql("[Subtotal] * (1 + [TaxRate])");
// ์ด์ TotalAmount๋ ์๋์ผ๋ก ๊ณ์ฐ๋จ
// ์ฟผ๋ฆฌํ ๋๋ง๋ค ๊ณ์ฐํ ํ์ ์์!
// ์๊ตฌ ์ ์ฅ ์ต์
(stored)
modelBuilder.Entity<order>()
.Property(o => o.TotalAmount)
.HasComputedColumnSql("[Subtotal] * (1 + [TaxRate])", stored: true);
// stored: true๋ฉด ๊ฐ์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ์ ์ฅ๋์ด ๋ ๋น ๋ฆ</order></order>
4๏ธโฃ ์ฝ๊ธฐ ์ ์ฉ ๋ณต์ ๋ณธ ์ฌ์ฉ
// ์ฝ๊ธฐ/์ฐ๊ธฐ ๋ถ๋ฆฌ ์ํคํ
์ฒ
public class MyDbContext : DbContext
{
private readonly bool _isReadOnly;
public MyDbContext(DbContextOptions<mydbcontext> options, bool isReadOnly = false)
: base(options)
{
_isReadOnly = isReadOnly;
if (_isReadOnly)
{
// ์ฝ๊ธฐ ์ ์ฉ ๋ชจ๋์์๋ ๋ณ๊ฒฝ ์ถ์ ๋นํ์ฑํ
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
ChangeTracker.AutoDetectChangesEnabled = false;
}
}
}
// DI ๋ฑ๋ก
services.AddDbContext<mydbcontext>(options =>
options.UseSqlServer(writeConnectionString));
services.AddDbContext<mydbcontext>(options =>
options.UseSqlServer(readReplicaConnectionString),
contextLifetime: ServiceLifetime.Scoped,
optionsLifetime: ServiceLifetime.Singleton);
// ์ฌ์ฉ
public class ProductService
{
private readonly MyDbContext _writeContext;
private readonly MyDbContext _readContext;
public ProductService(
[FromKeyedServices("write")] MyDbContext writeContext,
[FromKeyedServices("read")] MyDbContext readContext)
{
_writeContext = writeContext;
_readContext = readContext;
}
// ์ฝ๊ธฐ๋ ๋ณต์ ๋ณธ์์
public async Task<list<product>> GetProductsAsync()
{
return await _readContext.Products.ToListAsync();
}
// ์ฐ๊ธฐ๋ ๋ง์คํฐ์์
public async Task AddProductAsync(Product product)
{
_writeContext.Products.Add(product);
await _writeContext.SaveChangesAsync();
}
}</list<product></mydbcontext></mydbcontext></mydbcontext>
๐ ๊ณ ๊ธ ์ต์ ํ ๊ธฐ๋ฒ ์ฌ์ฉ ์๊ธฐ:
์์ SQL: ๋ณต์กํ ์ง๊ณ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ํนํ ๊ธฐ๋ฅ ํ์ ์
๋ทฐ: ๋ณต์กํ ์กฐ์ธ์ด ๋ฐ๋ณต์ ์ผ๋ก ํ์ํ ๋
๊ณ์ฐ๋ ์ปฌ๋ผ: ์์ฃผ ๊ณ์ฐ๋๋ ๊ฐ์ด ์์ ๋
์ฝ๊ธฐ ๋ณต์ ๋ณธ: ์ฝ๊ธฐ ๋ถํ๊ฐ ๋์ ๋๊ท๋ชจ ์์คํ
๋จ, ์ด๋ฐ ๊ธฐ๋ฒ๋ค์ ๋ณต์ก๋๋ฅผ ์ฆ๊ฐ์ํค๋ฏ๋ก ์ ๋ง ํ์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํด!
๐ผ ์ค์ ์ฌ๋ก ์ฐ๊ตฌ: ์ ์์๊ฑฐ๋ ์์คํ ์ต์ ํ
์ด๋ก ๋ง์ผ๋ก๋ ๋ถ์กฑํ์ง! ์ค์ ํ๋ก์ ํธ์์ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง ์ฌ๋ก๋ฅผ ๊ณต์ ํด๋ณผ๊ฒ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ด๋ฐ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ด ์ค์ ๋ก ์ฌ์ฉ๋๊ณ ์์ด! ๐๏ธ
๐ ๋ฌธ์ ์ํฉ
์ด๊ธฐ ์ํ:
โข ์ํ ๋ชฉ๋ก ํ์ด์ง ๋ก๋ฉ ์๊ฐ: 8-12์ด
โข ๋์ ์ฌ์ฉ์ 50๋ช
์ด์์ผ ๋ ์๋ฒ ์๋ต ์์
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค CPU ์ฌ์ฉ๋ฅ : 90% ์ด์
โข ์ฌ์ฉ์ ๋ถ๋ง ํญ์ฃผ ๐ญ
๐ด ๋ฌธ์ ๊ฐ ์๋ ์ฝ๋
// ์ต์
์ ์ฝ๋ (์ค์ ๋ก ์ด๋ ๊ฒ ์์ฑ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์!)
public async Task<list<productdto>> GetProducts(int page)
{
var products = await _context.Products
.ToListAsync(); // ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋!
var result = new List<productdto>();
foreach (var product in products)
{
// ๊ฐ ์ํ๋ง๋ค ๊ฐ๋ณ ์ฟผ๋ฆฌ ์คํ (N+1 ๋ฌธ์ )
var category = await _context.Categories
.FirstOrDefaultAsync(c => c.Id == product.CategoryId);
var reviews = await _context.Reviews
.Where(r => r.ProductId == product.Id)
.ToListAsync();
var images = await _context.ProductImages
.Where(i => i.ProductId == product.Id)
.ToListAsync();
result.Add(new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
CategoryName = category?.Name,
ReviewCount = reviews.Count,
AverageRating = reviews.Any() ? reviews.Average(r => r.Rating) : 0,
ImageUrls = images.Select(i => i.Url).ToList()
});
}
// ํ์ด์ง์ ๋ฉ๋ชจ๋ฆฌ์์ ์ฒ๋ฆฌ!
return result
.Skip((page - 1) * 20)
.Take(20)
.ToList();
}
// ์คํ๋๋ ์ฟผ๋ฆฌ ๊ฐ์: 1 + (N * 3) = 1 + (10000 * 3) = 30,001๊ฐ!
// ์ ์ก๋๋ ๋ฐ์ดํฐ: ์๋ฐฑ MB
// ์คํ ์๊ฐ: 8-12์ด</productdto></list<productdto>
โ ์ต์ ํ๋ ์ฝ๋
// ์ต์ ํ๋ ๋ฒ์
public async Task<pagedresult<productdto>> GetProducts(int page, int pageSize = 20)
{
// 1๋จ๊ณ: ํ์ํ ๋ฐ์ดํฐ๋ง ํ๋ก์ ์
+ ํ์ด์ง
var query = _context.Products
.AsNoTracking() // ์ฝ๊ธฐ ์ ์ฉ
.Where(p => p.IsActive) // ํ์ฑ ์ํ๋ง
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name, // ์กฐ์ธ ์๋ ์ฒ๋ฆฌ
ReviewCount = p.Reviews.Count(), // ์ง๊ณ ํจ์
AverageRating = p.Reviews.Any()
? p.Reviews.Average(r => r.Rating)
: 0,
ImageUrls = p.Images
.OrderBy(i => i.Order)
.Take(3) // ์ฒ์ 3๊ฐ๋ง
.Select(i => i.Url)
.ToList()
});
// 2๋จ๊ณ: ์ ์ฒด ๊ฐ์ ์กฐํ (์บ์ฑ ๊ฐ๋ฅ)
var totalCount = await query.CountAsync();
// 3๋จ๊ณ: ํ์ด์ง ์ ์ฉ
var items = await query
.OrderBy(p => p.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<productdto>
{
Items = items,
TotalCount = totalCount,
PageNumber = page,
PageSize = pageSize
};
}
// ์คํ๋๋ ์ฟผ๋ฆฌ ๊ฐ์: 2๊ฐ (count + select)
// ์ ์ก๋๋ ๋ฐ์ดํฐ: ์์ญ KB
// ์คํ ์๊ฐ: 0.2-0.5์ด</productdto></pagedresult<productdto>
๐ ์ต์ ํ ๊ฒฐ๊ณผ
์ต์ ํ ํ:
โข ์ํ ๋ชฉ๋ก ํ์ด์ง ๋ก๋ฉ ์๊ฐ: 0.3-0.5์ด (96% ๊ฐ์ ! ๐)
โข ๋์ ์ฌ์ฉ์ 500๋ช
๊น์ง ์์ ์ ์ฒ๋ฆฌ
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค CPU ์ฌ์ฉ๋ฅ : 20% ์ดํ
โข ์๋ฒ ๋น์ฉ 50% ์ ๊ฐ
โข ์ฌ์ฉ์ ๋ง์กฑ๋ ๋ํญ ์์น ๐
๐ฏ ์ถ๊ฐ ์ต์ ํ
// ์ธ๋ฑ์ค ์ถ๊ฐ
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ์์ฃผ ๊ฒ์๋๋ ์ปฌ๋ผ์ ์ธ๋ฑ์ค
modelBuilder.Entity<product>()
.HasIndex(p => new { p.IsActive, p.CategoryId, p.Name });
// ์ธ๋ ํค์ ์ธ๋ฑ์ค
modelBuilder.Entity<review>()
.HasIndex(r => r.ProductId);
modelBuilder.Entity<productimage>()
.HasIndex(i => new { i.ProductId, i.Order });
}
// ์ปดํ์ผ๋ ์ฟผ๋ฆฌ๋ก ๋์ฑ ์ต์ ํ
private static readonly Func<mydbcontext, int,="" task<list<productdto="">>>
_getProductsQuery = EF.CompileAsyncQuery(
(MyDbContext context, int page, int pageSize) =>
context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
// ... ํ๋ก์ ์
})
.OrderBy(p => p.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList());
// ์บ์ฑ ์ถ๊ฐ (Redis ๋๋ ๋ฉ๋ชจ๋ฆฌ ์บ์)
public async Task<pagedresult<productdto>> GetProductsCached(int page)
{
var cacheKey = $"products_page_{page}";
if (_cache.TryGetValue(cacheKey, out PagedResult<productdto> cached))
{
return cached;
}
var result = await GetProducts(page);
_cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
return result;
}</productdto></pagedresult<productdto></mydbcontext,></productimage></review></product>
๐ ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ์ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์ ๋ฆฌํด์ ์ค๋ฌด์์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๋ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด๋ดค์ด! ์๋ก์ด ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๊ฑฐ๋ ๊ธฐ์กด ์ฝ๋๋ฅผ ๋ฆฌ๋ทฐํ ๋ ์ด ๋ฆฌ์คํธ๋ฅผ ํ์ธํด๋ด. โ
๐ฏ ์ฟผ๋ฆฌ ์์ฑ ์ ์ฒดํฌ๋ฆฌ์คํธ:
โก AsNoTracking ์ฌ์ฉ
์ฝ๊ธฐ ์ ์ฉ ์ฟผ๋ฆฌ์๋ ํญ์ AsNoTracking์ ์ถ๊ฐํ๋์?
โก Include๋ก N+1 ๋ฐฉ์ง
๊ด๋ จ ์ํฐํฐ๋ฅผ Include๋ก ๋ฏธ๋ฆฌ ๋ก๋ํ๋์?
โก Select ํ๋ก์ ์
ํ์ํ ์ปฌ๋ผ๋ง ์ ํํด์ ๊ฐ์ ธ์ค๋์?
โก ํ์ด์ง ์ ์ฉ
๋๋ ๋ฐ์ดํฐ๋ ํ์ด์ง์ผ๋ก ๋๋ ์ ์กฐํํ๋์?
โก Where ์กฐ๊ฑด ์ต์ ํ
์ธ๋ฑ์ค๋ฅผ ํ์ฉํ ์ ์๋ ์กฐ๊ฑด์ ์ฌ์ฉํ๋์?
โก ๋ถํ์ํ ์ ๋ ฌ ์ ๊ฑฐ
๊ผญ ํ์ํ ๊ฒฝ์ฐ์๋ง OrderBy๋ฅผ ์ฌ์ฉํ๋์?
๐ ์ฑ๋ฅ ์ต์ ํ ์ฐ์ ์์:
1์์ (๊ฐ์ฅ ํฐ ํจ๊ณผ):
โข N+1 ๋ฌธ์ ํด๊ฒฐ (Include ์ฌ์ฉ)
โข AsNoTracking ์ ์ฉ
โข ํ์ด์ง ๊ตฌํ
2์์ (์ค๊ฐ ํจ๊ณผ):
โข Select ํ๋ก์ ์
โข ์ ์ ํ ์ธ๋ฑ์ค ์ถ๊ฐ
โข ๋ถํ์ํ ์ฟผ๋ฆฌ ์ ๊ฑฐ
3์์ (์ธ๋ฐํ ์ต์ ํ):
โข ์ปดํ์ผ๋ ์ฟผ๋ฆฌ
โข Split Query
โข ๋ฐฐ์น ์ฒ๋ฆฌ
ํญ์ ์ธก์ โ ์ต์ ํ โ ์ฌ์ธก์ ์์๋ก ์งํํด!
๐ซ ํผํด์ผ ํ ์ํฐํจํด
์ ๋ ํ์ง ๋ง์์ผ ํ ๊ฒ๋ค:
1. ToList() ํ LINQ ์ฌ์ฉ
var result = context.Products.ToList().Where(p => p.Price > 100)โ ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ๋ก ๊ฐ์ ธ์จ ํ ํํฐ๋ง! ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํํฐ๋งํด์ผ ํด
2. ๋ฃจํ ์์์ SaveChanges
foreach(var item in items) { context.Add(item); context.SaveChanges(); }โ ๋ฃจํ ๋ฐ์์ ํ ๋ฒ๋ง ํธ์ถ!
3. Select ์์ด ์ ์ฒด ์ํฐํฐ ์กฐํ
var users = context.Users.ToList()โ ํ์ํ ์ปฌ๋ผ๋ง Select๋ก ๊ฐ์ ธ์ค๊ธฐ
4. ์ง์ฐ ๋ก๋ฉ์ ์์กด
foreach(var post in posts) { var author = post.Author.Name; }โ Include๋ก ๋ฏธ๋ฆฌ ๋ก๋!
5. ํ์ด์ง ์์ด ๋๋ ๋ฐ์ดํฐ ์กฐํ
var allOrders = context.Orders.ToList()โ Skip/Take๋ก ํ์ด์ง ํ์!
๐ ์ถ๊ฐ ํ์ต ๋ฆฌ์์ค
๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด:
๊ณต์ ๋ฌธ์:
โข Microsoft EF Core ์ฑ๋ฅ ๊ฐ์ด๋
โข EF Core ์ฟผ๋ฆฌ ์๋ ๋ฐฉ์
๋๊ตฌ:
โข EF Core Power Tools (Visual Studio ํ์ฅ)
โข LINQPad (์ฟผ๋ฆฌ ํ
์คํธ ๋๊ตฌ)
โข SQL Server Profiler
์ปค๋ฎค๋ํฐ:
โข Stack Overflow์ EF Core ํ๊ทธ
โข GitHub EF Core ์ ์ฅ์
โข ์ฌ๋ฅ๋ท์ ํ๋ก๊ทธ๋๋ฐ ์นดํ
๊ณ ๋ฆฌ์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ง์ ๊ณต์ !
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ฝ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ฐ๋ฆฌ๊ฐ ํจ๊ป ์ดํด๋ณธ EF Core ์ฑ๋ฅ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ:
๐ฏ ํต์ฌ ์์ฝ:
1. N+1 ๋ฌธ์ ํด๊ฒฐ - Include๋ก ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋
2. AsNoTracking - ์ฝ๊ธฐ ์ ์ฉ ์ฟผ๋ฆฌ์ ์ฑ๋ฅ ํฅ์
3. Select ํ๋ก์ ์
- ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํ์ ์ผ๋ก ์กฐํ
4. ํ์ด์ง - ๋๋ ๋ฐ์ดํฐ๋ฅผ ๋๋ ์ ์ฒ๋ฆฌ
5. ์ปดํ์ผ๋ ์ฟผ๋ฆฌ - ๋ฐ๋ณต ์ฟผ๋ฆฌ์ ๋ณํ ๋น์ฉ ์ ๊ฐ
6. ์ธ๋ฑ์ค ์ต์ ํ - ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ๋ฒจ ์ฑ๋ฅ ํฅ์
7. ๋ฐฐ์น ์ฒ๋ฆฌ - ๋ฒํฌ ์์
์ผ๋ก ํจ์จ์ฑ ๊ทน๋ํ
8. ์ ์ ํ ๋ก๋ฉ ์ ๋ต - ์ํฉ์ ๋ง๋ ๋ก๋ฉ ๋ฐฉ์ ์ ํ
9. ์ฟผ๋ฆฌ ํํฐ - ๊ณตํต ์กฐ๊ฑด์ ์๋ ์ ์ฉ
10. Split Query - ์นดํฐ์
๊ณฑ ๋ฌธ์ ํด๊ฒฐ
11. DbContext ๊ด๋ฆฌ - ์ฌ๋ฐ๋ฅธ ์๋ช
์ฃผ๊ธฐ ๊ด๋ฆฌ
12. ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง - ์ธก์ ๊ณผ ๋ถ์์ ์ค์์ฑ
13. ๊ณ ๊ธ ๊ธฐ๋ฒ - ์์ SQL, ๋ทฐ, ๊ณ์ฐ๋ ์ปฌ๋ผ ๋ฑ
์ด๋ฐ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ๋จ์ํ "๋น ๋ฅด๊ฒ ๋ง๋๋ ๊ฒ" ์ด์์ ์๋ฏธ๊ฐ ์์ด. ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฐ์ ํ๊ณ , ์๋ฒ ๋น์ฉ์ ์ ๊ฐํ๋ฉฐ, ์์คํ ์ ํ์ฅ์ฑ์ ๋์ด๋ ๊ฑฐ์ง. ํนํ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ง์ ์ฌ์ฉ์๋ค์๊ฒ ์๋น์ค๋ฅผ ์ ๊ณตํ ๋๋ ๋์ฑ ์ค์ํด! ๐
๊ธฐ์ตํด! ์ต์ ํ๋ ์ธก์ โ ๋ถ์ โ ๊ฐ์ โ ์ฌ์ธก์ ์ ๋ฐ๋ณต์ด์ผ. ๋ฌด์์ ๋ชจ๋ ๊ธฐ๋ฒ์ ์ ์ฉํ๋ ๊ฒ ์๋๋ผ, ์ค์ ๋ก ๋ณ๋ชฉ์ด ๋๋ ๋ถ๋ถ์ ์ฐพ์์ ๊ฐ์ ํ๋ ๊ฒ ์ค์ํด. ๊ทธ๋ฆฌ๊ณ ํญ์ ์ฝ๋์ ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ๋ ํจ๊ป ๊ณ ๋ คํด์ผ ํด.
์ค๋ฌด์์ ์ด๋ฐ ๊ธฐ๋ฒ๋ค์ ์ ์ฉํ๋ค ๋ณด๋ฉด ์ฒ์์๋ ์ด๋ ค์ธ ์ ์์ด. ํ์ง๋ง ํ๋์ฉ ์ ์ฉํด๋ณด๊ณ , ๊ฒฐ๊ณผ๋ฅผ ์ธก์ ํ๋ฉด์ ๊ฒฝํ์ ์๋ค ๋ณด๋ฉด ์ด๋์ ์ฑ๋ฅ ์ต์ ํ์ ๋ฌ์ธ์ด ๋์ด ์์ ๊ฑฐ์ผ! ๐ช
ํน์ ๋ ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋, ์ค์ ํ๋ก์ ํธ์์ ์ ์ฉํ๋ค๊ฐ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ฌ๋ฅ๋ท ์ปค๋ฎค๋ํฐ์์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ํตํด๋ด. ํจ๊ป ๋ฐฐ์ฐ๊ณ ์ฑ์ฅํ๋ ๊ฒ ๊ฐ๋ฐ์์ ๋งค๋ ฅ์ด์์? ๐
์, ์ด์ ๋น์ ์ ํ๋ก์ ํธ๋ก ๋์๊ฐ์ ์ค๋ ๋ฐฐ์ด ๋ด์ฉ์ ์ ์ฉํด๋ณผ ์๊ฐ์ด์ผ! ์ฑ๋ฅ์ด 10๋ฐฐ ๋นจ๋ผ์ง ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค์ด๋ณด์! ํ์ดํ ! ๐ฅ
๐ ์ด ๊ธ์ด ๋์์ด ๋์๋์?
์ฌ๋ฅ๋ท์์ ๋ ๋ง์ ํ๋ก๊ทธ๋๋ฐ ์ง์์ ๊ณต์ ํ๊ณ ,
๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ํจ๊ป ์ฑ์ฅํ์ธ์!
๐ป Happy Coding! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.