์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ Entity Framework Core ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ๋ฒ•: ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์จ๋จน๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿš€ Entity Framework Core ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ๋ฒ•: ์‹ค์ „์—์„œ ๋ฐ”๋กœ ์จ๋จน๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ์†๋„๋ฅผ 10๋ฐฐ ๋น ๋ฅด๊ฒŒ ๋งŒ๋“œ๋Š” ๋น„๋ฒ• ๋Œ€๊ณต๊ฐœ! ๐Ÿ’จ

์•ˆ๋…•! ๐Ÿ˜Š ์˜ค๋Š˜์€ C# ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ํ•œ ๋ฒˆ์ฏค ๊ณ ๋ฏผํ•ด๋ดค์„ ์ฃผ์ œ, ๋ฐ”๋กœ Entity Framework Core(EF Core) ์„ฑ๋Šฅ ์ตœ์ ํ™”์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ํ˜น์‹œ "์™œ ๋‚ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์ด๋ ‡๊ฒŒ ๋А๋ฆด๊นŒ?" ํ•˜๊ณ  ๊ณ ๋ฏผํ•ด๋ณธ ์  ์žˆ์–ด? ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, ๋ฌธ์ œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ ๋ฐฉ์‹์— ์žˆ๊ฑฐ๋“ !

EF Core๋Š” ์ •๋ง ํŽธ๋ฆฌํ•œ ORM(Object-Relational Mapping) ๋„๊ตฌ์•ผ. ํ•˜์ง€๋งŒ ์ž˜๋ชป ์‚ฌ์šฉํ•˜๋ฉด ์„ฑ๋Šฅ์ด ๊ธ‰๊ฒฉํžˆ ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์–ด. ๋งˆ์น˜ ๊ณ ๊ธ‰ ์Šคํฌ์ธ ์นด๋ฅผ 1๋‹จ ๊ธฐ์–ด๋กœ๋งŒ ์šด์ „ํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ง€. ๐ŸŽ๏ธ

์ด ๊ธ€์—์„œ๋Š” ์‹ค๋ฌด์—์„œ ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์ฒด์ ์ธ ์ตœ์ ํ™” ๊ธฐ๋ฒ•๋“ค์„ ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•˜๋“ฏ ์‰ฝ๊ฒŒ ํ’€์–ด๋ณผ๊ฒŒ. ์žฌ๋Šฅ๋„ท์—์„œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ด€๋ จ ์ง€์‹์„ ๊ณต์œ ํ•˜๋Š” ๋งŒํผ, ์‹ค์ „ ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ํ•œ ํŒ๋“ค์„ ๊ฐ€๋“ ๋‹ด์•˜์–ด!

๋А๋ฆฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๋น ๋ฅธ ์„ฑ๋Šฅ N+1 ๋ฌธ์ œ ๋ถˆํ•„์š”ํ•œ ๋กœ๋”ฉ Eager Loading AsNoTracking 10๋ฐฐ ํ–ฅ์ƒ ์ตœ์  ์„ฑ๋Šฅ

๐ŸŽฏ ์™œ 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>
์„ฑ๋Šฅ ์ตœ์ ํ™” ํšจ๊ณผ ๋น„๊ต ์ตœ์ ํ™” ์ „ ๋ถ€๋ถ„ ์ตœ์ ํ™” ์™„์ „ ์ตœ์ ํ™” 15์ดˆ 3์ดˆ 0.5์ดˆ

๐Ÿ’พ 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 ์ฃผ์˜์‚ฌํ•ญ: โ€ข ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์™•๋ณต ํšŸ์ˆ˜ ์ฆ๊ฐ€
โ€ข ์ฟผ๋ฆฌ ์‚ฌ์ด์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Œ (์ผ๊ด€์„ฑ ๋ฌธ์ œ)
โ€ข ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์•ˆ์ „
โ€ข ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์‹ค์ œ๋กœ ๊ฐœ์„ ๋˜๋Š”์ง€ ํ™•์ธ ํ•„์š”

EF Core ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ๋ณ€๊ฒฝ ์ถ”์  ์ธ๋ฑ์‹ฑ Include ์‚ฌ์šฉ Select ํ”„๋กœ์ ์…˜ ํŽ˜์ด์ง• ์ ์šฉ ์ปดํŒŒ์ผ๋œ ์ฟผ๋ฆฌ Eager Loading Split Query ํ•„์š”ํ•œ ๊ฒƒ๋งŒ AsNoTracking ์ฝ๊ธฐ ์ „์šฉ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ ์ ˆํ•œ ์ธ๋ฑ์Šค ๋ณตํ•ฉ ์ธ๋ฑ์Šค โœ“ โœ“ โœ“ โœ“

๐ŸŽช 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
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.